separated templates and made the binary standalone, which means there are no external files needed for the template after compilation

This commit is contained in:
Stefan Friese 2025-02-05 11:59:26 +00:00
parent 9fe1f78c7f
commit 1144be1c1c
8 changed files with 301 additions and 263 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
build/
./gommand

30
Makefile Normal file
View File

@ -0,0 +1,30 @@
# Define variables
APP_NAME = gommand
GO_CMD = go
GO_BUILD = $(GO_CMD) build
GO_INSTALL = $(GO_CMD) install
GO_CLEAN = $(GO_CMD) clean
BUILD_DIR = build
BINARY = $(BUILD_DIR)/$(APP_NAME)
.PHONY: all build help clean install
all: build
build: ## Build the application
@echo "Building $(APP_NAME)..."
$(GO_BUILD) -o $(BINARY)
install: build ## Install the application
@echo "Installing $(APP_NAME)..."
$(GO_INSTALL)
clean: ## Remove build artifacts
@echo "Cleaning up build artifacts..."
$(GO_CLEAN)
rm -f $(BINARY)
help: ## Print the make targets
@echo "Makefile for $(APP_NAME)"
@echo "Targets:"
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module gommand
go 1.23.5

View File

@ -4,6 +4,7 @@ import (
// "bytes"
// "bufio"
"bytes"
"embed"
"fmt"
"html/template"
"io"
@ -36,9 +37,16 @@ type CommandNode struct {
Command string
}
//go:embed templates/*
var templateFiles embed.FS
//go:embed static/*
var staticFiles embed.FS
var commandLog []CommandOutput
var variables = make(map[string]string)
var tmpl = template.Must(template.ParseFS(templateFiles, "templates/index.html"))
func expandCommandSubstitution(command string, currentDir string) (string, error) {
// Do not expand single quoted strings
reSingleQuotes := regexp.MustCompile(`'([^']*)'`)
@ -458,269 +466,6 @@ func handler(w http.ResponseWriter, r *http.Request) {
}
}
tmpl := template.Must(template.New("index").Parse(`
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Go Web Shell</title>
<script>
document.addEventListener("DOMContentLoaded", function() {
const input = document.getElementById("command-input");
if (!input) return;
let commandHistory = JSON.parse(sessionStorage.getItem("commandHistory")) || [];
let historyIndex = commandHistory.length;
let tabIndex = -1;
let tabMatches = [];
document.querySelector("form").addEventListener("submit", function(event) {
const command = input.value.trim();
if (command) {
commandHistory.push(command);
sessionStorage.setItem("commandHistory", JSON.stringify(commandHistory));
historyIndex = commandHistory.length;
}
});
window.addEventListener("keydown", function(event) {
if (document.activeElement !== input) return;
const cursorPos = input.selectionStart;
const textLength = input.value.length;
// Prevent default behavior for specific Ctrl+Key shortcuts
if (event.ctrlKey && ["w", "n", "p", "h", "e", "a", "k", "u", "d", "r", "t"].includes(event.key.toLowerCase())) {
event.preventDefault();
event.stopPropagation();
}
// Ctrl+A: Move cursor to the beginning
if (event.ctrlKey && event.key === "a") {
input.setSelectionRange(0, 0);
}
// Ctrl+E: Move cursor to the end
else if (event.ctrlKey && event.key === "e") {
input.setSelectionRange(textLength, textLength);
}
// Ctrl+U: Clear the input field
else if (event.ctrlKey && event.key === "u") {
input.value = "";
}
// Ctrl+K: Delete everything after the cursor
else if (event.ctrlKey && event.key === "k") {
input.value = input.value.substring(0, cursorPos);
}
// Ctrl+W: Delete the previous word
else if (event.ctrlKey && event.key === "w") {
const beforeCursor = input.value.substring(0, cursorPos);
const afterCursor = input.value.substring(cursorPos);
const newBeforeCursor = beforeCursor.replace(/\S+\s*$/, ""); // Delete last word
input.value = newBeforeCursor + afterCursor;
input.setSelectionRange(newBeforeCursor.length, newBeforeCursor.length);
}
// Ctrl+H: Delete the previous character (Backspace)
else if (event.ctrlKey && event.key === "h") {
if (cursorPos > 0) {
input.value = input.value.substring(0, cursorPos - 1) + input.value.substring(cursorPos);
input.setSelectionRange(cursorPos - 1, cursorPos - 1);
}
}
// Ctrl+D: Delete character under cursor (or clear input if empty)
else if (event.ctrlKey && event.key === "d") {
if (textLength === 0) {
console.log("Ctrl+D: No input, simulating EOF");
} else if (cursorPos < textLength) {
input.value = input.value.substring(0, cursorPos) + input.value.substring(cursorPos + 1);
input.setSelectionRange(cursorPos, cursorPos);
}
}
// Ctrl+P: Previous command (up)
else if (event.ctrlKey && event.key === "p") {
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
} else if (historyIndex === 0) {
input.value = commandHistory[0];
}
}
// Ctrl+N: Next command (down)
else if (event.ctrlKey && event.key === "n") {
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = "";
}
}
// Ctrl+R: Prevent page reload (for future reverse search)
else if (event.ctrlKey && event.key === "r") {
console.log("Reverse search triggered (not yet implemented)");
}
// Tab Completion
else if (event.key === "Tab") {
event.preventDefault();
const currentText = input.value.trim();
if (currentText === "") return;
// Find all matching commands from history
if (tabIndex === -1) {
tabMatches = commandHistory.filter(cmd => cmd.startsWith(currentText));
if (tabMatches.length === 0) return;
}
// Cycle through matches
if (event.shiftKey) {
tabIndex = tabIndex > 0 ? tabIndex - 1 : tabMatches.length - 1; // Shift+Tab goes backward
} else {
tabIndex = (tabIndex + 1) % tabMatches.length;
}
input.value = tabMatches[tabIndex];
}
// Allow Enter key to submit form
if (event.key === "Enter") {
tabIndex = -1; // Reset tab completion cycle
return;
}
}, true);
});
</script>
<script>
document.addEventListener("DOMContentLoaded", function() {
const input = document.getElementById("command-input");
const fileInput = document.getElementById("fileInput");
let isUploadTriggered = false;
document.querySelector("form").addEventListener("submit", function(event) {
const command = input.value.trim();
const parts = command.split(" ");
if (parts[0] === "upload") {
event.preventDefault();
if (parts.length === 1) {
fileInput.click();
isUploadTriggered = true;
} else {
const filePath = parts[1];
const targetPath = parts.length > 2 ? parts[2] : ".";
uploadFileFromBrowser(filePath, targetPath);
}
}
});
fileInput.addEventListener("change", function () {
if (fileInput.files.length > 0 && isUploadTriggered) {
const file = fileInput.files[0];
input.value = 'upload "' + file.name + '"';
isUploadTriggered = false;
}
});
});
function uploadFileFromBrowser(filePath, targetPath) {
const fileInput = document.getElementById("fileInput");
if (fileInput.files.length === 0) {
console.error("No file selected");
alert("No file selected");
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append("file", file);
fetch("/upload", {
method: "POST",
body: formData,
})
.then((response) => response.text())
.then((data) => {
console.log("Upload successful:", data);
document.getElementById("command-input").value = "";
})
.catch((error) => console.error("Upload failed:", error));
}
</script>
<style>
body {
background-color: #222;
color: #eee;
font-family: monospace;
font-size: 14pt;
margin: 0;
padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
}
#terminal {
flex: 1;
padding: 12px;
overflow-y: auto;
white-space: pre-wrap;
border: 1px solid white;
margin: 10px;
display: flex;
flex-direction: column;
}
input {
font-size: 14pt;
background: #222;
color: #eee;
border: none;
width: 80%;
font-family: monospace;
}
input:focus {
outline: none;
}
span.command {
color: #75df0b;
}
span.error {
color: #ff5555;
}
span.directory {
color: #1bc9e7;
}
</style>
</head>
<body>
<div id="terminal">
<span>Current Directory: {{.CurrentDir}}</span>
{{range .CommandLog}}
<div><span class="command">gommand:$ {{.Command}}</span></div>
{{if .Output}}
<div>{{.Output}}</div>
{{end}}
{{if .Error}}
<div class="error">{{.Error}}</div>
{{end}}
{{end}}
<form method="POST" autocomplete="off">
<div style="display: flex; align-items: center;">
<span class="command">{{.CurrentUsername}}:<span class="directory">{{.CurrentDir}}</span>$ </span>
<input id="command-input" type="text" name="command" placeholder="Type a command here..." autofocus required>
</div>
</form>
</div>
<input type="file" id="fileInput" style="display: none;" onchange="setFilePath()">
</body>
</html>
`))
data := PageData{
CurrentDir: currentDir,
CurrentUsername: currentUsername,
@ -733,6 +478,7 @@ func main() {
http.HandleFunc("/", handler)
http.HandleFunc("/upload", fileUploadHandler)
http.HandleFunc("/download", fileDownloadHandler)
http.Handle("/static/", http.FileServer(http.FS(staticFiles)))
fmt.Println("Starting server on :8080")
http.ListenAndServe(":8080", nil)
}

View File

@ -0,0 +1,52 @@
document.addEventListener("DOMContentLoaded", function() {
const input = document.getElementById("command-input");
const fileInput = document.getElementById("fileInput");
let isUploadTriggered = false;
document.querySelector("form").addEventListener("submit", function(event) {
const command = input.value.trim();
const parts = command.split(" ");
if (parts[0] === "upload") {
event.preventDefault();
if (parts.length === 1) {
fileInput.click();
isUploadTriggered = true;
} else {
const filePath = parts[1];
const targetPath = parts.length > 2 ? parts[2] : ".";
uploadFileFromBrowser(filePath, targetPath);
}
}
});
fileInput.addEventListener("change", function () {
if (fileInput.files.length > 0 && isUploadTriggered) {
const file = fileInput.files[0];
input.value = 'upload "' + file.name + '"';
isUploadTriggered = false;
}
});
});
function uploadFileFromBrowser(filePath, targetPath) {
const fileInput = document.getElementById("fileInput");
if (fileInput.files.length === 0) {
console.error("No file selected");
alert("No file selected");
return;
}
const file = fileInput.files[0];
const formData = new FormData();
formData.append("file", file);
fetch("/upload", {
method: "POST",
body: formData,
})
.then((response) => response.text())
.then((data) => {
console.log("Upload successful:", data);
document.getElementById("command-input").value = "";
})
.catch((error) => console.error("Upload failed:", error));
}

View File

@ -0,0 +1,132 @@
document.addEventListener("DOMContentLoaded", function() {
const input = document.getElementById("command-input");
if (!input) return;
let commandHistory = JSON.parse(sessionStorage.getItem("commandHistory")) || [];
let historyIndex = commandHistory.length;
let tabIndex = -1;
let tabMatches = [];
document.querySelector("form").addEventListener("submit", function(event) {
const command = input.value.trim();
if (command) {
commandHistory.push(command);
sessionStorage.setItem("commandHistory", JSON.stringify(commandHistory));
historyIndex = commandHistory.length;
}
});
window.addEventListener("keydown", function(event) {
if (document.activeElement !== input) return;
const cursorPos = input.selectionStart;
const textLength = input.value.length;
// Prevent default behavior for specific Ctrl+Key shortcuts
if (event.ctrlKey && ["w", "n", "p", "h", "e", "a", "k", "u", "d", "r", "t"].includes(event.key.toLowerCase())) {
event.preventDefault();
event.stopPropagation();
}
// Ctrl+A: Move cursor to the beginning
if (event.ctrlKey && event.key === "a") {
input.setSelectionRange(0, 0);
}
// Ctrl+E: Move cursor to the end
else if (event.ctrlKey && event.key === "e") {
input.setSelectionRange(textLength, textLength);
}
// Ctrl+U: Clear the input field
else if (event.ctrlKey && event.key === "u") {
input.value = "";
}
// Ctrl+K: Delete everything after the cursor
else if (event.ctrlKey && event.key === "k") {
input.value = input.value.substring(0, cursorPos);
}
// Ctrl+W: Delete the previous word
else if (event.ctrlKey && event.key === "w") {
const beforeCursor = input.value.substring(0, cursorPos);
const afterCursor = input.value.substring(cursorPos);
const newBeforeCursor = beforeCursor.replace(/\S+\s*$/, ""); // Delete last word
input.value = newBeforeCursor + afterCursor;
input.setSelectionRange(newBeforeCursor.length, newBeforeCursor.length);
}
// Ctrl+H: Delete the previous character (Backspace)
else if (event.ctrlKey && event.key === "h") {
if (cursorPos > 0) {
input.value = input.value.substring(0, cursorPos - 1) + input.value.substring(cursorPos);
input.setSelectionRange(cursorPos - 1, cursorPos - 1);
}
}
// Ctrl+D: Delete character under cursor (or clear input if empty)
else if (event.ctrlKey && event.key === "d") {
if (textLength === 0) {
console.log("Ctrl+D: No input, simulating EOF");
} else if (cursorPos < textLength) {
input.value = input.value.substring(0, cursorPos) + input.value.substring(cursorPos + 1);
input.setSelectionRange(cursorPos, cursorPos);
}
}
// Ctrl+P: Previous command (up)
else if (event.ctrlKey && event.key === "p") {
if (historyIndex > 0) {
historyIndex--;
input.value = commandHistory[historyIndex];
} else if (historyIndex === 0) {
input.value = commandHistory[0];
}
}
// Ctrl+N: Next command (down)
else if (event.ctrlKey && event.key === "n") {
if (historyIndex < commandHistory.length - 1) {
historyIndex++;
input.value = commandHistory[historyIndex];
} else {
historyIndex = commandHistory.length;
input.value = "";
}
}
// Ctrl+R: Prevent page reload (for future reverse search)
else if (event.ctrlKey && event.key === "r") {
console.log("Reverse search triggered (not yet implemented)");
}
// Tab Completion
else if (event.key === "Tab") {
event.preventDefault();
const currentText = input.value.trim();
if (currentText === "") return;
// Find all matching commands from history
if (tabIndex === -1) {
tabMatches = commandHistory.filter(cmd => cmd.startsWith(currentText));
if (tabMatches.length === 0) return;
}
// Cycle through matches
if (event.shiftKey) {
tabIndex = tabIndex > 0 ? tabIndex - 1 : tabMatches.length - 1; // Shift+Tab goes backward
} else {
tabIndex = (tabIndex + 1) % tabMatches.length;
}
input.value = tabMatches[tabIndex];
}
// Allow Enter key to submit form
if (event.key === "Enter") {
tabIndex = -1; // Reset tab completion cycle
return;
}
}, true);
});

41
static/stylesheet.css Normal file
View File

@ -0,0 +1,41 @@
body {
background-color: #222;
color: #eee;
font-family: monospace;
font-size: 14pt;
margin: 0;
padding: 0;
height: 100vh;
display: flex;
flex-direction: column;
}
#terminal {
flex: 1;
padding: 12px;
overflow-y: auto;
white-space: pre-wrap;
border: none;
margin: 10px;
display: flex;
flex-direction: column;
}
input {
font-size: 14pt;
background: #222;
color: #eee;
border: none;
width: 80%;
font-family: monospace;
}
input:focus {
outline: none;
}
span.command {
color: #75df0b;
}
span.error {
color: #ff5555;
}
span.directory {
color: #1bc9e7;
}

32
templates/index.html Normal file
View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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>
<link rel="stylesheet" type="text/css" href="static/stylesheet.css">
</head>
<body>
<div id="terminal">
<span>Current Directory: {{.CurrentDir}}</span>
{{range .CommandLog}}
<div><span class="command">gommand:$ {{.Command}}</span></div>
{{if .Output}}
<div>{{.Output}}</div>
{{end}}
{{if .Error}}
<div class="error">{{.Error}}</div>
{{end}}
{{end}}
<form method="POST" autocomplete="off">
<div style="display: flex; align-items: center;">
<span class="command">{{.CurrentUsername}}:<span class="directory">{{.CurrentDir}}</span>$ </span>
<input id="command-input" type="text" name="command" placeholder="Type a command here..." autofocus required>
</div>
</form>
</div>
<input type="file" id="fileInput" style="display: none;" onchange="setFilePath()">
</body>
</html>