From 9e33737e675cd7b489fba54634443a595b65f433 Mon Sep 17 00:00:00 2001 From: Stefan Friese Date: Thu, 13 Feb 2025 15:45:00 +0000 Subject: [PATCH] added help menu. --- main.go | 29 ++++++- static/help-command.js | 35 ++++++++ static/start-interactive.js | 161 ++++++++++++++++++++++-------------- static/stylesheet.css | 24 +++++- templates/index.html | 1 + 5 files changed, 184 insertions(+), 66 deletions(-) create mode 100644 static/help-command.js diff --git a/main.go b/main.go index 1223b34..d53500d 100644 --- a/main.go +++ b/main.go @@ -5,6 +5,7 @@ import ( // "bufio" "bytes" "embed" + "encoding/json" "fmt" "html/template" "io" @@ -509,6 +510,7 @@ func terminalHandler (w http.ResponseWriter, r *http.Request) { defer ws.Close() cmd := exec.Command("bash") + cmd.Env = append(os.Environ(), "TERM=xterm-256color") ptmx, err := pty.Start(cmd) if err != nil { log.Printf("Error starting PTY: %v", err) @@ -533,10 +535,35 @@ func terminalHandler (w http.ResponseWriter, r *http.Request) { }() for { - _, message, err := ws.ReadMessage() + _, message, err := ws.ReadMessage() if err != nil { break } + + // Check if the message is binary and starts with control prefix 0xFF. + if len(message) > 0 && message[0] == 0xFF { + var resizeMsg struct { + Type string `json:"type"` + Cols uint16 `json:"cols"` + Rows uint16 `json:"rows"` + } + if err := json.Unmarshal(message[1:], &resizeMsg); err == nil && resizeMsg.Type == "resize" { + err := pty.Setsize(ptmx, &pty.Winsize{ + Cols: resizeMsg.Cols, + Rows: resizeMsg.Rows, + X: 0, + Y: 0, + }) + if err != nil { + log.Printf("Error resizing PTY: %v", err) + } else { + log.Printf("Resized PTY to cols: %d, rows: %d", resizeMsg.Cols, resizeMsg.Rows) + } + continue // Do not write this message to the PTY. + } + } + + // Otherwise, treat it as normal input. if _, err := ptmx.Write(message); err != nil { break } diff --git a/static/help-command.js b/static/help-command.js new file mode 100644 index 0000000..1b9b371 --- /dev/null +++ b/static/help-command.js @@ -0,0 +1,35 @@ +const helpMsg = ` +This is a non interactive Webshell including some additional features to ease +communications between server and client. + Available Commands: + upload Upload files to the server through the file selector of the browser. + download Download files from the server to your local download directory. + theme Change the colorscheme of the shell. Type theme to get an overview of all colorschemes. + start-interactive Opens a bash shell in an interactive terminal. Type ctrl+d to exit the interactive shell. +` +// const helpMsg = 'This is a non interactive Webshell including some additional features to ease communications between server and client.\n Available Commands:\n upload\t\t\t\tUpload files to the server through the file selector of the browser.\n download \t\t\tDownload files from the server to your local download directory.\n theme \t\t\tChange the colorscheme of the shell. Type theme to get an overview of all colorschemes.\n start-interactive\t\t\tOpens a bash shell in an interactive terminal. Type ctrl+d to exi the interactive shell.' + +document.addEventListener("DOMContentLoaded", function() { + const input = document.getElementById("command-input"); + const terminal = document.getElementById("terminal"); + + input.addEventListener("keydown", function(event) { + if (event.key === "Enter") { + const command = input.value.trim(); + if (command.startsWith("help")) { + event.preventDefault(); + addLogEntry(helpMsg, 'info'); + input.value = ''; + } + } + }); + + function addLogEntry(message, type) { + const logEntry = document.createElement("div"); + logEntry.classList.add(type === 'error' ? 'error' : 'info'); + logEntry.textContent = message; + terminal.appendChild(logEntry); + terminal.scrollTop = terminal.scrollHeight; + } + +}); diff --git a/static/start-interactive.js b/static/start-interactive.js index d314e65..60d10e6 100644 --- a/static/start-interactive.js +++ b/static/start-interactive.js @@ -6,7 +6,6 @@ document.addEventListener("DOMContentLoaded", function () { let interactiveMode = false; const ansi_up = new AnsiUp; - // Listen for the "start-interactive" command in normal mode. input.addEventListener("keydown", function (event) { if (!interactiveMode && event.key === "Enter") { const command = input.value.trim(); @@ -21,77 +20,113 @@ document.addEventListener("DOMContentLoaded", function () { }); function startInteractiveSession() { - // Hide the normal terminal and input. - normalTerminal.style.display = "none"; - input.style.display = "none"; + interactiveMode = true; + // Hide the normal terminal and input. + normalTerminal.style.display = "none"; + input.style.display = "none"; - // Create a new container for xterm.js. - const xtermContainer = document.createElement("div"); - xtermContainer.id = "xterm-container"; - xtermContainer.style.width = "100%"; - xtermContainer.style.height = "100vh"; - // Optionally set additional styling (e.g. background color). - document.body.appendChild(xtermContainer); + // Create a new container for xterm.js. + const xtermContainer = document.createElement("div"); + xtermContainer.id = "xterm-container"; + xtermContainer.style.position = "fixed"; + xtermContainer.style.top = "0"; + xtermContainer.style.left = "0"; + xtermContainer.style.width = window.innerWidth + "px"; + xtermContainer.style.height = window.innerHeight + "px"; + xtermContainer.style.zIndex = "1000"; + document.body.appendChild(xtermContainer); - // Initialize xterm.js Terminal. - const term = new Terminal({ - cursorBlink: true, - scrollback: 1000, - theme: { - background: "#222", - foreground: "#eee" - } - }); - const fitAddon = new FitAddon.FitAddon(); - term.loadAddon(fitAddon); - term.open(xtermContainer); + const term = new Terminal({ + cursorBlink: true, + cursorStyle: 'block', + scrollback: 1000, + fontSize: 18, + theme: { + background: "#222", + foreground: "#eee" + } + }); + const fitAddon = new FitAddon.FitAddon(); + term.loadAddon(fitAddon); + term.open(xtermContainer); + setTimeout(() => { fitAddon.fit(); term.focus(); + console.log("Initial fit: container width =", xtermContainer.offsetWidth, "cols =", term.cols); + }, 100); - // Establish the WebSocket connection. - interactiveWS = new WebSocket("ws://" + location.host + "/terminal"); - interactiveWS.binaryType = "arraybuffer"; + interactiveWS = new WebSocket("ws://" + location.host + "/terminal"); + interactiveWS.binaryType = "arraybuffer"; - interactiveWS.onmessage = function (event) { - const text = new TextDecoder("utf-8").decode(event.data); - term.write(text); - }; + interactiveWS.onopen = function () { + sendResize(); + }; - interactiveWS.onclose = function () { - interactiveMode = false; - term.write("\r\n--- Interactive session ended ---\r\n"); - // Remove the xterm container. - if (xtermContainer.parentNode) { - xtermContainer.parentNode.removeChild(xtermContainer); - } - // Restore the normal terminal UI. - normalTerminal.style.display = "block"; - input.style.display = "block"; - input.focus(); - }; + interactiveWS.onmessage = function (event) { + const text = new TextDecoder("utf-8").decode(event.data); + term.write(text); + }; - interactiveWS.onerror = function (err) { - term.write("\r\n--- Error in interactive session ---\r\n"); - console.error("Interactive WS error:", err); - interactiveMode = false; - if (xtermContainer.parentNode) { - xtermContainer.parentNode.removeChild(xtermContainer); - } - normalTerminal.style.display = "block"; - input.style.display = "block"; - }; + interactiveWS.onclose = function () { + interactiveMode = false; + term.write("\r\n--- Interactive session ended ---\r\n"); + if (xtermContainer.parentNode) { + xtermContainer.parentNode.removeChild(xtermContainer); + } + window.removeEventListener("resize", handleResize); + normalTerminal.style.display = "block"; + input.style.display = "block"; + input.focus(); + }; - // When the user types in xterm, send the data to the server. - term.onData(function (data) { - // If the user presses Ctrl+D (ASCII 4), exit the session. - if (data === "\x04") { - term.write("\r\n--- Exiting interactive session ---\r\n"); - interactiveWS.close(); - } else { - interactiveWS.send(data); - } - }); + interactiveWS.onerror = function (err) { + term.write("\r\n--- Error in interactive session ---\r\n"); + console.error("Interactive WS error:", err); + interactiveMode = false; + if (xtermContainer.parentNode) { + xtermContainer.parentNode.removeChild(xtermContainer); + } + window.removeEventListener("resize", handleResize); + normalTerminal.style.display = "block"; + input.style.display = "block"; + }; - interactiveMode = true; + term.onData(function (data) { + if (data === "\x04") { + term.write("\r\n--- Exiting interactive session ---\r\n"); + interactiveWS.close(); + } else { + interactiveWS.send(data); + } + }); + + function handleResize() { + const newWidth = window.innerWidth; + const newHeight = window.innerHeight; + xtermContainer.style.width = newWidth + "px"; + xtermContainer.style.height = newHeight + "px"; + console.log("Resizing: new width =", newWidth, "new height =", newHeight); + fitAddon.fit(); + sendResize(); } + window.addEventListener("resize", handleResize); + + // Send a resize message using a custom control prefix (0xFF). + function sendResize() { + const resizeData = { + type: "resize", + cols: term.cols, + rows: term.rows + }; + const jsonStr = JSON.stringify(resizeData); + const encoder = new TextEncoder(); + const jsonBuffer = encoder.encode(jsonStr); + // Create a Uint8Array with one extra byte for the prefix. + const buffer = new Uint8Array(jsonBuffer.length + 1); + buffer[0] = 0xFF; // Control prefix. + buffer.set(jsonBuffer, 1); + interactiveWS.send(buffer.buffer); + console.log("Sent resize: cols =", term.cols, "rows =", term.rows); + } +} }); diff --git a/static/stylesheet.css b/static/stylesheet.css index 7908782..6cd6388 100644 --- a/static/stylesheet.css +++ b/static/stylesheet.css @@ -8,14 +8,15 @@ } /* Default is pwny theme */ -body { +html, body { background-color: var(--bg-color); color: var(--text-color); font-family: monospace; font-size: 14pt; margin: 0; padding: 0; - height: 100vh; + height: 100%; + width: 100%; display: flex; flex-direction: column; } @@ -80,6 +81,25 @@ span.ps1 { color: var(--ps1-color); } + +.info { + /* text-align: justify; */ + /* color: var(--text-color); */ + /* animation: 0.75s 2 changeColor; */ + animation: changeColor 1.75s forwards; + /* max-width: 80ch; */ +} +@keyframes changeColor { + from { color: var(--error-color) }; + to { color: var(--text-color) }; +} + +.error { + /* text-align: justify; */ + color: var(--error-color); + /* max-width: 80ch; */ +} + .light-theme { --bg-color: #fff; --text-color: #222; diff --git a/templates/index.html b/templates/index.html index 8cbe5ba..ed0f134 100644 --- a/templates/index.html +++ b/templates/index.html @@ -6,6 +6,7 @@ Go Web Shell +