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:
parent
9fe1f78c7f
commit
1144be1c1c
|
@ -0,0 +1,2 @@
|
|||
build/
|
||||
./gommand
|
|
@ -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}'
|
|
@ -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)
|
||||
}
|
|
@ -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));
|
||||
}
|
|
@ -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);
|
||||
});
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
Loading…
Reference in New Issue