cleanup
This commit is contained in:
parent
45568013ca
commit
2b0f897f18
|
@ -14,6 +14,10 @@ import (
|
|||
"slices"
|
||||
"time"
|
||||
|
||||
"io"
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"gontrol/src/logger"
|
||||
"gontrol/src/server/api"
|
||||
)
|
||||
|
@ -173,59 +177,239 @@ func (a *App) getAgentIds(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
// proxyAgentHandler tunnels HTTP and WebSocket traffic to an agent
|
||||
// selected by ?ip=…&port=… . It keeps the path *after* /proxyAgent,
|
||||
// removes the two query parameters, disables HTTP/2 (mandatory for WS),
|
||||
// and streams with no buffering.
|
||||
func (a *App) proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
ip := r.URL.Query().Get("ip")
|
||||
port := r.URL.Query().Get("port")
|
||||
if ip == "" || port == "" {
|
||||
http.Error(w, "ip and port query parameters are required", http.StatusBadRequest)
|
||||
// func readCookie(r *http.Request, name string) (string, bool){
|
||||
// c, err := r.Cookie(name)
|
||||
// if err != nil { return "", false }
|
||||
// v, _ := url.QueryUnescape(c.Value)
|
||||
// return v, true
|
||||
// }
|
||||
|
||||
// func setCookie(h http.Header, name, val string) {
|
||||
// h.Add("Set-Cookie", (&http.Cookie{
|
||||
// Name: name,
|
||||
// Value: url.QueryEscape(val),
|
||||
// Path: "/proxyAgent",
|
||||
// SameSite: http.SameSiteLaxMode,
|
||||
// }).String())
|
||||
// }
|
||||
|
||||
// // proxyAgentHandler tunnels HTTP and WebSocket traffic to an agent
|
||||
// // selected by ?ip=…&port=… . It keeps the path *after* /proxyAgent,
|
||||
// // removes the two query parameters, disables HTTP/2 (mandatory for WS),
|
||||
// // and streams with no buffering.
|
||||
// func (a *App) proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// ip := r.URL.Query().Get("ip")
|
||||
// port := r.URL.Query().Get("port")
|
||||
// if ip == "" || port == "" {
|
||||
// http.Error(w, "ip and port query parameters are required", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // We leave the scheme "http" even for WebSockets – the Upgrade
|
||||
// // header does the rest. (Only cosmetic to change it to "ws”.)
|
||||
// target, err := url.Parse("http://" + ip + ":" + port)
|
||||
// if err != nil {
|
||||
// http.Error(w, "invalid ip/port: "+err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// proxy := &httputil.ReverseProxy{
|
||||
// Director: func(req *http.Request) {
|
||||
// // Point to the agent
|
||||
// req.URL.Scheme = target.Scheme
|
||||
// req.URL.Host = target.Host
|
||||
|
||||
// // Trim the first "/proxyAgent" prefix
|
||||
// if strings.HasPrefix(req.URL.Path, "/proxyAgent") {
|
||||
// req.URL.Path = strings.TrimPrefix(req.URL.Path, "/proxyAgent")
|
||||
// if req.URL.Path == "" {
|
||||
// req.URL.Path = "/"
|
||||
// }
|
||||
// }
|
||||
|
||||
// // Scrub ip/port from downstream query
|
||||
// q := req.URL.Query()
|
||||
// q.Del("ip")
|
||||
// q.Del("port")
|
||||
// req.URL.RawQuery = q.Encode()
|
||||
|
||||
// // Preserve Host (many CLI-style servers care)
|
||||
// req.Host = target.Host
|
||||
// },
|
||||
|
||||
// // Critical tweaks for WebSockets
|
||||
// Transport: &http.Transport{
|
||||
// Proxy: http.ProxyFromEnvironment,
|
||||
// DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
// ForceAttemptHTTP2: false, // MUST be HTTP/1.1 for WS
|
||||
// ResponseHeaderTimeout: 0,
|
||||
// },
|
||||
|
||||
// FlushInterval: -1, // stream bytes immediately
|
||||
// }
|
||||
|
||||
// proxy.ServeHTTP(w, r)
|
||||
// }
|
||||
|
||||
// Registration (keep the slash so sub‑paths are included):
|
||||
// webMux.HandleFunc("/proxyAgent/", app.proxyAgentHandler)
|
||||
|
||||
//func (app *App) proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
// //------------------------------------------------------------------
|
||||
// // 0. Helpers for cookie handling
|
||||
// //------------------------------------------------------------------
|
||||
// readCookie := func(name string) (string, bool) {
|
||||
// c, err := r.Cookie(name)
|
||||
// if err != nil { return "", false }
|
||||
// v, _ := url.QueryUnescape(c.Value)
|
||||
// return v, true
|
||||
// }
|
||||
// writeCookie := func(h http.Header, name, val string) {
|
||||
// h.Add("Set-Cookie", (&http.Cookie{
|
||||
// Name: name,
|
||||
// Value: url.QueryEscape(val),
|
||||
// Path: "/proxyAgent", // cookie visible only here
|
||||
// SameSite: http.SameSiteLaxMode,
|
||||
// }).String())
|
||||
// }
|
||||
|
||||
// //------------------------------------------------------------------
|
||||
// // 1. Decide which agent to talk to
|
||||
// //------------------------------------------------------------------
|
||||
// ip, port := r.URL.Query().Get("ip"), r.URL.Query().Get("port")
|
||||
// // fallback to cookies if query params are absent
|
||||
// if ip == "" || port == "" {
|
||||
// if v, ok := readCookie("proxyagent_ip"); ok { ip = v }
|
||||
// if v, ok := readCookie("proxyagent_port"); ok { port = v }
|
||||
// }
|
||||
// if ip == "" || port == "" {
|
||||
// http.Error(w, "ip and port query parameters are required", http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// target, err := url.Parse("http://" + ip + ":" + port)
|
||||
// if err != nil {
|
||||
// http.Error(w, "invalid ip/port: "+err.Error(), http.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // keep copies for ModifyResponse (Director will delete them)
|
||||
// ipForCookie, portForCookie := ip, port
|
||||
|
||||
// //------------------------------------------------------------------
|
||||
// // 2. Reverse proxy
|
||||
// //------------------------------------------------------------------
|
||||
// proxy := &httputil.ReverseProxy{
|
||||
|
||||
// Director: func(req *http.Request) {
|
||||
// // 2.1 upstream address
|
||||
// req.URL.Scheme = target.Scheme
|
||||
// req.URL.Host = target.Host
|
||||
|
||||
// // 2.2 strip ONE leading /proxyAgent
|
||||
// if strings.HasPrefix(req.URL.Path, "/proxyAgent") {
|
||||
// req.URL.Path = strings.TrimPrefix(req.URL.Path, "/proxyAgent")
|
||||
// if req.URL.Path == "" { req.URL.Path = "/" }
|
||||
// }
|
||||
|
||||
// // 2.3 remove ip/port from outgoing query
|
||||
// q := req.URL.Query()
|
||||
// q.Del("ip"); q.Del("port")
|
||||
// req.URL.RawQuery = q.Encode()
|
||||
|
||||
// // 2.4 keep original Host header
|
||||
// req.Host = target.Host
|
||||
|
||||
// // 2.5 force uncompressed HTML so we can patch it
|
||||
// req.Header.Del("Accept-Encoding")
|
||||
// },
|
||||
|
||||
// Transport: &http.Transport{
|
||||
// Proxy: http.ProxyFromEnvironment,
|
||||
// DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
// ForceAttemptHTTP2: false, // must stay on h1 for WebSocket upgrade
|
||||
// ResponseHeaderTimeout: 0,
|
||||
// },
|
||||
|
||||
// FlushInterval: -1, // instant streaming for xterm.js
|
||||
|
||||
// //------------------------------------------------------------------
|
||||
// // 3. Response rewrite & cookie injection
|
||||
// //------------------------------------------------------------------
|
||||
// ModifyResponse: func(resp *http.Response) error {
|
||||
|
||||
// // 3.1 always (once per agent) set the cookies
|
||||
// writeCookie(resp.Header, "proxyagent_ip", ipForCookie)
|
||||
// writeCookie(resp.Header, "proxyagent_port", portForCookie)
|
||||
|
||||
// // 3.2 if the payload is HTML, inject <base href="/proxyAgent/">
|
||||
// if ct := resp.Header.Get("Content-Type"); strings.HasPrefix(ct, "text/html") {
|
||||
// body, _ := io.ReadAll(resp.Body)
|
||||
// patched := bytes.Replace(
|
||||
// body,
|
||||
// []byte("<head>"),
|
||||
// []byte(`<head><base href="/proxyAgent/">`),
|
||||
// 1)
|
||||
|
||||
// resp.Body = io.NopCloser(bytes.NewReader(patched))
|
||||
// resp.ContentLength = int64(len(patched))
|
||||
// resp.Header.Set("Content-Length", strconv.Itoa(len(patched)))
|
||||
// }
|
||||
|
||||
// return nil
|
||||
// },
|
||||
// }
|
||||
|
||||
// proxy.ServeHTTP(w, r)
|
||||
//}
|
||||
|
||||
func (app *App) proxyAgentHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// path is /proxyAgent/<agentKey>/whatever…
|
||||
tail := strings.TrimPrefix(r.URL.Path, "/proxyAgent/")
|
||||
parts := strings.SplitN(tail, "/", 2)
|
||||
if len(parts) < 1 || parts[0] == "" {
|
||||
http.Error(w, "missing agent key in path", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
agentKey := parts[0] // "192.168.0.12:46059"
|
||||
upstreamPath := "/"
|
||||
if len(parts) == 2 { upstreamPath += parts[1] } // "whatever…"
|
||||
|
||||
// We leave the scheme "http" even for WebSockets – the Upgrade
|
||||
// header does the rest. (Only cosmetic to change it to "ws”.)
|
||||
target, err := url.Parse("http://" + ip + ":" + port)
|
||||
target, err := url.Parse("http://" + agentKey)
|
||||
if err != nil {
|
||||
http.Error(w, "invalid ip/port: "+err.Error(), http.StatusBadRequest)
|
||||
http.Error(w, "invalid agent key", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := &httputil.ReverseProxy{
|
||||
Director: func(req *http.Request) {
|
||||
// Point to the agent
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
|
||||
// Trim the first "/proxyAgent" prefix
|
||||
if strings.HasPrefix(req.URL.Path, "/proxyAgent") {
|
||||
req.URL.Path = strings.TrimPrefix(req.URL.Path, "/proxyAgent")
|
||||
if req.URL.Path == "" {
|
||||
req.URL.Path = "/"
|
||||
}
|
||||
}
|
||||
|
||||
// Scrub ip/port from downstream query
|
||||
q := req.URL.Query()
|
||||
q.Del("ip")
|
||||
q.Del("port")
|
||||
req.URL.RawQuery = q.Encode()
|
||||
|
||||
// Preserve Host (many CLI-style servers care)
|
||||
req.URL.Path = upstreamPath // strip key
|
||||
req.Host = target.Host
|
||||
req.Header.Del("Accept-Encoding") // for HTML rewrite
|
||||
},
|
||||
|
||||
// Critical tweaks for WebSockets
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{Timeout: 30 * time.Second, KeepAlive: 30 * time.Second}).DialContext,
|
||||
ForceAttemptHTTP2: false, // MUST be HTTP/1.1 for WS
|
||||
ResponseHeaderTimeout: 0,
|
||||
ForceAttemptHTTP2: false,
|
||||
},
|
||||
FlushInterval: -1,
|
||||
ModifyResponse: func(resp *http.Response) error {
|
||||
if strings.HasPrefix(resp.Header.Get("Content-Type"), "text/html") {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
patched := bytes.Replace(
|
||||
body,
|
||||
[]byte("<head>"),
|
||||
[]byte(fmt.Sprintf(`<head><base href="/proxyAgent/%s/">`, agentKey)),
|
||||
1)
|
||||
resp.Body = io.NopCloser(bytes.NewReader(patched))
|
||||
resp.ContentLength = int64(len(patched))
|
||||
resp.Header.Set("Content-Length", strconv.Itoa(len(patched)))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
|
||||
FlushInterval: -1, // stream bytes immediately
|
||||
}
|
||||
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
|
|
@ -1,52 +0,0 @@
|
|||
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));
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
const helpMsg = `
|
||||
This is a shell in the browser, a web shell.
|
||||
|
||||
It includes a non-interactive shell with an optional interactive mode.
|
||||
Furthermore, additional commands 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.
|
||||
`
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
});
|
|
@ -1,132 +0,0 @@
|
|||
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);
|
||||
});
|
|
@ -1,158 +0,0 @@
|
|||
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…
|
||||
}
|
||||
});
|
||||
|
||||
// Helper: get ?ip=…&port=… from the current location
|
||||
function getQueryParam(name) {
|
||||
return new URLSearchParams(window.location.search).get(name);
|
||||
}
|
||||
|
||||
function makeWsUrl() {
|
||||
const proxyIp = getQueryParam("ip"); // "10.0.0.42" if you came via /proxyAgent
|
||||
const proxyPort = getQueryParam("port");
|
||||
const usingProxy = proxyIp && proxyPort; // true only in that case
|
||||
|
||||
if (usingProxy) {
|
||||
// Build ws(s)://<main-server>/proxyAgent/terminal?ip=…&port=…
|
||||
const u = new URL("/proxyAgent/terminal", window.location);
|
||||
u.searchParams.set("ip", proxyIp);
|
||||
u.searchParams.set("port", proxyPort);
|
||||
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
|
||||
return u.toString();
|
||||
}
|
||||
// Fallback: open directly on the agent we’re already on
|
||||
const u = new URL("/terminal", window.location);
|
||||
u.protocol = u.protocol === "https:" ? "wss:" : "ws:";
|
||||
return u.toString();
|
||||
}
|
||||
|
||||
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(makeWsUrl());
|
||||
// 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,155 +0,0 @@
|
|||
:root {
|
||||
--bg-color: #222;
|
||||
--text-color: #eee;
|
||||
--command-color: #75df0b;
|
||||
--error-color: #ff5555;
|
||||
--directory-color: #1bc9e7;
|
||||
--ps1-color: #75df0b;
|
||||
}
|
||||
|
||||
/* 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);
|
||||
}
|
||||
|
||||
|
||||
.info {
|
||||
/* text-align: justify; */
|
||||
/* color: var(--text-color); */
|
||||
/* animation: 0.75s 2 changeColor; */
|
||||
animation: changeColor 1.75s forwards;
|
||||
/* max-width: 80ch; */
|
||||
}
|
||||
@keyframes changeColor {
|
||||
from { color: var(--command-color) };
|
||||
to { color: var(--text-color) };
|
||||
}
|
||||
|
||||
.error {
|
||||
/* text-align: justify; */
|
||||
color: var(--error-color);
|
||||
/* max-width: 80ch; */
|
||||
}
|
||||
|
||||
.light-theme {
|
||||
--bg-color: #fff;
|
||||
--text-color: #222;
|
||||
--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;
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
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;
|
||||
}
|
||||
});
|
|
@ -1,2 +0,0 @@
|
|||
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?exports.FitAddon=t():e.FitAddon=t()}(self,(()=>(()=>{"use strict";var e={};return(()=>{var t=e;Object.defineProperty(t,"__esModule",{value:!0}),t.FitAddon=void 0,t.FitAddon=class{activate(e){this._terminal=e}dispose(){}fit(){const e=this.proposeDimensions();if(!e||!this._terminal||isNaN(e.cols)||isNaN(e.rows))return;const t=this._terminal._core;this._terminal.rows===e.rows&&this._terminal.cols===e.cols||(t._renderService.clear(),this._terminal.resize(e.cols,e.rows))}proposeDimensions(){if(!this._terminal)return;if(!this._terminal.element||!this._terminal.element.parentElement)return;const e=this._terminal._core,t=e._renderService.dimensions;if(0===t.css.cell.width||0===t.css.cell.height)return;const r=0===this._terminal.options.scrollback?0:e.viewport.scrollBarWidth,i=window.getComputedStyle(this._terminal.element.parentElement),o=parseInt(i.getPropertyValue("height")),s=Math.max(0,parseInt(i.getPropertyValue("width"))),n=window.getComputedStyle(this._terminal.element),l=o-(parseInt(n.getPropertyValue("padding-top"))+parseInt(n.getPropertyValue("padding-bottom"))),a=s-(parseInt(n.getPropertyValue("padding-right"))+parseInt(n.getPropertyValue("padding-left")))-r;return{cols:Math.max(2,Math.floor(a/t.css.cell.width)),rows:Math.max(1,Math.floor(l/t.css.cell.height))}}}})(),e})()));
|
||||
//# sourceMappingURL=xterm-addon-fit.js.map
|
|
@ -1,209 +0,0 @@
|
|||
/**
|
||||
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
|
||||
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
* https://github.com/chjj/term.js
|
||||
* @license MIT
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
* of this software and associated documentation files (the "Software"), to deal
|
||||
* in the Software without restriction, including without limitation the rights
|
||||
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
* copies of the Software, and to permit persons to whom the Software is
|
||||
* furnished to do so, subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in
|
||||
* all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
* THE SOFTWARE.
|
||||
*
|
||||
* Originally forked from (with the author's permission):
|
||||
* Fabrice Bellard's javascript vt100 for jslinux:
|
||||
* http://bellard.org/jslinux/
|
||||
* Copyright (c) 2011 Fabrice Bellard
|
||||
* The original design remains. The terminal itself
|
||||
* has been extended to include xterm CSI codes, among
|
||||
* other features.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Default styles for xterm.js
|
||||
*/
|
||||
|
||||
.xterm {
|
||||
cursor: text;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
-ms-user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.xterm.focus,
|
||||
.xterm:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.xterm .xterm-helpers {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
/**
|
||||
* The z-index of the helpers must be higher than the canvases in order for
|
||||
* IMEs to appear on top.
|
||||
*/
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.xterm .xterm-helper-textarea {
|
||||
padding: 0;
|
||||
border: 0;
|
||||
margin: 0;
|
||||
/* Move textarea out of the screen to the far left, so that the cursor is not visible */
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: -9999em;
|
||||
top: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
z-index: -5;
|
||||
/** Prevent wrapping so the IME appears against the textarea at the correct position */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.xterm .composition-view {
|
||||
/* TODO: Composition position got messed up somewhere */
|
||||
background: #000;
|
||||
color: #FFF;
|
||||
display: none;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.xterm .composition-view.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.xterm .xterm-viewport {
|
||||
/* On OS X this is required in order for the scroll bar to appear fully opaque */
|
||||
background-color: #000;
|
||||
overflow-y: scroll;
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.xterm .xterm-screen canvas {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.xterm .xterm-scroll-area {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.xterm-char-measure-element {
|
||||
display: inline-block;
|
||||
visibility: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -9999em;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.xterm.enable-mouse-events {
|
||||
/* When mouse events are enabled (eg. tmux), revert to the standard pointer cursor */
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.xterm.xterm-cursor-pointer,
|
||||
.xterm .xterm-cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.xterm.column-select.focus {
|
||||
/* Column selection mode */
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
.xterm .xterm-accessibility,
|
||||
.xterm .xterm-message {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm .live-region {
|
||||
position: absolute;
|
||||
left: -9999px;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.xterm-dim {
|
||||
/* Dim should not apply to background, so the opacity of the foreground color is applied
|
||||
* explicitly in the generated class and reset to 1 here */
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.xterm-underline-1 { text-decoration: underline; }
|
||||
.xterm-underline-2 { text-decoration: double underline; }
|
||||
.xterm-underline-3 { text-decoration: wavy underline; }
|
||||
.xterm-underline-4 { text-decoration: dotted underline; }
|
||||
.xterm-underline-5 { text-decoration: dashed underline; }
|
||||
|
||||
.xterm-overline {
|
||||
text-decoration: overline;
|
||||
}
|
||||
|
||||
.xterm-overline.xterm-underline-1 { text-decoration: overline underline; }
|
||||
.xterm-overline.xterm-underline-2 { text-decoration: overline double underline; }
|
||||
.xterm-overline.xterm-underline-3 { text-decoration: overline wavy underline; }
|
||||
.xterm-overline.xterm-underline-4 { text-decoration: overline dotted underline; }
|
||||
.xterm-overline.xterm-underline-5 { text-decoration: overline dashed underline; }
|
||||
|
||||
.xterm-strikethrough {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration {
|
||||
z-index: 6;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.xterm-screen .xterm-decoration-container .xterm-decoration.xterm-decoration-top-layer {
|
||||
z-index: 7;
|
||||
}
|
||||
|
||||
.xterm-decoration-overview-ruler {
|
||||
z-index: 8;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.xterm-decoration-top {
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
}
|
File diff suppressed because one or more lines are too long
|
@ -12,9 +12,9 @@
|
|||
<script type="text/javascript" src="static/agents-graph.js"></script>
|
||||
<script type="text/javascript" src="static/gontrol-helper.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="static/xterm/xterm.css" />
|
||||
<script rel="text/javascript" src="static/xterm/xterm.js"></script>
|
||||
<script rel="text/javascript" src="static/xterm/xterm-addon-fit.js"></script>
|
||||
<!-- <link rel="stylesheet" href="static/xterm/xterm.css" /> -->
|
||||
<!-- <script rel="text/javascript" src="static/xterm/xterm.js"></script> -->
|
||||
<!-- <script rel="text/javascript" src="static/xterm/xterm-addon-fit.js"></script> -->
|
||||
|
||||
<title>g2: gommand & gontrol</title>
|
||||
</head>
|
||||
|
|
|
@ -10,9 +10,13 @@
|
|||
<p>Hostname: {{.HostName}}</p>
|
||||
<!-- <button class="btn btn-warning" hx-get="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}" hx-target="#agentConnect" hx-swap="innerHTML">Proxy</button> -->
|
||||
<a class="btn btn-info"
|
||||
href="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}"
|
||||
href="/proxyAgent/{{.IPv4Address}}:{{.AddPort}}/"
|
||||
hx-target="_blank"
|
||||
target="_blank">Connect via Proxy</a>
|
||||
<!-- <a class="btn btn-info" -->
|
||||
<!-- href="/proxyAgent?ip={{.IPv4Address}}&port={{.AddPort}}" -->
|
||||
<!-- hx-target="_blank" -->
|
||||
<!-- target="_blank">Connect via Proxy</a> -->
|
||||
<a href="http://{{.IPv4Address}}:{{.AddPort}}" class="btn btn-outline-info" target="_blank">Connect without Proxy</a>
|
||||
<!-- <a href="http://{{.IPv4Address}}:{{.AddPort}}" target="_blank">Connect without Proxy</a> -->
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue