diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..10f3e1c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +certs/ diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 0000000..a2715dd --- /dev/null +++ b/nodemon.json @@ -0,0 +1,6 @@ +{ + "ignore": [ + "public/**/*.js", + "*.json" + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f4eb5af --- /dev/null +++ b/package-lock.json @@ -0,0 +1,44 @@ +{ + "name": "openterminal", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "openterminal", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "ws": "^8.14.2" + } + }, + "node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + }, + "dependencies": { + "ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "requires": {} + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c6eb13f --- /dev/null +++ b/package.json @@ -0,0 +1,23 @@ +{ + "name": "openterminal", + "version": "1.0.0", + "description": "", + "main": "./server/main.js", + "scripts": { + "start": "node ./server/main.js", + "dev": "nodemon ./server/main.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/mellodoot/openterminal.git" + }, + "author": "ari melody", + "license": "ISC", + "bugs": { + "url": "https://github.com/mellodoot/openterminal/issues" + }, + "homepage": "https://github.com/mellodoot/openterminal#readme", + "dependencies": { + "ws": "^8.14.2" + } +} diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..ebb0c36 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/index.html b/public/index.html index d466cc0..3731f6e 100644 --- a/public/index.html +++ b/public/index.html @@ -1,17 +1,18 @@ - Open Terminal + OpenTerminal - + + - +

-			
+
diff --git a/public/scripts/main.js b/public/scripts/main.js index b08ce5d..c4a7efc 100644 --- a/public/scripts/main.js +++ b/public/scripts/main.js @@ -1,45 +1,106 @@ -var TERM_INTERVAL = 0; var buffer = ""; +var send_buffer = ""; var content; - -const banner = -`OpenTerminal v0.1.0 -made with <3 by ari melody - -`; +var client; +var pre_buffer_chars = 0; +var term_interval = 10; +var ready = false; function start() { + console.log("%chello, world!", "color: #b7fd49; font-size: 3rem; font-weight: bold"); + console.log( +`welcome to OpenTerminal! +home to a little shared text buffer. + +i hope you enjoy your stay here! +to help you feel a little more comfortable, i've prepared some commands for you: + +- set_colours(foreground, background) + changes the foreground and background colours of your terminal! + \`foreground\` and \`background\` must be hex colour codes, such as \`#ff00ff\`. + +- set_palette(palette) + changes the foreground and background colours of your terminal to one of our many options of premade themes! including but not limited to the entire collection of catppuccin mocha colours! (i really like their palette ;p) + + try it out! type \`PALETTE.\` into your console and browse the list of themes we have!`); + + const foreground = localStorage.getItem("foreground"); + const background = localStorage.getItem("background"); + if (foreground && background) { + set_colours(foreground, background); + } + content = document.getElementById("content"); - send_text(banner); + + buffer += "Connecting to the server..."; + + setTimeout(connect, 500); + loop(); } function loop() { if (buffer.length > 0) { - const carat = content.querySelector("#carat"); - if (carat) carat.remove(); - - const char = buffer.slice(0, 1); - if (char == "\b") { - content.innerText = content.innerText.slice(0, content.innerText.length - 1); - } else { - content.innerText += char; - } - buffer = buffer.slice(1); - - const new_carat = document.createElement("div"); - new_carat.id = "carat"; - content.appendChild(new_carat); + const char = buffer.substring(0, 1); + insert_text(char); + buffer = buffer.substring(1); } - setTimeout(loop, TERM_INTERVAL); + if (send_buffer.length > 0) { + const char = send_buffer.substring(0, 1); + client.send(char); + send_buffer = send_buffer.substring(1); + } + + setTimeout(loop, term_interval); +} + +function connect() { + client = new WebSocket("wss://localhost:8080"); + + client.addEventListener('open', () => { + // insert_text('\x00'); + buffer += "\nConnection successful.\n\n"; + buffer += "=== BEGIN SESSION ===\n\n"; + }); + + client.addEventListener('message', event => { + buffer += event.data; + if (pre_buffer_chars == 0) { + pre_buffer_chars = content.innerText.length + buffer.length; + } + }); + + client.addEventListener('close', () => { + insert_text("\n\n[CONNECTION LOST]"); + }); +} + +function insert_text(text) { + const carat = content.querySelector("#carat"); + if (carat) carat.remove(); + + if (text == "\x00") { + content.innerText = ""; + pre_buffer_chars = 0; + } else if (text == "\b" && content.innerText.length > pre_buffer_chars) { + content.innerText = content.innerText.slice(0, content.innerText.length - 1); + } else { + content.innerText += text; + } + + const new_carat = document.createElement("div"); + new_carat.id = "carat"; + content.appendChild(new_carat); } function handle_input(event) { - // console.debug(event.key); + if (event.key == "'") { + event.preventDefault(); + } if (event.key == "Backspace") { - if (event.ctrlKey) { + if (event.ctrlKey && send_buffer.length == 0) { const last_space = content.innerText.lastIndexOf(" "); const last_newline = content.innerText.lastIndexOf("\n"); @@ -49,34 +110,108 @@ function handle_input(event) { } const word_length = content.innerText.length - break_at; - send_text("\b".repeat(word_length)); + for (let i = 0; i < word_length; i++) { + send_buffer += '\b'; + } return; } - send_text("\b"); + send_buffer += '\b'; return; } - if (event.key.startsWith("Arrow")) { + if (event.key == "Enter") { + send_buffer += '\n'; return; } - switch (event.key) { - case 'Shift': - case 'Control': - case 'Alt': - return; - case 'Enter': - send_text('\n'); - break; - } if (event.key.length > 1) { return; } + if (event.ctrlKey) { + return; + } - send_text(event.key); + send_buffer += event.key; + content.scrollTop = content.scrollHeight; } -function send_text(char) { +function handle_paste(event) { + event.preventDefault(); + + if (send_buffer.length > 0) { + return; + } + + const paste = (event.clipboardData || window.clipboardData).getData("text"); + send_buffer += paste; content.scrollTop = content.scrollHeight; - buffer += char; +} + +const PALETTE = { + ari: + ["#b7fd49", "#111111"], + green: + ["#00ff00", "#111111"], + gold: + ["#f9cb16", "#111111"], + bsod: + ["#ffffff", "#0000ff"], + starlight: + ["#d2b660", "#110717"], + catppuccin: { + frappe: { + green: ["#a6d189", "#232634"], + }, + macchiato: { + green: ["#a6da95", "#24273a"], + }, + mocha: { + rosewater: ["#f9e2af", "#1e1e2e"], + flamingo: ["#f2cdcd", "#1e1e2e"], + pink: ["#f5c2e7", "#1e1e2e"], + mauve: ["#cba6f7", "#1e1e2e"], + red: ["#f38ba8", "#1e1e2e"], + maroon: ["#eba0ac", "#1e1e2e"], + peach: ["#fab387", "#1e1e2e"], + yellow: ["#f9e2af", "#1e1e2e"], + green: ["#a6e3a1", "#1e1e2e"], + teal: ["#94e2d5", "#1e1e2e"], + sky: ["#89dceb", "#1e1e2e"], + sapphire: ["#74c7ec", "#1e1e2e"], + blue: ["#89b4fa", "#1e1e2e"], + lavendar: ["#b4befe", "#1e1e2e"], + }, + }, + community: { + jorun: /* @jorun@meta.jorun.dev */ + ["#0080ff", "#0d1020"], + meowca: /* @meowcatheorange@moth.zone */ + ["#ff4000", "#130805"], + halloween: + ["#ff8000", "#1a120a"], + alcea: { + peach: + ["#cf4a7299", "#fff"], + purple: + ["#7f00ff", "#fff"], + }, + }, +}; + +function set_palette(palette) { + set_colours(palette[0], palette[1]); +} + +function set_colours(foreground, background) { + localStorage.setItem("foreground", foreground); + localStorage.setItem("background", background); + document.documentElement.style.setProperty('--colour', foreground); + document.documentElement.style.setProperty('--bgcolour', background); +} + +function clear_colours() { + localStorage.removeItem("foreground"); + localStorage.removeItem("background"); + document.documentElement.style.removeProperty('--colour'); + document.documentElement.style.removeProperty('--bgcolour'); } document.addEventListener("DOMContentLoaded", () => { @@ -84,4 +219,5 @@ document.addEventListener("DOMContentLoaded", () => { }); document.addEventListener("keydown", handle_input); +document.addEventListener("paste", handle_paste); diff --git a/public/styles/main.css b/public/styles/main.css index d978aef..795052a 100644 --- a/public/styles/main.css +++ b/public/styles/main.css @@ -1,19 +1,20 @@ :root { - --term-colour: #00ff00; + --colour: #a6e3a1; + --bgcolour: #1e1e2e; } body { margin: 0; padding: 0; - color: var(--term-colour); - background: #111; + color: var(--colour); + background-color: var(--bgcolour); font-family: monospace; font-size: 12px; } main { margin: 1rem; - border: 1px solid var(--term-colour); + border: 1px solid var(--colour); } pre#content { @@ -31,7 +32,7 @@ div#carat { width: .5em; height: .9em; display: inline-block; - background: var(--term-colour); + background: var(--colour); transform: translateY(1px); animation: linear .5s infinite forwards carat-blink; } diff --git a/res/favicon.afdesign b/res/favicon.afdesign new file mode 100644 index 0000000..688ece8 Binary files /dev/null and b/res/favicon.afdesign differ diff --git a/server/main.js b/server/main.js new file mode 100644 index 0000000..321937d --- /dev/null +++ b/server/main.js @@ -0,0 +1,122 @@ +const fs = require('fs'); +const https = require('https'); +const path = require('path'); +const Websocket = require('ws'); + +const config = { + cert: fs.readFileSync('./certs/cert.crt'), + key: fs.readFileSync('./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 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!", +]; + +const STATIC_PATH = path.join(process.cwd(), "public"); + +const banner = +`OpenTerminal v0.1.0 +made with <3 by ari melody + +`; + +let sockets = []; + +let buffer = ""; +let MAX_BUFFER_SIZE = 10240; + +async function get_file(url) { + const paths = [STATIC_PATH, url]; + if (url.endsWith("/")) paths.push("index.html"); + const file_path = path.join(...paths); + 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 }); + file.stream.pipe(res); + // console.log(`${req.method} - ${req.url}`); +}); + +const wss = new Websocket.Server({ server }); +wss.on('connection', socket => { + socket.send(banner + motds[Math.floor(Math.random() * motds.length)] + "\n\n"); + socket.send(buffer); + + sockets.push(socket); + + console.log(`new connection.\n\tcurrent connections: ${sockets.length}`); + + socket.on('message', handle_message); + + socket.on('close', () => { + sockets = sockets.filter(s => s !== socket); + console.log(`connection closed.\n\tcurrent connections: ${sockets.length}`); + }); +}); + +function handle_message(msg) { + if (msg == '\b') { + buffer = buffer.slice(0, buffer.length - 1); + send_text('\b'); + return; + } else if (buffer.length >= MAX_BUFFER_SIZE) { + return; + } + if (msg == '\n') { + buffer += '\n'; + send_text('\n'); + return; + } + if (msg.length > 1) { + return; + } + + buffer += msg.toString(); + send_text(msg.toString()); + + /* + if (buffer.length > MAX_BUFFER_SIZE) { + buffer = buffer.slice(buffer.length - MAX_BUFFER_SIZE, buffer.length); + } + */ +} + +server.listen(8080); + +function send_text(text) { + sockets.forEach(s => s.send(text)); +} +