added help menu.

This commit is contained in:
Stefan Friese 2025-02-13 15:45:00 +00:00
parent 82b6670838
commit 9e33737e67
5 changed files with 184 additions and 66 deletions

29
main.go
View File

@ -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
}

35
static/help-command.js Normal file
View File

@ -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 <file> Download files from the server to your local download directory.
theme <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 <file>\t\t\tDownload files from the server to your local download directory.\n theme <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;
}
});

View File

@ -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);
}
}
});

View File

@ -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;

View File

@ -6,6 +6,7 @@
<title>Go Web Shell</title>
<script type="text/javascript" src="static/keyboard-shortcuts.js"></script>
<script type="text/javascript" src="static/download-command.js"></script>
<script type="text/javascript" src="static/help-command.js"></script>
<script type="text/javascript" src="static/switch-themes.js"></script>
<script type="text/javascript" src="static/start-interactive.js"></script>
<script src="https://cdn.jsdelivr.net/npm/ansi_up@5.0.0/ansi_up.min.js"></script>