feat(run): sync UltraVNC password from run.bat + clearer noVNC errors

- run.bat: call scripts/ensure-vnc-password.ps1 so the password typed once
  also sets the UltraVNC service VNC password (UAC only when it differs);
  skip --spawn when 5900 is already taken (don't launch a 2nd winvnc).
- scripts/ensure-vnc-password.ps1: new - compares the password against
  %ProgramData%\UltraVNC\ultravnc.ini (reverse-engineered UltraVNC DES
  obfuscation) and only writes+restarts the service when it differs.
- scripts/set-vnc-password.bat: set UltraVNC service VNC password as admin
  (createpassword/setpasswd + verify, GUI fallback). Manual fallback.
- internal/server/static/vnc.html: don't let the generic 'disconnect'
  banner overwrite the specific 'securityfailure' reason (noVNC's
  disconnect event has no reason field, so it always said 'unknown').
- AGENTS.md: document the above (scripts, run.bat, UltraVNC service note).
This commit is contained in:
Codex
2026-07-30 19:15:09 +03:00
parent cf0915878b
commit 6a1ee64b14
5 changed files with 271 additions and 5 deletions
+9 -1
View File
@@ -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-сервер, а подключается к существующему
```
## Соглашения и подводные камни
@@ -96,3 +98,9 @@ run.bat запуск в один клик (спрашивае
- На Windows захват экрана может требовать запуск VNC-сервера от администратора.
- VNC-сервер требует свой пароль; `--vnc-password` передаёт его noVNC
автоматически, но пароль 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`.
+13 -2
View File
@@ -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(`
<h2>VNC authentication failed</h2>
<div>The VNC server rejected the password${reason ? (": " + reason) : "."}</div>
<div class="hint">Configure the VNC server (UltraVNC/TightVNC) with the same password you use for the web gate, then retry.</div>
<div class="hint">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.</div>
<button onclick="location.reload()">Retry</button>`);
});
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(`
<h2>Disconnected</h2>
<div>Reason: ${d.reason || "unknown"}</div>
<div>Reason: ${clean ? "connection closed" : "connection lost"}. See the browser console (F12) for the exact error.</div>
<div class="hint">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.</div>
<button onclick="location.reload()">Retry</button>`);
});
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."
+13 -1
View File
@@ -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
+126
View File
@@ -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 "<pw>"
# 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 "<pw>"
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
+109
View File
@@ -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