diff --git a/main.go b/main.go
index 3d2b2f3..6d8b259 100644
--- a/main.go
+++ b/main.go
@@ -17,7 +17,9 @@ import (
"sync"
"syscall"
- "golang.org/x/net/html"
+ "net/http/httputil"
+ "net/url"
+ // "golang.org/x/net/html"
"gontrol/src/logger"
"gontrol/src/randomname"
api "gontrol/src/server/api"
@@ -217,75 +219,164 @@ func logsHandler(w http.ResponseWriter, r *http.Request) {
renderTemplate(w, "templates/partials/logs_partial.html", logs)
}
+// func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
+// agentIP := r.URL.Query().Get("ip") // e.g., 10.0.0.42
+// if agentIP == "" {
+// http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
+// return
+// }
+// agentPort := r.URL.Query().Get("port")
+// if agentIP == "" {
+// http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
+// return
+// }
+
+// // Construct the URL to proxy to
+// agentURL := "http://" + agentIP + ":" + agentPort
+
+// // Send request to agent server
+// resp, err := http.Get(agentURL)
+// if err != nil {
+// http.Error(w, "Failed to reach agent: "+err.Error(), http.StatusBadGateway)
+// return
+// }
+// defer resp.Body.Close()
+
+// // Parse the HTML from the agent's response
+// doc, err := html.Parse(resp.Body)
+// if err != nil {
+// http.Error(w, "Failed to parse agent response: "+err.Error(), http.StatusInternalServerError)
+// return
+// }
+
+// // Extract all elements for stylesheets
+// var stylesheets []string
+// var extractStylesheets func(*html.Node)
+// extractStylesheets = func(n *html.Node) {
+// if n.Type == html.ElementNode && n.Data == "link" {
+// for _, attr := range n.Attr {
+// if attr.Key == "rel" && attr.Val == "stylesheet" {
+// for _, attr := range n.Attr {
+// if attr.Key == "href" {
+// stylesheets = append(stylesheets, attr.Val)
+// }
+// }
+// }
+// }
+// }
+// for c := n.FirstChild; c != nil; c = c.NextSibling {
+// extractStylesheets(c)
+// }
+// }
+// extractStylesheets(doc)
+
+// // Return the HTML and inject the stylesheets in the
section
+// w.Header().Set("Content-Type", "text/html")
+// w.WriteHeader(http.StatusOK)
+
+// // Inject the stylesheets into the section of your page
+// fmt.Fprintf(w, "")
+// for _, stylesheet := range stylesheets {
+// // Make sure the stylesheet is loaded properly (absolute URLs or proxy it)
+// fmt.Fprintf(w, ``, stylesheet)
+// }
+// fmt.Fprintf(w, "")
+
+// // Now, serve the HTML content of the agent web app (or an iframe)
+// // Output the rest of the HTML (including the agent's content inside iframe)
+// fmt.Fprintf(w, `
+//
+// `, agentURL)
+
+// fmt.Fprintf(w, "")
+// }
+
+// proxyAgentHandler proxies requests to the agent server specified by IP and port query parameters.
+// It strips the "/proxyAgent" prefix from the request path before forwarding.
func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
- agentIP := r.URL.Query().Get("ip") // e.g., 10.0.0.42
+ agentIP := r.URL.Query().Get("ip")
if agentIP == "" {
http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
return
}
-
- // Construct the URL to proxy to
- agentURL := "http://" + agentIP + ":8080"
-
- // Send request to agent server
- resp, err := http.Get(agentURL)
- if err != nil {
- http.Error(w, "Failed to reach agent: "+err.Error(), http.StatusBadGateway)
- return
- }
- defer resp.Body.Close()
-
- // Parse the HTML from the agent's response
- doc, err := html.Parse(resp.Body)
- if err != nil {
- http.Error(w, "Failed to parse agent response: "+err.Error(), http.StatusInternalServerError)
+ agentPort := r.URL.Query().Get("port")
+ if agentPort == "" {
+ http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
return
}
- // Extract all elements for stylesheets
- var stylesheets []string
- var extractStylesheets func(*html.Node)
- extractStylesheets = func(n *html.Node) {
- if n.Type == html.ElementNode && n.Data == "link" {
- for _, attr := range n.Attr {
- if attr.Key == "rel" && attr.Val == "stylesheet" {
- for _, attr := range n.Attr {
- if attr.Key == "href" {
- stylesheets = append(stylesheets, attr.Val)
- }
- }
- }
+ agentBaseURL := "http://" + agentIP + ":" + agentPort
+
+ targetURL, err := url.Parse(agentBaseURL)
+ if err != nil {
+ http.Error(w, "Invalid agent URL: "+err.Error(), http.StatusBadRequest)
+ return
+ }
+
+ proxy := httputil.NewSingleHostReverseProxy(targetURL)
+
+ // Modify the Director function to rewrite the request URL before sending to agent
+ originalDirector := proxy.Director
+ proxy.Director = func(req *http.Request) {
+ originalDirector(req)
+
+ // Strip "/proxyAgent" prefix from the path
+ const prefix = "/proxyAgent"
+ if strings.HasPrefix(req.URL.Path, prefix) {
+ req.URL.Path = strings.TrimPrefix(req.URL.Path, prefix)
+ if req.URL.Path == "" {
+ req.URL.Path = "/"
}
}
- for c := n.FirstChild; c != nil; c = c.NextSibling {
- extractStylesheets(c)
- }
+
+ // Preserve original query parameters except ip and port (remove those for backend)
+ query := req.URL.Query()
+ query.Del("ip")
+ query.Del("port")
+ req.URL.RawQuery = query.Encode()
+
+ // Optional: set the Host header to the target host
+ req.Host = targetURL.Host
}
- extractStylesheets(doc)
- // Return the HTML and inject the stylesheets in the section
- w.Header().Set("Content-Type", "text/html")
- w.WriteHeader(http.StatusOK)
-
- // Inject the stylesheets into the section of your page
- fmt.Fprintf(w, "")
- for _, stylesheet := range stylesheets {
- // Make sure the stylesheet is loaded properly (absolute URLs or proxy it)
- fmt.Fprintf(w, ``, stylesheet)
- }
- fmt.Fprintf(w, "")
-
- // Now, serve the HTML content of the agent web app (or an iframe)
- // Output the rest of the HTML (including the agent's content inside iframe)
- fmt.Fprintf(w, `
-
- `, agentURL)
-
- fmt.Fprintf(w, "")
+ proxy.ServeHTTP(w, r)
}
+// func proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
+// agentIP := r.URL.Query().Get("ip")
+// if agentIP == "" {
+// http.Error(w, "Missing 'ip' parameter", http.StatusBadRequest)
+// return
+// }
+// agentPort := r.URL.Query().Get("port")
+// if agentPort == "" {
+// http.Error(w, "Missing 'port' parameter", http.StatusBadRequest)
+// return
+// }
+
+// agentURL := "http://" + agentIP + ":" + agentPort + "/"
+
+// resp, err := http.Get(agentURL)
+// if err != nil {
+// http.Error(w, "Failed to reach agent: "+err.Error(), http.StatusBadGateway)
+// return
+// }
+// defer resp.Body.Close()
+
+// // Copy headers from agent response (you might want to filter some)
+// for k, v := range resp.Header {
+// for _, vv := range v {
+// w.Header().Add(k, vv)
+// }
+// }
+// w.WriteHeader(resp.StatusCode)
+
+// // Stream the entire response body directly to the client
+// io.Copy(w, resp.Body)
+// }
+
func main() {
diff --git a/static/download-command.js b/static/download-command.js
new file mode 100644
index 0000000..25ad655
--- /dev/null
+++ b/static/download-command.js
@@ -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));
+}
diff --git a/static/gontrol-stylesheet.css b/static/gontrol-stylesheet.css
new file mode 100644
index 0000000..43d6f91
--- /dev/null
+++ b/static/gontrol-stylesheet.css
@@ -0,0 +1,48 @@
+/* :root{ */
+/* --grey-color: #1B2B34; */
+/* --error-color: #EC5f67; */
+/* --warning-color: #F99157; */
+/* --yellow-color: #FAC863; */
+/* --info-color: #99C794; */
+/* --teal-color: #5FB3B3; */
+/* --blue-color: #6699CC; */
+/* --debug-color: #C594C5; */
+/* --fatal-color: #AB7967; */
+/* } */
+
+#logs-container {
+ height: 300px; /* or any fixed height you prefer */
+ overflow-y: auto; /* enables vertical scroll when content overflows */
+ border: 1px solid #fff; /* optional: for visual clarity */
+ padding: 10px; /* optional: spacing inside the container */
+ /* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */
+}
+
+.log-info, .log-warning, .log-error, .log-fatal, .log-debug{
+ font-family: "Lucida Console", Monaco, monospace;
+ font-size: 12px;
+}
+
+.log-info {
+ color: var(--bs-success);
+}
+.log-warning {
+ color: var(--bs-warning);
+}
+.log-error {
+ color: var(--bs-danger);
+}
+.log-fatal {
+ color: var(--bs-purple);
+}
+.log-debug {
+ color: var(--bs-primary);
+}
+
+#graph-container {
+ margin: 0; padding: 0;
+}
+
+tr.selected-row {
+ background-color: var(--bs-table-hover-bg) !important;
+}
diff --git a/static/help-command.js b/static/help-command.js
new file mode 100644
index 0000000..4e36063
--- /dev/null
+++ b/static/help-command.js
@@ -0,0 +1,35 @@
+const helpMsg = `
+This is a non interactive Webshell with an interactive mode, 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 go back to non-interactive mode.
+`
+// 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/keyboard-shortcuts.js b/static/keyboard-shortcuts.js
new file mode 100644
index 0000000..d6e126c
--- /dev/null
+++ b/static/keyboard-shortcuts.js
@@ -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);
+});
diff --git a/static/start-interactive.js b/static/start-interactive.js
new file mode 100644
index 0000000..60d10e6
--- /dev/null
+++ b/static/start-interactive.js
@@ -0,0 +1,132 @@
+document.addEventListener("DOMContentLoaded", function () {
+ const input = document.getElementById("command-input");
+ // We assume your normal terminal UI is in the element with id "terminal".
+ const normalTerminal = document.getElementById("terminal");
+ let interactiveWS = null;
+ let interactiveMode = false;
+ const ansi_up = new AnsiUp;
+
+ input.addEventListener("keydown", function (event) {
+ if (!interactiveMode && event.key === "Enter") {
+ const command = input.value.trim();
+ if (command === "start-interactive") {
+ startInteractiveSession();
+ input.value = "";
+ event.preventDefault();
+ return;
+ }
+ // Otherwise, your normal HTTP submission…
+ }
+ });
+
+ function startInteractiveSession() {
+ 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.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);
+
+ 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);
+
+ interactiveWS = new WebSocket("ws://" + location.host + "/terminal");
+ interactiveWS.binaryType = "arraybuffer";
+
+ interactiveWS.onopen = function () {
+ sendResize();
+ };
+
+ interactiveWS.onmessage = function (event) {
+ const text = new TextDecoder("utf-8").decode(event.data);
+ term.write(text);
+ };
+
+ 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();
+ };
+
+ 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";
+ };
+
+ 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 43d6f91..cbf53ed 100644
--- a/static/stylesheet.css
+++ b/static/stylesheet.css
@@ -1,48 +1,155 @@
-/* :root{ */
-/* --grey-color: #1B2B34; */
-/* --error-color: #EC5f67; */
-/* --warning-color: #F99157; */
-/* --yellow-color: #FAC863; */
-/* --info-color: #99C794; */
-/* --teal-color: #5FB3B3; */
-/* --blue-color: #6699CC; */
-/* --debug-color: #C594C5; */
-/* --fatal-color: #AB7967; */
-/* } */
-
-#logs-container {
- height: 300px; /* or any fixed height you prefer */
- overflow-y: auto; /* enables vertical scroll when content overflows */
- border: 1px solid #fff; /* optional: for visual clarity */
- padding: 10px; /* optional: spacing inside the container */
- /* background-color: #f8f9fa; /1* optional: subtle background for log readability *1/ */
+:root {
+ --bg-color: #222;
+ --text-color: #eee;
+ --command-color: #75df0b;
+ --error-color: #ff5555;
+ --directory-color: #1bc9e7;
+ --ps1-color: #75df0b;
}
-.log-info, .log-warning, .log-error, .log-fatal, .log-debug{
- font-family: "Lucida Console", Monaco, monospace;
- font-size: 12px;
+/* Default is pwny theme */
+html, body {
+ background-color: var(--bg-color);
+ color: var(--text-color);
+ font-family: monospace;
+ font-size: 14pt;
+ margin: 0;
+ padding: 0;
+ height: 100%;
+ width: 100%;
+ display: flex;
+ flex-direction: column;
+}
+#terminal {
+ flex: 1;
+ padding: 12px;
+ overflow-y: auto;
+ white-space: pre-wrap;
+ border: none;
+ margin: 10px 10px 0 10px;
+ display: flex;
+ flex-direction: column;
+ padding-bottom: 3vh;
+}
+#terminal form {
+ display: flex;
+ /* flex-wrap: nowrap; */
+ align-items: center;
+ width: 100%;
+}
+#terminal .command {
+ white-space: nowrap;
+ /* overflow: hidden; */
+ /* text-overflow: ellipsis; */
+}
+/* current-line is for interactive mode */
+#current-line {
+ display: inline;
+}
+input {
+ flex-grow: 1;
+ font-size: 14pt;
+ background: var(--bg-color);
+ color: var(--text-color);
+ border: none;
+ min-width: 100vh;
+ font-family: monospace;
+ margin-left: 8px;
+}
+input:focus {
+ outline: none;
+}
+span.cursor {
+ display: inline-block;
+ width: 10px;
+ animation: blink 1s step-end infinite;
+ vertical-align: bottom;
+}
+@keyframes blink {
+ 50% { opacity: 0; }
+}
+span.command {
+ color: var(--command-color);
+}
+span.error {
+ color: var(--error-color);
+}
+span.directory {
+ color: var(--directory-color);
+}
+span.ps1 {
+ color: var(--ps1-color);
}
-.log-info {
- color: var(--bs-success);
+
+.info {
+ /* text-align: justify; */
+ /* color: var(--text-color); */
+ /* animation: 0.75s 2 changeColor; */
+ animation: changeColor 1.75s forwards;
+ /* max-width: 80ch; */
}
-.log-warning {
- color: var(--bs-warning);
-}
-.log-error {
- color: var(--bs-danger);
-}
-.log-fatal {
- color: var(--bs-purple);
-}
-.log-debug {
- color: var(--bs-primary);
+@keyframes changeColor {
+ from { color: var(--command-color) };
+ to { color: var(--text-color) };
}
-#graph-container {
- margin: 0; padding: 0;
+.error {
+ /* text-align: justify; */
+ color: var(--error-color);
+ /* max-width: 80ch; */
}
-tr.selected-row {
- background-color: var(--bs-table-hover-bg) !important;
+.light-theme {
+ --bg-color: #fff;
+ --text-color: #222;
+ --command-color: #007700;
+ --error-color: #cc0000;
+ --directory-color: #0044cc;
+ --ps1-color: #222;
+}
+
+.catppuccin-theme {
+ --bg-color: #363a4f;
+ --text-color: #cad3f5;
+ --command-color: #a6da95;
+ --error-color: #ed8796;
+ --directory-color: #7dc4e4;
+ --ps1-color: #c6a0f6;
+}
+
+.doom1-theme {
+ --bg-color: #21242b;
+ --text-color: #bbc2cf;
+ --command-color: #50fa7b;
+ --error-color: #da8548;
+ --directory-color: #51afef;
+ --ps1-color: #ffa8ff;
+}
+
+.gruvbox-theme {
+ --bg-color: #282828;
+ --text-color: #ebdbb2;
+ --command-color: #b8bb26;
+ --error-color: #fb4934;
+ --directory-color: #83a598;
+ --ps1-color: #d3869b;
+}
+
+.nord-theme {
+ --bg-color: #2e3440;
+ --text-color: #d8dee9;
+ --command-color: #8fbcbb;
+ --error-color: #bf616a;
+ --directory-color: #81a1c1;
+ --ps1-color: #a3be8c;
+}
+
+.dracula-theme {
+ --bg-color: #282a36;
+ --text-color: #f8f8f2;
+ --command-color: #6272a4;
+ --error-color: #ff5555;
+ --directory-color: #bd93f9;
+ --ps1-color: #ff79c6;
}
diff --git a/static/switch-themes.js b/static/switch-themes.js
new file mode 100644
index 0000000..2eb3a3e
--- /dev/null
+++ b/static/switch-themes.js
@@ -0,0 +1,46 @@
+document.addEventListener("DOMContentLoaded", function () {
+ const input = document.getElementById("command-input");
+ const terminal = document.getElementById("terminal");
+ const availableThemes = ["pwny", "light", "catppuccin", "doom1", "dracula", "gruvbox", "nord"];
+
+ // Load the saved theme from localStorage, defaulting to "pwny"
+ let currentTheme = localStorage.getItem("theme") || "pwny";
+ applyTheme(currentTheme);
+
+ input.addEventListener("keydown", function (event) {
+ if (event.key === "Enter") {
+ const command = input.value.trim();
+ if (command.startsWith("theme")) {
+ event.preventDefault();
+ const parts = command.split(" ");
+ if (parts.length === 1) {
+ addLogEntry(`Available themes: ${availableThemes.join(', ')}. Current theme: ${currentTheme}`, 'info');
+ } else {
+ const theme = parts[1];
+ applyTheme(theme);
+ addLogEntry(`Theme changed to: ${theme}`, 'info');
+ }
+ input.value = ''; // Clear input
+ }
+ }
+ });
+
+ function applyTheme(themeName) {
+ if (availableThemes.includes(themeName)) {
+ document.body.className = ""; // Clear all theme classes
+ document.body.classList.add(`${themeName}-theme`); // Add the new theme class
+ localStorage.setItem("theme", themeName);
+ currentTheme = themeName;
+ } else {
+ addLogEntry(`Error: Theme "${themeName}" not found. Available themes: ${availableThemes.join(', ')}`, 'error');
+ }
+ }
+
+ 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/templates/index.html b/templates/index.html
index b8a4b96..cdd015c 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -5,7 +5,7 @@
-
+
@@ -95,6 +95,7 @@
+
diff --git a/templates/partials/agent_detail.html b/templates/partials/agent_detail.html
index 5cd282a..3dd5b7e 100644
--- a/templates/partials/agent_detail.html
+++ b/templates/partials/agent_detail.html
@@ -8,6 +8,7 @@
Initial Contact: {{.InitialContact}}
Last Contact: {{.LastContact}}
Hostname: {{.HostName}}
-
+
+
Proxy
Open