From 6a1ee64b1428b92eb4e14b47f30c7ff50ee41593 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 30 Jul 2026 19:15:09 +0300 Subject: [PATCH] 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). --- AGENTS.md | 12 ++- internal/server/static/vnc.html | 15 +++- run.bat | 14 +++- scripts/ensure-vnc-password.ps1 | 126 ++++++++++++++++++++++++++++++++ scripts/set-vnc-password.bat | 109 +++++++++++++++++++++++++++ 5 files changed, 271 insertions(+), 5 deletions(-) create mode 100644 scripts/ensure-vnc-password.ps1 create mode 100644 scripts/set-vnc-password.bat 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