Add a dedicated security audit trail to the Go server. Login attempts,
authorization denials, WebSocket lifecycle, remote screen and terminal
sessions, clipboard operations, device authentication and admin mutations
now emit stable JSON-line events (audit=true, event=<name>) to a separate
logs/audit.log, mirrored to server.log. The path is configurable through
YAMA_AUDIT_LOG and the separate file can be switched off. Records never
contain passcodes, password hashes, tokens or clipboard contents, and
untrusted string fields are stripped of control characters and truncated
so they cannot forge log lines.
Expose the trail to administrators through a query_audit WebSocket command
and an Audit Log page in the web UI. The page filters by time range, event
(exact or prefix), actor, IP, device id, keyword and warning level, pages
newest-first, and stays hidden unless the server advertises the auditLog
capability, so older C++ servers degrade cleanly. Each query scans at most
the newest 32 MB of the current file to bound latency, records itself as
admin.audit.query, and tolerates torn or non-audit lines.
Add unit tests for event emission and redaction, query filtering, paging,
malformed lines, missing or disabled audit files, and handler
authorization.
Co-Authored-By: deepseek-flash
Persist every device that has ever logged in to the Go server in a small
JSON store (devices.json, overridable with YAMA_DEVICES_FILE and capped by
YAMA_DEVICES_MAX, default 1000 newest). The store follows the users.json
convention: atomic temp-file write with fsync and rename, a 500 ms debounce
for bursts of reconnects, first_seen preserved across logins, and pruning
that evicts offline records before devices that are still connected.
Devices connected at shutdown are stamped offline before the final flush so
last_seen stays accurate across restarts.
The server advertises an offlineDevices capability and device_list now
appends historical devices that are not online, marked with online:false and
last_seen. The web UI shows an Offline checkbox after Total/Showing (state
kept in localStorage), renders offline cards dimmed and dashed, and always
lists them in the screen-wall sidebar. Older servers without the capability
keep the online-only list.
Grid and List cards gain a delete button after the terminal button, admin
only, behind a confirmation dialog. Deleting an offline device removes its
history record; deleting an online device first sends the single-byte
TOKEN_UNINSTALL command (matching the MFC OnOnlineUninstall flow) and then
removes the record too, so the device does not come back as a dead offline
entry. The agent's own disconnect then runs the existing cleanup path:
screen and terminal sub-connections are closed, the hub entry is removed,
and browsers receive device_offline to release their watch sets, wall
sessions, and stream references. Wall mode exposes no delete action.
Tests cover history persistence/pruning/recovery, offline ordering, the
device_list merge, and all three delete outcomes (offline removal, online
uninstall with a single 0x3F byte, and viewer rejection).
Co-Authored-By: deepseek-flash
Bring the Go web backend up to parity with the MFC server's screen wall.
A browser tab can now stream several devices over one WebSocket: the
single `watching` field becomes a per-connection watch set guarded by the
hub mutex, capped at 32 devices per socket. The challenge advertises
`caps.multiWatch` and `caps.keyframeRequest`, binary frames stay keyed by
the low 32 bits of the device ID, and connect/disconnect replies echo the
device id and request id so the wall can route concurrent responses.
Input commands (mouse, key, clipboard, rdp_reset) resolve their target
from the watch set: an explicit id must be a current subscription, and an
absent id is accepted only while exactly one device is watched. Legacy
frontends that do not send `multiWatch` keep replace-all semantics, so a
repeated device switch cannot grow the set without bound. Offline
devices are removed from every watch set, and teardown is computed from a
snapshot taken outside the hub lock to avoid re-entrant locking.
Add the `keyframe` command: it replays the cached IDR, then asks the
device to emit a fresh one, rate-limited to one request per device per
second. This exposed a Go-only corruption bug: the client answers the
request with TOKEN_KEYFRAME (134), which the dispatcher treated as a
no-op. That token only appears on forced IDRs because the client's GOP is
effectively infinite, so the dropped IDR left the browser decoding
P-frames against a reference chain it never received, producing
persistent garbling. Relay it as a keyframe instead. Also drop the
cached IDR when the resolution changes, mirroring the C++ server, so a
late joiner cannot receive an old-geometry keyframe.
Add tests for the watch-set transitions and for relaying TOKEN_KEYFRAME.
Co-Authored-By: deepseek-flash
Add keyboard-shortcut (Ctrl+C / Ctrl+X / Ctrl+V) text clipboard transfer
between the browser and the remote desktop, reusing the clipboard protocol
that already exists in the native viewer (COMMAND_SCREEN_GET_CLIPBOARD=24,
COMMAND_SCREEN_SET_CLIPBOARD=25, TOKEN_CLIPBOARD_TEXT=118) so the client
needs no change.
Local -> remote: pressing Ctrl+V reads the browser clipboard, pushes it to
the remote clipboard (UTF-8 -> GBK, matching the client's CF_TEXT/ANSI
handling) and then synthesizes a remote Ctrl+V so the pasted text is the
newly delivered one.
Remote -> local: Ctrl+C or Ctrl+X is forwarded to the remote as usual and,
after a short delay, the remote clipboard is pulled back and written to the
browser clipboard for a later local Ctrl+V.
Both the MFC and Go servers get the same three clipboard signaling paths
(clipboard_set, clipboard_get, clipboard_text) so the shared single-page
frontend behaves identically on either backend.
Co-Authored-By: deepseek-v4-pro
SOCKET_DLLLOADER (210) is the shellcode / memory-DLL loader asking the
server for the full DLL payload before it can load the client. The MFC
master console serves it via SendServerDll; the Go web device port has no
payload to hand out, so every memory-DLL client startup/reconnect fell
into the default dispatch branch and logged "Unhandled command 210",
flooding the log. Add a protocol constant and drop the command silently.
While auditing the device-port logging surface:
- Downgrade wrapTLS's per-connection hex-dump lines and the screen-spy
"[timing]" cold-start line from Info to Debug (they fire once per TCP
connection / screen view and are diagnostics, not operational events).
- Distinguish a sub-connection hijack attempt from a plain orphan: when a
screen/terminal sub-conn claims a MasterID from a source IP other than
the device's login conn, log a dedicated Warn instead of mislabeling it
as "orphan ... unknown device".
Co-Authored-By: deepseek-v4-pro
Verify the HMAC-SHA256 signature and timestamp of device sub-connections
(screen/terminal/file) when the YAMA_CONN_AUTH_KEY environment variable is
set. With the key set, a sub-connection whose signature or clock skew fails
is answered with a non-OK ConnAuth status and dropped; without the key the
legacy always-OK behavior is preserved.
The key is never compiled in: it is read from the environment at startup, so
no secret enters the repository. Source-IP binding in hub.BindScreenConn
remains in place as defense-in-depth.
Co-Authored-By: deepseek-v4-pro
When the web remote desktop is mounted under another domain's subpath
(YAMA_WEB_BASE_PATH non-empty), read the product title from
YAMA_WEB_BRAND_NAME, substitute it for the SimpleRemoter titles and
PWA manifest name, and strip the security notice and website links.
The brand name is JSON- and HTML-escaped so operator input can't
produce malformed output. Served from the domain root, the official
SimpleRemoter branding is left unchanged.
Co-Authored-By: deepseek-v4-pro
Add optional one-way TLS on the device port (6543), enabled per client via a
pinned trust anchor, so new clients encrypt their transport while legacy
plaintext clients keep working unchanged.
Server (server/go): sniff each accepted connection and wrap TLS peers
(ClientHello 0x16 0x03 + handshake type 0x01) in crypto/tls with MinVersion
TLS 1.2; everything else stays on the plaintext path. TLS is enabled only when
both YAMA_TLS_CERT and YAMA_TLS_KEY are set, and a cert/key load failure fails
startup loudly rather than silently serving plaintext.
Client (client/, Windows): Schannel one-way TLS. When the trust anchor is
present — registry value Software\<exehash>\settings\tls_cert (REG_SZ holding
the PEM), with fallback to settings.ini [settings] tls_cert or yama-ca.pem
beside the exe — every outbound connection (main + sub-connections) completes
a TLS 1.2 handshake before the app protocol and is strictly pinned to that
anchor via a chain engine with hExclusiveRoot; any handshake/cert failure is
fail-closed (no plaintext downgrade). Send uses EncryptMessage; Recv uses
DecryptMessage and reads plaintext from the stream-mode DATA segment (bufs[1])
rather than the record header.
Co-Authored-By: deepseek-v4-pro
Consistent with the per-IP limits, a zero cap now means "no limit" instead
of closing every connection after its first byte, so a Config built from
zero values cannot wedge the listener.
Co-Authored-By: deepseek-v4-pro
The protocol length field is validated to 50 MiB, but a peer that sends
bytes which never assemble into a complete packet — for example an HTTP-mask
envelope without its terminating CRLF CRLF, which Parse() waits on
indefinitely — could grow the per-connection input buffer without bound.
handleConnection now closes the connection once unparsed buffered bytes
exceed MaxBufferedBytes (default 50 MiB + 64 KiB slack, covering the largest
legitimate packet plus its mask envelope and a read chunk).
Co-Authored-By: deepseek-v4-pro
The TCP device port is exposed directly to the public internet, so a single
host must not be able to exhaust the global connection budget, flood new
connection attempts, or brute-force the device authorization passcode.
- server: cap concurrent connections and the accept rate per source IP
(defaults 256 concurrent / 120 per minute, overridable via
YAMA_PERIP_MAX_CONNS and YAMA_PERIP_CONN_RATE). Rejections happen at
accept time, before any buffer or goroutine is allocated, and a rejected
device simply retries on its reconnect loop.
- cmd: throttle TOKEN_AUTH per source IP (120/min) and answer over-limit
attempts with a normal failed response so clients get a definitive reply.
- web: periodically sweep the login rate-limit maps so the username-keyed
one (which is attacker-controlled) cannot grow without bound.
Per-IP defaults are generous enough that a NAT office whose devices share
one source IP is unaffected.
Co-Authored-By: deepseek-v4-pro
A screen or terminal sub-connection was keyed solely by a MasterID that any
TCP peer may supply, so any internet host could claim an online device's
MasterID and bind a sub-conn in its place, hijacking the live screen stream
or a pending shell session. The ConnAuth signature cannot be verified on the
server because its key lives in a vendored client library, so the trust
anchor here is the source IP: sub-conns are always opened device-to-server
from the same host as the login conn. BindScreenConn and BindTerminalConn
now reject a sub-conn whose peer IP differs from the device's recorded
main-connection PeerIP.
Co-Authored-By: deepseek-v4-pro
Bridge an already-authenticated portal session into a YAMA web session via
a new /sso endpoint, so portal administrators can enter the remote-desktop
UI without a second login. Only the "admin" portal role is bridged; any
other role (viewer, unknown) still falls back to the built-in login form,
so a low-privileged portal account cannot bypass authentication.
- wsauth: extract IssueToken from VerifyLogin so an out-of-band-authenticated
identity (SSO) can mint a session token without the challenge/response.
- web: add the SSODetector interface, a fail-closed JWT detector (HMAC-only),
and the /sso handler gated to the admin role.
- cmd: enable the JWT detector via YAMA_SSO_JWT_SECRET, the shared HS256
secret matching the parent portal.
- web UI: attempt SSO before showing the login form, falling back to login
when the server returns 401.
- docs: document SSO (admin-only) and license trial-vs-paid troubleshooting
in README and the env example.
Co-Authored-By: deepseek-v4-pro
iOS WebKit lacks Media Source Extensions with WebM/Opus, so remote audio
was silent on iPhone Safari/Chrome even though audio frames kept flowing.
Add a Web Audio API fallback path that decodes Opus packets with the
`opus-decoder` WASM library and schedules them onto an AudioContext.
The decoder bundle is now self-hosted — served at /static/opus-decoder.min.js
by both the Go and C++ servers — instead of pulled from a CDN, which was
unreachable from some networks.
Gotcha fixed during bring-up: the unscoped `opus-decoder` build's
`OpusDecoder.decodeFrame` is synchronous and returns {channelData,
samplesDecoded, sampleRate, errors} directly, not a Promise, so it must be
called in try/catch rather than via .then().
Also remove the on-screen diagnostic overlay (audio toast + frame counter)
used while debugging the phone without a console; diagnostics now go to
console.* only.
Co-Authored-By: deepseek-v4-pro
Add optional YAMA_WEB_BASE_PATH to mount the whole web UI (index, manifest,
static assets, /health, /api/devices, /ws) under a URL prefix such as
/remoter behind a reverse proxy. index.html now uses a __BASE_PATH__
placeholder substituted at serve time, the PWA manifest start_url is rooted
at the base path, and a mount wrapper redirects the bare prefix to prefix/
and strips it before routing. Include nginx, systemd, and env-file deployment
examples for the subpath setup.
The shared placeholder is also handled by the MFC server, which serves from
the domain root and strips __BASE_PATH__ (to "") so its web remote desktop
URLs stay rooted.
Co-Authored-By: deepseek-v4-pro
GitHub mirror is no longer maintained; v1.3.4+ releases land on Gitea only.
Repoint the Release-version badge and Download-Latest button (href + shields
endpoint + logo) at git.simpleremoter.com so visitors don't end up on a stale
GitHub release page.
Stars/forks badges stay on GitHub — vanity counters reflecting historical
accumulation, not navigation targets.
Also: server/go/README.md yama-issue-token link and the line-294 Markdown
"Releases" link in all three READMEs now point at Gitea.