]>
| Commit | Line | Data |
|---|---|---|
| 1 | @echo off | |
| 2 | setlocal enableextensions enabledelayedexpansion | |
| 3 | title setup-windows-xp | |
| 4 | ||
| 5 | @rem --------------------------------------------------------------------------- | |
| 6 | @rem setup-windows-xp.bat - provision a Windows XP VM with an SSH server + Python | |
| 7 | @rem --------------------------------------------------------------------------- | |
| 8 | @rem Companion to setup-windows.bat, for the throwaway XP VM that the XP-toolset | |
| 9 | @rem builds get tested on. Almost none of the modern machinery exists here: no | |
| 10 | @rem winget, no PowerShell, no where.exe, no setx.exe, no curl, no tar. The one | |
| 11 | @rem that shapes this whole script is TLS: XP's SChannel stops at TLS 1.0, so the | |
| 12 | @rem VM cannot download anything from python.org, sourceforge or github over | |
| 13 | @rem HTTPS. So nothing is fetched at run time. Stage the installers on the HOST, | |
| 14 | @rem into the folder this script lives in (or a vendor-xp\ subfolder of it, which | |
| 15 | @rem is what the repo gitignores), and run the script from the share. Run it with | |
| 16 | @rem the payload missing and it prints the exact shopping list, then stops. | |
| 17 | @rem | |
| 18 | @rem Usage - run it from your NORMAL account first; it stages a local copy and | |
| 19 | @rem hands back the runas line to elevate with: | |
| 20 | @rem | |
| 21 | @rem setup-windows-xp.bat --user your-normal-account | |
| 22 | @rem | |
| 23 | @rem Options: | |
| 24 | @rem --user NAME account that will log in over SSH (default: you) | |
| 25 | @rem --vendor DIR where the staged installers are (default: vendor-xp\) | |
| 26 | @rem --port N sshd port (default 22) | |
| 27 | @rem --stage-dir DIR local copy used for elevation (default C:\xp-setup) | |
| 28 | @rem --python27 also install Python 2.7.18 (last of the 2.x line) | |
| 29 | @rem --no-python skip Python entirely | |
| 30 | @rem --no-ssh skip the SSH server | |
| 31 | @rem --keep-forceguest leave the LSA network-logon policy alone (see below) | |
| 32 | @rem --help print this commentary and exit | |
| 33 | @rem | |
| 34 | @rem Everything is idempotent: an installed service, an existing Python and a | |
| 35 | @rem PATH entry that is already there are all detected and skipped, so re-running | |
| 36 | @rem after fixing one broken step is cheap. | |
| 37 | @rem --------------------------------------------------------------------------- | |
| 38 | ||
| 39 | set "SCRIPT=%~f0" | |
| 40 | set "SCRIPT_DIR=%~dp0" | |
| 41 | set "SCRIPT_NAME=%~nx0" | |
| 42 | set "TARGET_USER=%USERNAME%" | |
| 43 | set "VENDOR=" | |
| 44 | set "SSH_PORT=22" | |
| 45 | set "STAGE_DIR=C:\xp-setup" | |
| 46 | set "DO_SSH=1" | |
| 47 | set "DO_PY34=1" | |
| 48 | set "DO_PY27=0" | |
| 49 | set "DO_LSA=1" | |
| 50 | set "WARNINGS=0" | |
| 51 | set "PATH_CHANGED=0" | |
| 52 | ||
| 53 | :parse | |
| 54 | if "%~1"=="" goto parsed | |
| 55 | if /i "%~1"=="--user" (set "TARGET_USER=%~2"& shift& shift& goto parse) | |
| 56 | if /i "%~1"=="--vendor" (set "VENDOR=%~2"& shift& shift& goto parse) | |
| 57 | if /i "%~1"=="--port" (set "SSH_PORT=%~2"& shift& shift& goto parse) | |
| 58 | if /i "%~1"=="--stage-dir" (set "STAGE_DIR=%~2"& shift& shift& goto parse) | |
| 59 | if /i "%~1"=="--python27" (set "DO_PY27=1"& shift& goto parse) | |
| 60 | if /i "%~1"=="--no-python" (set "DO_PY34=0"& set "DO_PY27=0"& shift& goto parse) | |
| 61 | if /i "%~1"=="--no-ssh" (set "DO_SSH=0"& shift& goto parse) | |
| 62 | if /i "%~1"=="--keep-forceguest" (set "DO_LSA=0"& shift& goto parse) | |
| 63 | if /i "%~1"=="--help" goto usage | |
| 64 | if /i "%~1"=="-h" goto usage | |
| 65 | if /i "%~1"=="/?" goto usage | |
| 66 | echo Unknown option: %~1 | |
| 67 | echo Run "%SCRIPT_NAME% --help" for usage. | |
| 68 | exit /b 64 | |
| 69 | ||
| 70 | :usage | |
| 71 | @rem Print the commentary block at the top of this file, stopping at the first | |
| 72 | @rem line of real code. goto out of a for loop is legal and is the only way to | |
| 73 | @rem break one early. | |
| 74 | for /f "usebackq delims=" %%L in ("%SCRIPT%") do ( | |
| 75 | set "L=%%L" | |
| 76 | if "!L!"=="@rem" ( | |
| 77 | echo. | |
| 78 | ) else if "!L:~0,4!"=="@rem" ( | |
| 79 | echo !L:~5! | |
| 80 | ) else ( | |
| 81 | if not "!L:~0,1!"=="@" if /i not "!L:~0,8!"=="setlocal" if /i not "!L:~0,5!"=="title" goto usage_done | |
| 82 | ) | |
| 83 | ) | |
| 84 | :usage_done | |
| 85 | exit /b 0 | |
| 86 | ||
| 87 | :parsed | |
| 88 | if not defined VENDOR set "VENDOR=%SCRIPT_DIR%vendor-xp" | |
| 89 | ||
| 90 | @rem cmd.exe refuses a UNC working directory and this script is meant to run | |
| 91 | @rem straight off the \\VBOXSVR share, so pushd maps a temporary drive for it. | |
| 92 | pushd "%SCRIPT_DIR%" 2>nul | |
| 93 | if errorlevel 1 ( | |
| 94 | echo [setup-xp] FATAL: cannot enter "%SCRIPT_DIR%". | |
| 95 | exit /b 1 | |
| 96 | ) | |
| 97 | ||
| 98 | @rem --- Log: next to the script if the share is writable, otherwise TEMP ------- | |
| 99 | set "LOG=%SCRIPT_DIR%setup-windows-xp.log" | |
| 100 | echo.> "%LOG%" 2>nul | |
| 101 | if not exist "%LOG%" ( | |
| 102 | set "LOG=%TEMP%\setup-windows-xp.log" | |
| 103 | echo.> "!LOG!" 2>nul | |
| 104 | ) | |
| 105 | set "TMPOUT=%TEMP%\setup-xp-out.tmp" | |
| 106 | ||
| 107 | call :say "===========================================================" | |
| 108 | call :say " setup-windows-xp" | |
| 109 | call :say "===========================================================" | |
| 110 | call :say "" | |
| 111 | call :say "Script : %SCRIPT%" | |
| 112 | call :say "Payload : %VENDOR%" | |
| 113 | call :say "Log : %LOG%" | |
| 114 | call :say "Running as : %USERDOMAIN%\%USERNAME%" | |
| 115 | call :say "SSH user : %TARGET_USER%" | |
| 116 | call :say "SSH port : %SSH_PORT%" | |
| 117 | for /f "delims=" %%V in ('ver ^| findstr /r "."') do call :say "OS : %%V" | |
| 118 | call :say "Arch : %PROCESSOR_ARCHITECTURE%" | |
| 119 | call :say "" | |
| 120 | ||
| 121 | @rem --- Sanity: this should be XP (5.1) or XP x64 / 2003 (5.2) ----------------- | |
| 122 | ver | findstr /c:"5.1." >nul | |
| 123 | if errorlevel 1 ( | |
| 124 | ver | findstr /c:"5.2." >nul | |
| 125 | if errorlevel 1 ( | |
| 126 | call :warn "This does not look like Windows XP. The payload below is XP-specific," | |
| 127 | call :warn "and a modern OS has far better options - see setup-windows.bat." | |
| 128 | ) | |
| 129 | ) | |
| 130 | ||
| 131 | @rem --------------------------------------------------------------------------- | |
| 132 | @rem Administrator check | |
| 133 | @rem --------------------------------------------------------------------------- | |
| 134 | @rem XP has no UAC, so there is nothing to elevate into - the script simply has | |
| 135 | @rem to be started by an administrator. Reading the ACL of the SYSTEM hive is | |
| 136 | @rem the dependable test: "net session" needs the Server service, which is a coin | |
| 137 | @rem flip on a stripped-down VM, and a probe write leaks "Access is denied" onto | |
| 138 | @rem the console past 2>nul. If we are not admin we stage a local copy and | |
| 139 | @rem print the runas line: a VirtualBox share mounted under YOUR account is not | |
| 140 | @rem visible to the Administrator account, which is the whole reason for the copy. | |
| 141 | set "IS_ADMIN=0" | |
| 142 | cacls "%SystemRoot%\system32\config\system" >nul 2>&1 | |
| 143 | if not errorlevel 1 set "IS_ADMIN=1" | |
| 144 | if "%IS_ADMIN%"=="0" goto not_admin | |
| 145 | call :say "[ok] Running with administrator rights." | |
| 146 | call :say "" | |
| 147 | ||
| 148 | @rem --------------------------------------------------------------------------- | |
| 149 | @rem Locate the staged payload | |
| 150 | @rem --------------------------------------------------------------------------- | |
| 151 | call :find_payload "setupssh381-20040709.exe" SSH_EXE | |
| 152 | call :find_payload "python-3.4.4.msi" PY34_MSI | |
| 153 | call :find_payload "python-3.4.4.amd64.msi" PY34_MSI64 | |
| 154 | call :find_payload "python-2.7.18.msi" PY27_MSI | |
| 155 | call :find_payload "get-pip.py" GETPIP | |
| 156 | call :find_payload "authorized_keys" AUTHKEYS | |
| 157 | ||
| 158 | if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" if defined PY34_MSI64 set "PY34_MSI=%PY34_MSI64%" | |
| 159 | ||
| 160 | set "MISSING=0" | |
| 161 | if "%DO_SSH%"=="1" if not defined SSH_EXE set "MISSING=1" | |
| 162 | if "%DO_PY34%"=="1" if not defined PY34_MSI set "MISSING=1" | |
| 163 | if "%DO_PY27%"=="1" if not defined PY27_MSI set "MISSING=1" | |
| 164 | if "%MISSING%"=="1" goto payload_missing | |
| 165 | ||
| 166 | @rem --------------------------------------------------------------------------- | |
| 167 | @rem SSH server: OpenSSH for Windows 3.8.1p1-1 (the sshwindows build) | |
| 168 | @rem --------------------------------------------------------------------------- | |
| 169 | @rem Why this one: a single 2004-vintage NSIS installer that registers a real | |
| 170 | @rem service, authenticates against local Windows accounts, needs no network and | |
| 171 | @rem no runtime, and installs unattended. The price is its crypto - SSH-2, but | |
| 172 | @rem with 2004 algorithms - so a current OpenSSH client has to be told to | |
| 173 | @rem re-enable diffie-hellman-group1-sha1, ssh-rsa and a CBC cipher. The summary | |
| 174 | @rem at the end prints the exact client incantation and a ~/.ssh/config block. | |
| 175 | @rem If you would rather have modern crypto, the two other workable XP options | |
| 176 | @rem are Bitvise SSH Server 6.x (last XP-capable line, free for personal use) and | |
| 177 | @rem Cygwin 2.5.2 from the Cygwin Time Machine (OpenSSH 7.x plus rsync, served | |
| 178 | @rem over plain HTTP so XP can actually fetch it). Both want more hand-holding | |
| 179 | @rem than a batch file can give, which is why neither is the default here. | |
| 180 | if "%DO_SSH%"=="0" ( | |
| 181 | call :say "[skip] SSH server, because of --no-ssh." | |
| 182 | goto python | |
| 183 | ) | |
| 184 | ||
| 185 | set "OSSH_DIR=%ProgramFiles%\OpenSSH" | |
| 186 | set "OSSH_BIN=%OSSH_DIR%\bin" | |
| 187 | set "OSSH_ETC=%OSSH_DIR%\etc" | |
| 188 | ||
| 189 | call :say "--- OpenSSH for Windows -----------------------------------" | |
| 190 | if exist "%OSSH_BIN%\mkpasswd.exe" ( | |
| 191 | call :say "[skip] Already installed at %OSSH_DIR%." | |
| 192 | ) else ( | |
| 193 | call :say "Installing %SSH_EXE% ..." | |
| 194 | call :run "%SSH_EXE%" /S | |
| 195 | if not exist "%OSSH_BIN%\mkpasswd.exe" ( | |
| 196 | call :warn "The silent install produced nothing. Falling back to the interactive" | |
| 197 | call :warn "installer - click through it, keeping the default location." | |
| 198 | start /wait "OpenSSH" "%SSH_EXE%" | |
| 199 | ) | |
| 200 | ) | |
| 201 | if not exist "%OSSH_BIN%\mkpasswd.exe" call :die "OpenSSH did not install - no %OSSH_BIN%\mkpasswd.exe. See the log." 3 | |
| 202 | ||
| 203 | @rem --- Account database ------------------------------------------------------- | |
| 204 | @rem sshd here is a Cygwin program: it will not authenticate anyone who is not in | |
| 205 | @rem its own etc\passwd and etc\group. Both are rewritten (not appended to) on | |
| 206 | @rem every run, so repeated runs cannot pile up duplicate entries. | |
| 207 | call :say "Generating %OSSH_ETC%\group and %OSSH_ETC%\passwd ..." | |
| 208 | "%OSSH_BIN%\mkgroup.exe" -l > "%OSSH_ETC%\group" 2>>"%LOG%" | |
| 209 | "%OSSH_BIN%\mkpasswd.exe" -l -u "%TARGET_USER%" > "%OSSH_ETC%\passwd" 2>>"%LOG%" | |
| 210 | if /i not "%TARGET_USER%"=="%USERNAME%" "%OSSH_BIN%\mkpasswd.exe" -l -u "%USERNAME%" >> "%OSSH_ETC%\passwd" 2>>"%LOG%" | |
| 211 | findstr /b /i /c:"%TARGET_USER%:" "%OSSH_ETC%\passwd" >nul 2>&1 | |
| 212 | if errorlevel 1 ( | |
| 213 | call :say "No entry yet - retrying mkpasswd without -u, for every local account ..." | |
| 214 | "%OSSH_BIN%\mkpasswd.exe" -l > "%OSSH_ETC%\passwd" 2>>"%LOG%" | |
| 215 | ) | |
| 216 | findstr /b /i /c:"%TARGET_USER%:" "%OSSH_ETC%\passwd" >nul 2>&1 | |
| 217 | if errorlevel 1 ( | |
| 218 | call :warn "No passwd entry for '%TARGET_USER%' - is it a local account? Check" | |
| 219 | call :warn "'net user', then re-run with --user NAME. Without an entry, sshd" | |
| 220 | call :warn "will refuse the login no matter what the password is." | |
| 221 | ) | |
| 222 | type "%OSSH_ETC%\passwd" >> "%LOG%" 2>&1 | |
| 223 | ||
| 224 | @rem --- sshd_config ------------------------------------------------------------ | |
| 225 | @rem Rebuilt from a pristine copy on each run: strip the directives we own, then | |
| 226 | @rem append our own block. StrictModes has to go: it judges Windows ACLs by POSIX | |
| 227 | @rem rules and rejects an authorized_keys that is perfectly fine here. | |
| 228 | set "CFG=%OSSH_ETC%\sshd_config" | |
| 229 | if exist "%CFG%" ( | |
| 230 | if not exist "%CFG%.orig" copy /y "%CFG%" "%CFG%.orig" >nul 2>&1 | |
| 231 | findstr /v /b /i /c:"Port " /c:"#Port " /c:"PasswordAuthentication" /c:"#PasswordAuthentication" /c:"PubkeyAuthentication" /c:"#PubkeyAuthentication" /c:"StrictModes" /c:"#StrictModes" "%CFG%.orig" > "%CFG%.new" | |
| 232 | >>"%CFG%.new" echo. | |
| 233 | >>"%CFG%.new" echo # --- added by setup-windows-xp.bat --- | |
| 234 | >>"%CFG%.new" echo Port %SSH_PORT% | |
| 235 | >>"%CFG%.new" echo PasswordAuthentication yes | |
| 236 | >>"%CFG%.new" echo PubkeyAuthentication yes | |
| 237 | >>"%CFG%.new" echo StrictModes no | |
| 238 | move /y "%CFG%.new" "%CFG%" >nul | |
| 239 | call :say "[ok] sshd_config: port %SSH_PORT%, password + pubkey auth, StrictModes off." | |
| 240 | ) else ( | |
| 241 | call :warn "No sshd_config at %CFG% - keeping the installer defaults." | |
| 242 | ) | |
| 243 | ||
| 244 | @rem --- authorized_keys, if a public key was staged ---------------------------- | |
| 245 | @rem The home directory is read out of the passwd entry we just generated, in | |
| 246 | @rem Cygwin notation, and translated back to a Windows path rather than guessed. | |
| 247 | if defined AUTHKEYS ( | |
| 248 | set "CYGHOME=" | |
| 249 | for /f "tokens=6 delims=:" %%H in ('findstr /b /i /c:"%TARGET_USER%:" "%OSSH_ETC%\passwd"') do set "CYGHOME=%%H" | |
| 250 | if defined CYGHOME ( | |
| 251 | set "WINHOME=" | |
| 252 | if /i "!CYGHOME:~0,6!"=="/home/" set "WINHOME=%OSSH_DIR%\home\!CYGHOME:~6!" | |
| 253 | if /i "!CYGHOME:~0,10!"=="/cygdrive/" ( | |
| 254 | set "REST=!CYGHOME:~10!" | |
| 255 | set "WINHOME=!REST:~0,1!:!REST:~1!" | |
| 256 | ) | |
| 257 | if defined WINHOME ( | |
| 258 | set "WINHOME=!WINHOME:/=\!" | |
| 259 | call :say "Installing authorized_keys into !WINHOME!\.ssh ..." | |
| 260 | if not exist "!WINHOME!\.ssh" mkdir "!WINHOME!\.ssh" 2>nul | |
| 261 | copy /y "%AUTHKEYS%" "!WINHOME!\.ssh\authorized_keys" >nul 2>>"%LOG%" | |
| 262 | if exist "!WINHOME!\.ssh\authorized_keys" ( | |
| 263 | cacls "!WINHOME!\.ssh" /E /G "%TARGET_USER%":F >nul 2>>"%LOG%" | |
| 264 | call :say "[ok] Key installed - passwd says home is !CYGHOME!" | |
| 265 | ) else ( | |
| 266 | call :warn "Could not write !WINHOME!\.ssh\authorized_keys." | |
| 267 | ) | |
| 268 | ) else ( | |
| 269 | call :warn "Unrecognised home '!CYGHOME!' in passwd; install the key by hand." | |
| 270 | ) | |
| 271 | ) else ( | |
| 272 | call :warn "No passwd entry for %TARGET_USER%; skipped authorized_keys." | |
| 273 | ) | |
| 274 | ) else ( | |
| 275 | call :say "[skip] No authorized_keys staged - password auth only for now." | |
| 276 | ) | |
| 277 | ||
| 278 | @rem --- Firewall --------------------------------------------------------------- | |
| 279 | @rem SP2 and later only; on an unpatched XP the netsh firewall context does not | |
| 280 | @rem exist at all and the failure is harmless. | |
| 281 | call :say "Opening TCP %SSH_PORT% in the Windows Firewall ..." | |
| 282 | call :run netsh firewall add portopening protocol=TCP port=%SSH_PORT% name=OpenSSH mode=ENABLE scope=ALL | |
| 283 | if errorlevel 1 call :warn "netsh firewall failed - open TCP %SSH_PORT% by hand if you cannot connect." | |
| 284 | ||
| 285 | @rem --- Network logon policy --------------------------------------------------- | |
| 286 | @rem Two XP defaults break SSH password auth, both under the LSA key, because | |
| 287 | @rem sshd authenticates with a NETWORK logon: | |
| 288 | @rem forceguest=1 every network logon collapses to Guest. This is | |
| 289 | @rem the default in a workgroup, which a VM always is. | |
| 290 | @rem limitblankpassworduse=1 an account with an empty password cannot log on | |
| 291 | @rem over the network at all. | |
| 292 | @rem The first is switched to Classic here, because password auth cannot work | |
| 293 | @rem with it on; --keep-forceguest opts out. The second is only reported: give | |
| 294 | @rem the account a password rather than weakening that policy. | |
| 295 | if "%DO_LSA%"=="1" ( | |
| 296 | set "LSA=HKLM\SYSTEM\CurrentControlSet\Control\Lsa" | |
| 297 | set "FG=" | |
| 298 | for /f "tokens=2,*" %%A in ('reg query "!LSA!" /v forceguest 2^>nul ^| findstr /i /c:"forceguest"') do set "FG=%%B" | |
| 299 | if "!FG!"=="0x1" ( | |
| 300 | call :say "Setting LSA forceguest=0 - Classic logon - so SSH sees the real account." | |
| 301 | call :run reg add "!LSA!" /v forceguest /t REG_DWORD /d 0 /f | |
| 302 | call :say " to revert: reg add HKLM\SYSTEM\CurrentControlSet\Control\Lsa /v forceguest /t REG_DWORD /d 1 /f" | |
| 303 | ) else if "!FG!"=="0x0" ( | |
| 304 | call :say "[ok] LSA forceguest already off - Classic logon." | |
| 305 | ) else ( | |
| 306 | call :warn "Could not read LSA forceguest; if password auth logs you in as" | |
| 307 | call :warn "Guest, set it to 0 by hand." | |
| 308 | ) | |
| 309 | set "BLANK=" | |
| 310 | for /f "tokens=2,*" %%A in ('reg query "!LSA!" /v limitblankpassworduse 2^>nul ^| findstr /i /c:"limitblank"') do set "BLANK=%%B" | |
| 311 | if "!BLANK!"=="0x1" ( | |
| 312 | call :warn "limitblankpassworduse=1, so an account with a BLANK password cannot" | |
| 313 | call :warn "log in over SSH. Give %TARGET_USER% a password: net user %TARGET_USER% *" | |
| 314 | ) | |
| 315 | ) else ( | |
| 316 | call :say "[skip] LSA policy untouched, because of --keep-forceguest." | |
| 317 | ) | |
| 318 | ||
| 319 | @rem --- Service ---------------------------------------------------------------- | |
| 320 | call :say "Starting the OpenSSHd service ..." | |
| 321 | call :run sc config OpenSSHd start= auto | |
| 322 | net start OpenSSHd >>"%LOG%" 2>&1 | |
| 323 | sc query OpenSSHd | findstr /c:"RUNNING" >nul | |
| 324 | if errorlevel 1 ( | |
| 325 | call :warn "OpenSSHd is not running." | |
| 326 | call :run sc query OpenSSHd | |
| 327 | call :warn "Check %OSSH_DIR%\var\log\OpenSSHd.log for the reason." | |
| 328 | ) else ( | |
| 329 | call :say "[ok] OpenSSHd is running, and set to start automatically." | |
| 330 | ) | |
| 331 | call :say "" | |
| 332 | ||
| 333 | @rem --------------------------------------------------------------------------- | |
| 334 | @rem Python | |
| 335 | @rem --------------------------------------------------------------------------- | |
| 336 | @rem 3.4.4 (December 2015) is the last CPython that supports XP - 3.5 raised the | |
| 337 | @rem floor to Vista. 2.7.18 closes out the 2.x line and still runs here, worth | |
| 338 | @rem having if anything under test is 2.x. The 3.4 MSI has no "add to PATH" | |
| 339 | @rem feature (that arrived with the 3.5 installer), so PATH is edited directly in | |
| 340 | @rem the registry below. | |
| 341 | :python | |
| 342 | if "%DO_PY34%"=="0" if "%DO_PY27%"=="0" ( | |
| 343 | call :say "[skip] Python, because of --no-python." | |
| 344 | goto verify | |
| 345 | ) | |
| 346 | ||
| 347 | call :say "--- Python ------------------------------------------------" | |
| 348 | set "PY34_DIR=C:\Python34" | |
| 349 | set "PY27_DIR=C:\Python27" | |
| 350 | ||
| 351 | if "%DO_PY34%"=="1" ( | |
| 352 | if exist "%PY34_DIR%\python.exe" ( | |
| 353 | call :say "[skip] Python 3.4 already at %PY34_DIR%." | |
| 354 | ) else ( | |
| 355 | call :say "Installing %PY34_MSI% to %PY34_DIR% ..." | |
| 356 | call :run msiexec /i "%PY34_MSI%" /qn /norestart ALLUSERS=1 TARGETDIR="%PY34_DIR%" /l*v "%TEMP%\python34-msi.log" | |
| 357 | if not exist "%PY34_DIR%\python.exe" call :warn "Python 3.4 install failed; see %TEMP%\python34-msi.log." | |
| 358 | ) | |
| 359 | if exist "%PY34_DIR%\python.exe" ( | |
| 360 | call :add_syspath "%PY34_DIR%" | |
| 361 | call :add_syspath "%PY34_DIR%\Scripts" | |
| 362 | call :bootstrap_pip "%PY34_DIR%" | |
| 363 | ) | |
| 364 | ) | |
| 365 | ||
| 366 | if "%DO_PY27%"=="1" ( | |
| 367 | if exist "%PY27_DIR%\python.exe" ( | |
| 368 | call :say "[skip] Python 2.7 already at %PY27_DIR%." | |
| 369 | ) else ( | |
| 370 | call :say "Installing %PY27_MSI% to %PY27_DIR% ..." | |
| 371 | call :run msiexec /i "%PY27_MSI%" /qn /norestart ALLUSERS=1 TARGETDIR="%PY27_DIR%" /l*v "%TEMP%\python27-msi.log" | |
| 372 | if not exist "%PY27_DIR%\python.exe" call :warn "Python 2.7 install failed; see %TEMP%\python27-msi.log." | |
| 373 | ) | |
| 374 | ) | |
| 375 | call :say "" | |
| 376 | ||
| 377 | @rem A service reads its environment when it starts, so sshd is still holding the | |
| 378 | @rem pre-Python PATH. Bounce it, or the first SSH session cannot find python.exe | |
| 379 | @rem and it looks like the PATH edit never took. | |
| 380 | if "%DO_SSH%"=="1" if "%PATH_CHANGED%"=="1" ( | |
| 381 | call :say "Restarting OpenSSHd so SSH sessions inherit the new PATH ..." | |
| 382 | net stop OpenSSHd >>"%LOG%" 2>&1 | |
| 383 | net start OpenSSHd >>"%LOG%" 2>&1 | |
| 384 | ) | |
| 385 | ||
| 386 | @rem --------------------------------------------------------------------------- | |
| 387 | @rem Verify | |
| 388 | @rem --------------------------------------------------------------------------- | |
| 389 | :verify | |
| 390 | call :say "--- Verification ------------------------------------------" | |
| 391 | if "%DO_SSH%"=="1" ( | |
| 392 | call :run sc query OpenSSHd | |
| 393 | call :say "Sockets listening on port %SSH_PORT%:" | |
| 394 | netstat -an | findstr /c:":%SSH_PORT% " | findstr /c:"LISTENING" | |
| 395 | if errorlevel 1 call :warn "Nothing is listening on port %SSH_PORT%." | |
| 396 | netstat -an | findstr /c:":%SSH_PORT% " | findstr /c:"LISTENING" >> "%LOG%" | |
| 397 | ) | |
| 398 | if exist "%PY34_DIR%\python.exe" call :run "%PY34_DIR%\python.exe" -V | |
| 399 | if exist "%PY34_DIR%\python.exe" call :run "%PY34_DIR%\python.exe" -m pip --version | |
| 400 | if exist "%PY27_DIR%\python.exe" call :run "%PY27_DIR%\python.exe" -V | |
| 401 | call :say "" | |
| 402 | ||
| 403 | set "VMIP=" | |
| 404 | for /f "tokens=2 delims=:" %%I in ('ipconfig ^| findstr /c:"IP Address" /c:"IPv4 Address"') do if not defined VMIP set "VMIP=%%I" | |
| 405 | if defined VMIP set "VMIP=%VMIP: =%" | |
| 406 | if not defined VMIP set "VMIP=vm-ip" | |
| 407 | ||
| 408 | call :say "===========================================================" | |
| 409 | call :say " Done. Warnings: %WARNINGS%" | |
| 410 | call :say " Log: %LOG%" | |
| 411 | call :say "===========================================================" | |
| 412 | call :say "" | |
| 413 | if "%DO_SSH%"=="1" ( | |
| 414 | call :say "Connecting from a modern client means re-enabling the 2004" | |
| 415 | call :say "algorithms, or you get 'no matching key exchange method found':" | |
| 416 | call :say "" | |
| 417 | call :say " ssh -p %SSH_PORT% -o KexAlgorithms=+diffie-hellman-group1-sha1,diffie-hellman-group14-sha1 -o HostKeyAlgorithms=+ssh-rsa -o PubkeyAcceptedAlgorithms=+ssh-rsa -c aes128-cbc %TARGET_USER%@%VMIP%" | |
| 418 | call :say "" | |
| 419 | call :say "Or drop this into ~/.ssh/config on the host and just 'ssh xpvm':" | |
| 420 | call :say "" | |
| 421 | call :say " Host xpvm" | |
| 422 | call :say " HostName %VMIP%" | |
| 423 | call :say " Port %SSH_PORT%" | |
| 424 | call :say " User %TARGET_USER%" | |
| 425 | call :say " KexAlgorithms +diffie-hellman-group1-sha1,diffie-hellman-group14-sha1" | |
| 426 | call :say " HostKeyAlgorithms +ssh-rsa" | |
| 427 | call :say " PubkeyAcceptedAlgorithms +ssh-rsa" | |
| 428 | call :say " Ciphers +aes128-cbc" | |
| 429 | call :say " MACs +hmac-sha1" | |
| 430 | call :say "" | |
| 431 | call :say "The account needs a real password - a blank one cannot log on over" | |
| 432 | call :say "the network. A NAT-only VM needs a host port forwarded to %SSH_PORT%." | |
| 433 | ) | |
| 434 | if "%PATH_CHANGED%"=="1" ( | |
| 435 | call :say "" | |
| 436 | call :say "PATH was changed in the registry: log off and back on, or reboot," | |
| 437 | call :say "before an interactive console sees python." | |
| 438 | ) | |
| 439 | popd | |
| 440 | endlocal | |
| 441 | exit /b 0 | |
| 442 | ||
| 443 | @rem =========================================================================== | |
| 444 | @rem Subroutines | |
| 445 | @rem =========================================================================== | |
| 446 | ||
| 447 | @rem Console and log in one call. Messages are passed as ONE quoted argument, | |
| 448 | @rem which constrains what can go in them - all of these were found the hard way: | |
| 449 | @rem no ! ......... delayed expansion eats it | |
| 450 | @rem no < or > .... echo re-parses the expanded value and redirects. A caret | |
| 451 | @rem does NOT help: by then the quotes are gone. | |
| 452 | @rem no ( or ) .... inside a NESTED if-block these close the block early, even | |
| 453 | @rem quoted, and even carets do not save them. | |
| 454 | @rem Use commas and dashes instead. A line that genuinely needs quotes or one of | |
| 455 | @rem these characters is echoed twice inline instead, once to each destination. | |
| 456 | :say | |
| 457 | if "%~1"=="" (echo.& >>"%LOG%" echo.) else (echo %~1& >>"%LOG%" echo %~1) | |
| 458 | goto :eof | |
| 459 | ||
| 460 | :warn | |
| 461 | set /a WARNINGS+=1 | |
| 462 | echo [warn] %~1 | |
| 463 | >>"%LOG%" echo [warn] %~1 | |
| 464 | goto :eof | |
| 465 | ||
| 466 | :die | |
| 467 | call :say "" | |
| 468 | call :say "[FATAL] %~1" | |
| 469 | popd | |
| 470 | endlocal | |
| 471 | exit /b %~2 | |
| 472 | ||
| 473 | @rem Run a command, showing its output and copying it to the log. Redirection | |
| 474 | @rem cannot be passed through %*, so anything needing a > of its own runs inline | |
| 475 | @rem instead of through here. | |
| 476 | :run | |
| 477 | echo run: %* | |
| 478 | >>"%LOG%" echo run: %* | |
| 479 | %* > "%TMPOUT%" 2>&1 | |
| 480 | set "RC=%ERRORLEVEL%" | |
| 481 | if exist "%TMPOUT%" ( | |
| 482 | type "%TMPOUT%" | |
| 483 | type "%TMPOUT%" >> "%LOG%" | |
| 484 | del "%TMPOUT%" >nul 2>&1 | |
| 485 | ) | |
| 486 | exit /b %RC% | |
| 487 | ||
| 488 | @rem :find_payload <filename> <varname> - vendor dir first, then beside the script | |
| 489 | :find_payload | |
| 490 | set "%~2=" | |
| 491 | if exist "%VENDOR%\%~1" (set "%~2=%VENDOR%\%~1"& goto :eof) | |
| 492 | if exist "%SCRIPT_DIR%%~1" (set "%~2=%SCRIPT_DIR%%~1"& goto :eof) | |
| 493 | goto :eof | |
| 494 | ||
| 495 | @rem :add_syspath <dir> - append to the machine PATH, once. | |
| 496 | @rem setx.exe is a Support Tools extra on XP, so the registry is edited directly. | |
| 497 | @rem Nothing broadcasts WM_SETTINGCHANGE afterwards, which is why the summary | |
| 498 | @rem asks for a logoff: running shells and explorer keep the old value. | |
| 499 | :add_syspath | |
| 500 | set "ENVKEY=HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment" | |
| 501 | set "SYSPATH=" | |
| 502 | set "PATHTYPE=REG_EXPAND_SZ" | |
| 503 | for /f "tokens=2,*" %%A in ('reg query "%ENVKEY%" /v Path 2^>nul ^| findstr /i /r /c:"Path.*REG_"') do ( | |
| 504 | set "PATHTYPE=%%A" | |
| 505 | set "SYSPATH=%%B" | |
| 506 | ) | |
| 507 | if not defined SYSPATH ( | |
| 508 | call :warn "Could not read the machine PATH; add %~1 to it by hand." | |
| 509 | goto :eof | |
| 510 | ) | |
| 511 | echo ;%SYSPATH%;| findstr /i /c:";%~1;" >nul | |
| 512 | if not errorlevel 1 ( | |
| 513 | call :say "[ok] %~1 already in the machine PATH." | |
| 514 | goto :eof | |
| 515 | ) | |
| 516 | >>"%LOG%" echo [path] previous value: %SYSPATH% | |
| 517 | reg add "%ENVKEY%" /v Path /t %PATHTYPE% /d "%SYSPATH%;%~1" /f >>"%LOG%" 2>&1 | |
| 518 | if errorlevel 1 ( | |
| 519 | call :warn "Failed to append %~1 to the machine PATH." | |
| 520 | ) else ( | |
| 521 | call :say "[ok] Added %~1 to the machine PATH." | |
| 522 | set "PATH=%PATH%;%~1" | |
| 523 | set "PATH_CHANGED=1" | |
| 524 | ) | |
| 525 | goto :eof | |
| 526 | ||
| 527 | @rem :bootstrap_pip <python-dir> | |
| 528 | @rem ensurepip is offline and always works, but 3.4.4 carries pip 6, which | |
| 529 | @rem today's PyPI turns away. A staged get-pip.py - the bootstrap.pypa.io/pip/3.4 | |
| 530 | @rem one - carries pip 19.1.1, the last release supporting 3.4, so prefer it. | |
| 531 | @rem Either way, installing FROM PyPI later is its own problem: expect to stage | |
| 532 | @rem wheels on the host and use pip install --no-index --find-links. | |
| 533 | :bootstrap_pip | |
| 534 | if exist "%~1\Scripts\pip.exe" ( | |
| 535 | call :say "[skip] pip already present in %~1." | |
| 536 | goto :eof | |
| 537 | ) | |
| 538 | if defined GETPIP ( | |
| 539 | call :say "Bootstrapping pip from %GETPIP% ..." | |
| 540 | call :run "%~1\python.exe" "%GETPIP%" | |
| 541 | ) else ( | |
| 542 | call :say "Bootstrapping pip with ensurepip - no get-pip.py staged ..." | |
| 543 | call :run "%~1\python.exe" -m ensurepip --default-pip | |
| 544 | ) | |
| 545 | if not exist "%~1\Scripts\pip.exe" call :warn "pip bootstrap failed for %~1." | |
| 546 | goto :eof | |
| 547 | ||
| 548 | @rem =========================================================================== | |
| 549 | @rem Exits | |
| 550 | @rem =========================================================================== | |
| 551 | ||
| 552 | :not_admin | |
| 553 | @rem Stage the script and its payload somewhere the Administrator account can | |
| 554 | @rem actually reach - a share mounted under your account is not it - and hand | |
| 555 | @rem back the command line to run. | |
| 556 | call :say "[--] Not running as an administrator." | |
| 557 | call :say "" | |
| 558 | if /i "%SCRIPT_DIR%"=="%STAGE_DIR%\" goto not_admin_hint | |
| 559 | call :say "Staging a local copy in %STAGE_DIR% ..." | |
| 560 | if not exist "%STAGE_DIR%" mkdir "%STAGE_DIR%" 2>nul | |
| 561 | if not exist "%STAGE_DIR%" ( | |
| 562 | set "STAGE_DIR=%TEMP%\xp-setup" | |
| 563 | if not exist "!STAGE_DIR!" mkdir "!STAGE_DIR!" 2>nul | |
| 564 | ) | |
| 565 | copy /y "%SCRIPT%" "%STAGE_DIR%\" >nul 2>&1 | |
| 566 | if exist "%VENDOR%" ( | |
| 567 | if not exist "%STAGE_DIR%\vendor-xp" mkdir "%STAGE_DIR%\vendor-xp" 2>nul | |
| 568 | xcopy "%VENDOR%\*.*" "%STAGE_DIR%\vendor-xp\" /y /i >nul 2>&1 | |
| 569 | ) | |
| 570 | if exist "%SCRIPT_DIR%authorized_keys" copy /y "%SCRIPT_DIR%authorized_keys" "%STAGE_DIR%\" >nul 2>&1 | |
| 571 | call :say "[ok] Copied to %STAGE_DIR%." | |
| 572 | ||
| 573 | :not_admin_hint | |
| 574 | call :say "" | |
| 575 | call :say "Now run it as the admin account. From this same window:" | |
| 576 | call :say "" | |
| 577 | echo runas /user:%COMPUTERNAME%\Administrator "cmd /k %STAGE_DIR%\%SCRIPT_NAME% --user %USERNAME%" | |
| 578 | >>"%LOG%" echo runas /user:%COMPUTERNAME%\Administrator "cmd /k %STAGE_DIR%\%SCRIPT_NAME% --user %USERNAME%" | |
| 579 | call :say "" | |
| 580 | call :say "Substitute your own admin account name; runas needs the Secondary" | |
| 581 | call :say "Logon service. Right-clicking the .bat and picking 'Run as...' works" | |
| 582 | call :say "just as well." | |
| 583 | call :say "" | |
| 584 | call :say "The --user %USERNAME% part matters: the elevated run has to be told" | |
| 585 | call :say "which account you will actually be logging in as over SSH." | |
| 586 | popd | |
| 587 | endlocal | |
| 588 | exit /b 2 | |
| 589 | ||
| 590 | :payload_missing | |
| 591 | call :say "" | |
| 592 | call :say "[--] The installers are not staged yet." | |
| 593 | call :say "" | |
| 594 | call :say "XP cannot fetch them itself - its TLS stops at 1.0 and every one of" | |
| 595 | call :say "these hosts requires TLS 1.2. Download them on the HOST, drop them in" | |
| 596 | call :say " %VENDOR%" | |
| 597 | call :say "and run this again." | |
| 598 | call :say "" | |
| 599 | call :say "Required:" | |
| 600 | if not defined SSH_EXE ( | |
| 601 | call :say " setupssh381-20040709.exe OpenSSH for Windows 3.8.1p1-1" | |
| 602 | call :say " https://sourceforge.net/projects/sshwindows/files/" | |
| 603 | call :say " Binaries, then Release 3.8.1p1-1" | |
| 604 | ) | |
| 605 | if not defined PY34_MSI call :say " python-3.4.4.msi https://www.python.org/ftp/python/3.4.4/python-3.4.4.msi" | |
| 606 | if "%DO_PY27%"=="1" if not defined PY27_MSI call :say " python-2.7.18.msi https://www.python.org/ftp/python/2.7.18/python-2.7.18.msi" | |
| 607 | call :say "" | |
| 608 | call :say "Optional:" | |
| 609 | call :say " get-pip.py https://bootstrap.pypa.io/pip/3.4/get-pip.py" | |
| 610 | call :say " pip 19.1.1, the last release for 3.4" | |
| 611 | call :say " authorized_keys your host public key, for key auth" | |
| 612 | call :say "" | |
| 613 | call :say "On the host, in this folder:" | |
| 614 | call :say "" | |
| 615 | echo powershell -NoProfile -Command "New-Item -ItemType Directory -Force vendor-xp; iwr https://www.python.org/ftp/python/3.4.4/python-3.4.4.msi -OutFile vendor-xp\python-3.4.4.msi; iwr https://bootstrap.pypa.io/pip/3.4/get-pip.py -OutFile vendor-xp\get-pip.py" | |
| 616 | call :say "" | |
| 617 | call :say "SourceForge hands out a browser interstitial rather than the file, so" | |
| 618 | call :say "grab setupssh381-20040709.exe by hand." | |
| 619 | popd | |
| 620 | endlocal | |
| 621 | exit /b 4 |