diff --git a/main.go b/main.go index 958885c..9285a98 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "fmt" "html/template" "log" + "net" "net/http" "strconv" "sync" @@ -40,8 +41,8 @@ var ( //go:embed static/* var staticFiles embed.FS -//go:embed templates -var templateFiles embed.FS +// //go:embed templates +// var templateFiles embed.FS type Config struct { Database struct { @@ -68,8 +69,20 @@ func processError(err error) { func init() { tmpl, _ = template.ParseGlob("templates/*.html") + // var err error + // tmpl, err = template.ParseFS( + // templateFiles, + // "templates/*.html", + // "templates/partials/*.html", + // ) + + // if err != nil { + // log.Fatalf("Failed to parse embedded templates: %v", err) + // } + // Sqlite3 err := logger.InitDB("/tmp/gontrol_logs.db") + if err != nil { log.Fatal(err) } @@ -78,6 +91,19 @@ func init() { db = database.InitSQLiteDB("/tmp/gontrol_agents.db") } +// func renderTemplate(w http.ResponseWriter, tmplPath string, data interface{}) { +// t := tmpl.Lookup(strings.TrimPrefix(tmplPath, "templates/")) +// if t == nil { +// log.Printf("Template %s not found", tmplPath) +// http.Error(w, "Internal Server Error", http.StatusInternalServerError) +// return +// } + +// if err := t.Execute(w, data); err != nil { +// log.Printf("Failed to render template %s: %v", tmplPath, err) +// http.Error(w, "Internal Server Error", http.StatusInternalServerError) +// } + func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) { t, err := template.ParseFiles(tmpl) if err != nil { @@ -109,6 +135,7 @@ func agentsHandler(w http.ResponseWriter, r *http.Request) { } else { agent, _ := api.GetAgent(db, w, r, agentId) renderTemplate(w, "templates/partials/agent_detail.html", agent) + // renderTemplate(w, "agent_detail.html", agent) } case http.MethodPost: api.CreateAgent(db, w, r) @@ -155,6 +182,7 @@ func listAgents(w http.ResponseWriter, r *http.Request) { } renderTemplate(w, "templates/partials/agent_list.html", agents) + // renderTemplate(w, "agent_list.html", agents) } func getAgentsStatus() []string { @@ -218,167 +246,67 @@ func logsHandler(w http.ResponseWriter, r *http.Request) { renderTemplate(w, "templates/partials/logs_partial.html", logs) + // renderTemplate(w, "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. +// 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 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 - } + 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 + } - agentBaseURL := "http://" + agentIP + ":" + agentPort + // 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 + } - targetURL, err := url.Parse(agentBaseURL) - if err != nil { - http.Error(w, "Invalid agent URL: "+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 - proxy := httputil.NewSingleHostReverseProxy(targetURL) + // 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 = "/" + } + } - // Modify the Director function to rewrite the request URL before sending to agent - originalDirector := proxy.Director - proxy.Director = func(req *http.Request) { - originalDirector(req) + // Scrub ip/port from downstream query + q := req.URL.Query() + q.Del("ip") + q.Del("port") + req.URL.RawQuery = q.Encode() - // 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 = "/" - } - } + // Preserve Host (many CLI-style servers care) + req.Host = target.Host + }, - // 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() + // 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, + }, - // Optional: set the Host header to the target host - req.Host = targetURL.Host - } + FlushInterval: -1, // stream bytes immediately + } - proxy.ServeHTTP(w, r) + 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() { // sqlite3 has been initialized in init() @@ -405,8 +333,8 @@ func main() { webMux.HandleFunc("/logs", logsHandler) webMux.HandleFunc("/logs/{level}", logsHandler) webMux.Handle("/static/", http.FileServer(http.FS(staticFiles))) - webMux.Handle("/templates/", http.FileServer(http.FS(templateFiles))) webMux.HandleFunc("/proxyAgent", proxyAgentHandler) + webMux.HandleFunc("/proxyAgent/", proxyAgentHandler) // db := database.InitDB (cfg.Database.Username, cfg.Database.Password, cfg.Database.Host, cfg.Database.Port, cfg.Database.Name) // defer db.Close() diff --git a/static/start-interactive.js b/static/start-interactive.js index 60d10e6..8d3310a 100644 --- a/static/start-interactive.js +++ b/static/start-interactive.js @@ -10,6 +10,7 @@ document.addEventListener("DOMContentLoaded", function () { if (!interactiveMode && event.key === "Enter") { const command = input.value.trim(); if (command === "start-interactive") { + startInteractiveSession(); input.value = ""; event.preventDefault(); @@ -19,6 +20,30 @@ document.addEventListener("DOMContentLoaded", function () { } }); + // 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"); // "8080" +const usingProxy = proxyIp && proxyPort; // truthy only in that case + + if (usingProxy) { + // Build ws(s)://