Repository navigation
SpyTV v1.0.6 - C2 startup crash, silent dashboard buttons
SpyTV v1.0.6
Three of the four things that made the console feel broken came down to code
that failed without saying so. This release fixes the C2's own startup
crash and makes every button report what happened.
Fixed
1. The C2 could exit before it served a single request (critical)
main() prints a startup hint containing ⚙. On Windows, a piped or
redirected stdout is encoded with the ANSI code page (cp1252), which has no
such character — print() raised UnicodeEncodeError.
Because the order in main() is bind the port → print the banner → print the
hint → serve_forever(), the exception fired after the port was bound but
before the server started serving. The process died during startup: no
dashboard, no API, and the only visible effect was a terminal window that
opened with a traceback — which is what "the C2 keeps restarting, opening a
terminal window again" looks like from the operator's side.
An interactive console doesn't hit this (Python writes through the Unicode
console API), so it only surfaced whenever output was captured or redirected.
Fix: _harden_console_output() runs first in main() and reconfigures
sys.stdout / sys.stderr with errors="replace". Each stream keeps its own
encoding — interactive consoles still render ⚙ correctly — and substitutes a
placeholder for anything it cannot represent. No print() can be fatal again.
2. The Scan button failed without a message
startScan() had no error handling. Any failure — non-200, non-JSON body,
missing results, network error — died as an unhandled rejection, so the
results panel stayed exactly as it was and the log stopped at
"Starting network scan...". A dead button.
It now disables and relabels the button (Scanning...), writes a progress
note into the results panel itself, validates that results is a list, and
on failure shows a warning panel naming the reason while keeping the previous
rows. The button is restored in finally, so every path re-enables it.
Evidence from your own C2's history: 8 scans between 13:06 and 13:13, one
every 40–80s, each returning the same single host — with no spinner, no
disabled button and no in-panel feedback, a minute of silence looks like
nothing happened.
3. Client-details submit could hang with the button stuck
api() was await fetch(...); return r.json() — no timeout, no status
check, no non-JSON handling. fetch has no default timeout, so a C2 that
never answered left the promise pending forever: submitClientForm()'s
finally never ran, the Continue button stayed disabled and the modal never
closed.
api() now arms an AbortController per call — 30s by default, 600s
for /api/cve/patch (it stacks a 45s-per-command cap across several commands)
and 300s for scan / exploit / patch / cve — tolerates empty and non-JSON
replies, and rethrows timeouts and network failures as messages you can act on
("check that the C2 is running"). The
submit button also shows Submitting... while in flight.
4. Error bodies no longer blank the CVE tab or the device grid
renderCVEs() tested !cves || cves.length === 0. A failed /api/cves reply
is an object, so length was undefined, the empty-check passed, and
cves.map threw — the tab went blank with no message. The loaders
(loadDevices / loadClips / loadLocalIp) now only adopt a well-formed
reply, so a 500 or timeout can no longer overwrite the last good state.
5. The Exploit Payload Generator's Generate button did nothing
generatePayload() was the only generate path with no error handling: no
try/catch, no res.error check, no button state. It validated the form with
alert() — a suppressed, backgrounded or missed dialog leaves no trace, so an
empty Target IP looked exactly like a broken button. It also never wrote to the
card's own output container, so every failure was invisible.
The server was healthy the whole time — POST /api/payload measured on your
live C2 returned 200 in 0.07s for tizen-dev, webos-dev and dlna, the
route exists, and every element id the function touches is present — which is
what pointed at the client.
Now: inline validation on the card (never alert()), a Generating...
disabled button, a surfaced res.error, a reply without a payload reported
instead of opening a preview full of undefined, and the preview title reset
(it is shared with the AI generator, which retitles it to the CVE id).
deployPayload() got the same treatment.
Verification
| Check | Result |
|---|---|
| Full pytest suite | 107 passed (+1 startup crash, +2 dashboard) |
| Startup crash test | fails with the fix reverted: UnicodeEncodeError … '\u2699' |
| Dashboard script parse | node --check clean (before and after) |
| Dashboard behaviour (Node, stub DOM) | 67 assertions, 0 failures (13 sections) |
| Live scan against a new-code C2 | 200 in 38.1s, found=2, results is a list |
| Live authorize (client details) | 200 in 1.02s, PDF preview 0.02s |
| Live authorize while a scan runs | 200 in 0.43s — did not block |
The dashboard suite (tests/test_dashboard_ui.py) executes the whole inline
script in Node against a stub DOM: happy path, server rejection, unreachable
C2, malformed reply, the renderScanResults(undefined) guard, both
client-details outcomes, the bounded hang, the per-endpoint budgets and the new
list guards. It skips when node is absent (node is already required for the
Electron build).
Upgrading
A running C2 executes the code it was started with — restart it to pick up
this release:
python c2_server.py
No Android or desktop-shell source changed, so the app you already have
loads the fixed dashboard from the restarted C2.