diff --git a/AGENTS.md b/AGENTS.md
index 657e917..ed617c2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -63,7 +63,9 @@ scripts/get-novnc.{ps1,sh} скачать noVNC
scripts/get-vnc.ps1 скачать портативный UltraVNC в vnc/ (с верификацией zip)
scripts/list-ips.ps1 список IPv4 машины (используется run.bat)
scripts/open-firewall.bat открыть порт 8080 в Windows Firewall (от админа)
-run.bat запуск в один клик (спрашивает только пароль)
+scripts/set-vnc-password.bat задать VNC-пароль сервису UltraVNC (от админа; без него noVNC получает authentication rejected)
+scripts/ensure-vnc-password.ps1 синхронизирует VNC-пароль UltraVNC с паролем из run.bat (пишет только при несовпадении; UAC только при смене) — вызывается из run.bat
+run.bat запуск в один клик (спрашивает только пароль); синхронизирует VNC-пароль UltraVNC с введённым (см. ensure-vnc-password.ps1); если 5900 уже занят (сервис UltraVNC) — не порождает второй VNC-сервер, а подключается к существующему
```
## Соглашения и подводные камни
@@ -95,4 +97,10 @@ run.bat запуск в один клик (спрашивае
`get-vnc.ps1` теперь верифицирует zip-магию и даёт фолбэк на ручную установку).
- На Windows захват экрана может требовать запуск VNC-сервера от администратора.
- VNC-сервер требует свой пароль; `--vnc-password` передаёт его noVNC
- автоматически, но пароль VNC-сервера нужно один раз настроить под тот же.
\ No newline at end of file
+ автоматически, но пароль VNC-сервера нужно один раз настроить под тот же.
+ **UltraVNC как сервис (`uvnc_service`, LocalSystem):** пароль хранится в
+ `%ProgramData%\UltraVNC\ultravnc.ini`; задать его обычным пользователем через
+ tray-иконку молча не получается (нет прав на запись).
+ `run.bat` синхронизирует этот пароль сам через `scripts/ensure-vnc-password.ps1`
+ (пишет только при несовпадении, UAC только при смене; вручную — `scripts/set-vnc-password.bat` от админа). VNC-пароль = первые 8 байт,
+ поэтому используй ASCII-пароль <= 8 символов и там, и в `run.bat`.
diff --git a/internal/server/static/vnc.html b/internal/server/static/vnc.html
index 7b83794..3c1cde6 100644
--- a/internal/server/static/vnc.html
+++ b/internal/server/static/vnc.html
@@ -25,6 +25,9 @@ const wsURL = (location.protocol === "https:" ? "wss://" : "ws://") + location.h
const statusURL = "/api/status";
const banner = document.getElementById("banner");
let VNC_PASSWORD = "";
+// a securityfailure / unreachable-VNC banner is more specific than the generic
+// disconnect event, so we must not let "disconnect" clobber it.
+let specificError = false;
function showBanner(html){ banner.innerHTML = html; banner.style.display = "block"; }
function hideBanner(){ banner.style.display = "none"; }
@@ -64,18 +67,25 @@ async function connect(){
}
});
rfb.addEventListener("securityfailure", (ev) => {
+ specificError = true;
const reason = (ev.detail && ev.detail.reason) ? ev.detail.reason : "";
showBanner(`
VNC authentication failed
The VNC server rejected the password${reason ? (": " + reason) : "."}
-
Configure the VNC server (UltraVNC/TightVNC) with the same password you use for the web gate, then retry.
+
The VNC server password (UltraVNC: the "VNC Password" field) must equal the password you type in run.bat. VNC passwords are effectively the first 8 bytes — use plain ASCII, max 8 chars.
`);
});
rfb.addEventListener("disconnect", (ev) => {
+ // noVNC's "disconnect" event detail is { clean } only — it never carries a reason.
+ // The real cause was already reported via "securityfailure" or the unreachable-VNC
+ // banner, so do not overwrite it with a useless "Reason: unknown".
+ if (specificError) return;
const d = ev.detail || {};
+ const clean = !!(d && d.clean);
showBanner(`
Disconnected
-
Reason: ${d.reason || "unknown"}
+
Reason: ${clean ? "connection closed" : "connection lost"}. See the browser console (F12) for the exact error.
+
The "noVNC requires a secure context (TLS)" console warning is harmless for plain VNC-password auth. A real failure is usually a VNC password mismatch.
`);
});
window.addEventListener("beforeunload", () => { try { rfb.disconnect(); } catch(e){} });
@@ -85,6 +95,7 @@ async function start(){
const st = await fetchStatus();
if (st) VNC_PASSWORD = st.vncPassword || "";
if (st && st.vnc === false) {
+ specificError = true;
const addr = st.vncAddr || "127.0.0.1:5900";
const hint = st.spawned
? "Auto-launch was attempted but the VNC server is not listening yet. Install UltraVNC/TightVNC on Windows (or x11vnc/TigerVNC on Linux), then restart."
diff --git a/run.bat b/run.bat
index 7de77c7..5cc51d1 100644
--- a/run.bat
+++ b/run.bat
@@ -82,6 +82,11 @@ echo
goto getpw
:gotpw
+REM --- 4b. Sync UltraVNC password with the one entered (only when it differs) ---
+echo UltraVNC . ᮢ ࠢ ...
+powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0scripts\ensure-vnc-password.ps1" -Password "%PW%"
+if errorlevel 1 echo [।०] 㤠 ᨭ UltraVNC - த.
+
REM --- 5. + VNC-ࢥ ---
echo ...
for /f "delims=" %%i in ('web-vnc.exe --gen-hash "%PW%"') do set "HASH=%%i"
@@ -104,7 +109,14 @@ REM --- 7.
echo ᪠ web-vnc http://localhost:8080
echo ⠭: Ctrl+C
echo.
-web-vnc.exe --password-hash "%HASH%" %SPAWNARGS% --spawn --listen :8080
+REM --- 6b. 5900 already listening? -> do NOT spawn a 2nd VNC server (avoid conflict with the UltraVNC service) ---
+set "SPAWNFLAG=--spawn"
+powershell -NoProfile -Command "try{$c=New-Object Net.Sockets.TcpClient('127.0.0.1',5900);$c.Close();exit 0}catch{exit 1}"
+if not errorlevel 1 (
+ echo VNC-ࢥ 㦥 蠥 127.0.0.1:5900 - --spawn.
+ set "SPAWNFLAG="
+)
+web-vnc.exe --password-hash "%HASH%" %SPAWNARGS% %SPAWNFLAG% --listen :8080
echo.
echo ࢥ ⠭.
pause
diff --git a/scripts/ensure-vnc-password.ps1 b/scripts/ensure-vnc-password.ps1
new file mode 100644
index 0000000..66bb44c
--- /dev/null
+++ b/scripts/ensure-vnc-password.ps1
@@ -0,0 +1,126 @@
+# ensure-vnc-password.ps1 - keep the UltraVNC service VNC password in sync
+# with the password the user types in run.bat. Called from run.bat.
+#
+# It only WRITES when the password actually differs (so a normal launch with
+# the same password does nothing and asks for NO admin rights). When it must
+# change the password it re-launches itself elevated (one UAC prompt) and
+# restarts the uvnc_service so the new password takes effect.
+#
+# Usage: powershell -NoProfile -ExecutionPolicy Bypass -File ensure-vnc-password.ps1 -Password ""
+# Exit codes: 0 = password matches (or was set OK); 1 = could not set it.
+
+param([Parameter(Mandatory=$true)][string]$Password)
+
+$ErrorActionPreference = "Stop"
+$ini = Join-Path $env:ProgramData "UltraVNC\ultravnc.ini"
+
+function Write-Info($m){ Write-Host "ensure-vnc-password: $m" }
+
+if (-not (Test-Path $ini)) {
+ # No installed UltraVNC service config (e.g. portable copy / TightVNC) - nothing to sync.
+ Write-Info "no %ProgramData%\UltraVNC\ultravnc.ini - nothing to sync."
+ exit 0
+}
+
+# --- VNC password obfuscation: DES-ECB(key=bitrev([23,82,107,6,35,78,88,7]), pw8) + 1-byte checksum ---
+function Invoke-BitRev([byte]$b){ $r=0; for($i=0;$i -lt 8;$i++){ $r = $r -bor ((($b -shr $i) -band 1) -shl (7-$i)) }; return [byte]$r }
+function Get-ExpectedPasswd([string]$pw){
+ $fixedkey = [byte[]](23,82,107,6,35,78,88,7)
+ $kr = New-Object byte[] 8; for($i=0;$i -lt 8;$i++){ $kr[$i] = Invoke-BitRev $fixedkey[$i] }
+ $pb = [System.Text.Encoding]::ASCII.GetBytes($pw)
+ if ($pb.Length -gt 8) { $pb = $pb[0..7] }
+ $plain = New-Object byte[] 8; for($i=0;$i -lt 8;$i++){ if($i -lt $pb.Length){ $plain[$i] = $pb[$i] } }
+ $des = New-Object System.Security.Cryptography.DESCryptoServiceProvider
+ $des.Mode = [System.Security.Cryptography.CipherMode]::ECB
+ $des.Padding = [System.Security.Cryptography.PaddingMode]::None
+ $des.Key = $kr
+ $cipher = $des.CreateEncryptor().TransformFinalBlock($plain,0,8)
+ $sum = 0; foreach($b in $cipher){ $sum = ($sum + $b) -band 0xFF }
+ $h = ($cipher | ForEach-Object { $_.ToString("X2") }) -join ""
+ return $h + $sum.ToString("X2")
+}
+
+function Get-CurrentPasswd{
+ $line = Get-Content $ini -ErrorAction SilentlyContinue | Where-Object { $_ -match '^\s*passwd\s*=' -and $_ -notmatch 'passwd2' } | Select-Object -First 1
+ if (-not $line) { return "" }
+ return ($line -split '=',2)[1].Trim()
+}
+
+$expected = Get-ExpectedPasswd $Password
+$current = Get-CurrentPasswd
+if ($current -ieq $expected) {
+ Write-Info "UltraVNC password already matches - nothing to do."
+ exit 0
+}
+
+Write-Info "UltraVNC password differs (ini=$current, expected=$expected). Need to set it (admin required)."
+
+# Locate UltraVNC install + password tools.
+$uvncDirs = @(
+ "$env:ProgramFiles\uvnc bvba\UltraVNC",
+ "$env:ProgramFiles\UltraVNC",
+ "${env:ProgramFiles(x86)}\uvnc bvba\UltraVNC",
+ "${env:ProgramFiles(x86)}\UltraVNC"
+)
+$uvnc = $uvncDirs | Where-Object { Test-Path "$_\winvnc.exe" } | Select-Object -First 1
+if (-not $uvnc) { Write-Info "UltraVNC install not found - cannot set password."; exit 1 }
+
+# Are we admin?
+$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
+
+if (-not $isAdmin) {
+ Write-Info "re-launching elevated to set the password..."
+ $p = Start-Process -FilePath "powershell.exe" -Verb RunAs -Wait -PassThru -ArgumentList @("-NoProfile","-ExecutionPolicy","Bypass","-File","`"$PSCommandPath`"","-Password","`"$Password`"")
+ if ($p.ExitCode -ne 0) { Write-Info "elevated run failed (exit $($p.ExitCode))."; exit 1 }
+ $now = Get-CurrentPasswd
+ if ($now -ieq $expected) { Write-Info "password set OK."; exit 0 }
+ Write-Info "password still not updated after elevated run (ini=$now)."; exit 1
+}
+
+# --- admin path: actually set the password ---
+function Set-Tool{
+ param([string]$exe)
+ if (-not (Test-Path $exe)) { return $false }
+ try {
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
+ $psi.FileName = $exe
+ $psi.Arguments = "`"$Password`""
+ $psi.UseShellExecute = $false
+ $p = [System.Diagnostics.Process]::Start($psi)
+ if (-not $p.WaitForExit(5000)) { $p.Kill() }
+ return ($p.ExitCode -eq 0)
+ } catch { return $false }
+}
+
+$ok = $false
+if (Set-Tool (Join-Path $uvnc "createpassword.exe")) { $ok = $true }
+$now = Get-CurrentPasswd
+if ($now -ine $expected -and (Test-Path (Join-Path $uvnc "setpasswd.exe"))) {
+ # fallback to setpasswd.exe ""
+ try {
+ $psi = New-Object System.Diagnostics.ProcessStartInfo
+ $psi.FileName = Join-Path $uvnc "setpasswd.exe"
+ $psi.Arguments = "`"$Password`""
+ $psi.UseShellExecute = $false
+ $p = [System.Diagnostics.Process]::Start($psi)
+ if (-not $p.WaitForExit(5000)) { $p.Kill() }
+ } catch {}
+ $now = Get-CurrentPasswd
+}
+
+if ($now -ine $expected) {
+ Write-Info "tools did not update the password (ini=$now). Set it manually via UltraVNC Admin Properties as admin."
+ exit 1
+}
+Write-Info "password updated in $ini."
+
+# Restart the service so it reloads the new password.
+$svc = "uvnc_service"
+$svcExists = $false
+try { $null = (& sc.exe query $svc 2>$null); $svcExists = ($LASTEXITCODE -eq 0) } catch {}
+if ($svcExists) {
+ Write-Info "restarting service $svc ..."
+ & net.exe stop $svc 2>$null | Out-Null
+ & net.exe start $svc 2>$null | Out-Null
+}
+exit 0
\ No newline at end of file
diff --git a/scripts/set-vnc-password.bat b/scripts/set-vnc-password.bat
new file mode 100644
index 0000000..8a45693
--- /dev/null
+++ b/scripts/set-vnc-password.bat
@@ -0,0 +1,109 @@
+@echo off
+REM ============================================================
+REM set-vnc-password.bat - reliably set the UltraVNC VNC password
+REM for the running service (uvnc_service / LocalSystem), which
+REM reads %ProgramData%\UltraVNC\ultravnc.ini.
+REM
+REM Runs AS ADMINISTRATOR (auto-elevates). It first tries the
+REM UltraVNC CLI helpers (createpassword.exe / setpasswd.exe) and
+REM VERIFIES the password actually changed in ultravnc.ini; if they
+REM fail silently (as they can), it opens the UltraVNC settings GUI
+REM so you can set "VNC Password" there. Then it restarts the
+REM service. Use the SAME password in run.bat (ASCII, max 8 chars).
+REM ============================================================
+chcp 866 >nul
+cd /d "%~dp0"
+
+net session >nul 2>nul
+if errorlevel 1 (
+ echo Requesting administrator rights ...
+ powershell -Command "Start-Process -FilePath '%~f0' -Verb RunAs"
+ exit /b
+)
+
+set "UVNC="
+for %%P in (
+ "C:\Program Files\uvnc bvba\UltraVNC"
+ "C:\Program Files\UltraVNC"
+ "C:\Program Files (x86)\uvnc bvba\UltraVNC"
+ "C:\Program Files (x86)\UltraVNC"
+) do (
+ if exist "%%~P\winvnc.exe" if not defined UVNC set "UVNC=%%~P"
+)
+if not defined UVNC (
+ echo [error] UltraVNC installation not found.
+ pause
+ exit /b 1
+)
+echo UltraVNC: %UVNC%
+set "INI=%ProgramData%\UltraVNC\ultravnc.ini"
+
+:getpw
+set "PW="
+set /p "PW=Enter VNC password (ASCII, max 8 chars): "
+if "%PW%"=="" ( echo Password cannot be empty. & goto getpw )
+
+REM record old passwd
+set "OLDPW="
+if exist "%INI%" for /f "tokens=2 delims==" %%a in ('findstr /b /i "passwd=" "%INI%" 2^>nul ^| findstr /v /i "passwd2"') do set "OLDPW=%%a"
+echo Old passwd=%OLDPW%
+
+REM try CLI helpers, verify after each
+set "DONE=0"
+if exist "%UVNC%\createpassword.exe" (
+ echo Trying createpassword.exe ...
+ "%UVNC%\createpassword.exe" "%PW%" >nul 2>nul
+ call :checkchanged
+)
+if "%DONE%"=="0" if exist "%UVNC%\setpasswd.exe" (
+ echo Trying setpasswd.exe ...
+ "%UVNC%\setpasswd.exe" "%PW%" "%PW%" >nul 2>nul
+ call :checkchanged
+)
+if "%DONE%"=="0" (
+ echo.
+ echo CLI helpers did not change the password file. Opening UltraVNC settings...
+ echo -> right-click the UltraVNC tray icon, choose Admin Properties,
+ echo set the "VNC Password" to: %PW%
+ echo -> close the settings/tray app, then come back here and press a key.
+ echo.
+ start "" "%UVNC%\winvnc.exe"
+ pause
+ call :checkchanged
+)
+set "PW="
+if "%DONE%"=="0" (
+ echo [warn] Password still not changed in %INI%.
+ echo Set it manually in UltraVNC Admin Properties as administrator.
+ pause
+ exit /b 1
+)
+
+REM restart the service so it reloads the password
+set "SVC=uvnc_service"
+sc query %SVC% >nul 2>nul
+if errorlevel 1 (
+ echo [warn] Service '%SVC%' not found; restart winvnc manually if it runs in app mode.
+ goto showini
+)
+echo Restarting service %SVC% ...
+net stop %SVC% >nul 2>nul
+net start %SVC%
+
+:showini
+echo.
+echo --- %INI% ---
+if exist "%INI%" type "%INI%"
+echo.
+echo Done. Use the SAME password in run.bat, then reconnect in the browser.
+pause
+exit /b
+
+:checkchanged
+set "NEWPW="
+if exist "%INI%" for /f "tokens=2 delims==" %%a in ('findstr /b /i "passwd=" "%INI%" 2^>nul ^| findstr /v /i "passwd2"') do set "NEWPW=%%a"
+if not "%NEWPW%"=="%OLDPW%" (
+ echo Password file updated. New passwd=%NEWPW%
+ set "DONE=1"
+)
+goto :eof
\ No newline at end of file