]> vilimpoc.org git repositories - dotfiles/blame - setup-windows-xp.bat
dotfiles: Windows XP VM provisioning
[dotfiles] / setup-windows-xp.bat
CommitLineData
fc15a36e
MV
1@echo off
2setlocal enableextensions enabledelayedexpansion
3title 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
39set "SCRIPT=%~f0"
40set "SCRIPT_DIR=%~dp0"
41set "SCRIPT_NAME=%~nx0"
42set "TARGET_USER=%USERNAME%"
43set "VENDOR="
44set "SSH_PORT=22"
45set "STAGE_DIR=C:\xp-setup"
46set "DO_SSH=1"
47set "DO_PY34=1"
48set "DO_PY27=0"
49set "DO_LSA=1"
50set "WARNINGS=0"
51set "PATH_CHANGED=0"
52
53:parse
54if "%~1"=="" goto parsed
55if /i "%~1"=="--user" (set "TARGET_USER=%~2"& shift& shift& goto parse)
56if /i "%~1"=="--vendor" (set "VENDOR=%~2"& shift& shift& goto parse)
57if /i "%~1"=="--port" (set "SSH_PORT=%~2"& shift& shift& goto parse)
58if /i "%~1"=="--stage-dir" (set "STAGE_DIR=%~2"& shift& shift& goto parse)
59if /i "%~1"=="--python27" (set "DO_PY27=1"& shift& goto parse)
60if /i "%~1"=="--no-python" (set "DO_PY34=0"& set "DO_PY27=0"& shift& goto parse)
61if /i "%~1"=="--no-ssh" (set "DO_SSH=0"& shift& goto parse)
62if /i "%~1"=="--keep-forceguest" (set "DO_LSA=0"& shift& goto parse)
63if /i "%~1"=="--help" goto usage
64if /i "%~1"=="-h" goto usage
65if /i "%~1"=="/?" goto usage
66echo Unknown option: %~1
67echo Run "%SCRIPT_NAME% --help" for usage.
68exit /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.
74for /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
85exit /b 0
86
87:parsed
88if 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.
92pushd "%SCRIPT_DIR%" 2>nul
93if 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 -------
99set "LOG=%SCRIPT_DIR%setup-windows-xp.log"
100echo.> "%LOG%" 2>nul
101if not exist "%LOG%" (
102 set "LOG=%TEMP%\setup-windows-xp.log"
103 echo.> "!LOG!" 2>nul
104)
105set "TMPOUT=%TEMP%\setup-xp-out.tmp"
106
107call :say "==========================================================="
108call :say " setup-windows-xp"
109call :say "==========================================================="
110call :say ""
111call :say "Script : %SCRIPT%"
112call :say "Payload : %VENDOR%"
113call :say "Log : %LOG%"
114call :say "Running as : %USERDOMAIN%\%USERNAME%"
115call :say "SSH user : %TARGET_USER%"
116call :say "SSH port : %SSH_PORT%"
117for /f "delims=" %%V in ('ver ^| findstr /r "."') do call :say "OS : %%V"
118call :say "Arch : %PROCESSOR_ARCHITECTURE%"
119call :say ""
120
121@rem --- Sanity: this should be XP (5.1) or XP x64 / 2003 (5.2) -----------------
122ver | findstr /c:"5.1." >nul
123if 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.
141set "IS_ADMIN=0"
142cacls "%SystemRoot%\system32\config\system" >nul 2>&1
143if not errorlevel 1 set "IS_ADMIN=1"
144if "%IS_ADMIN%"=="0" goto not_admin
145call :say "[ok] Running with administrator rights."
146call :say ""
147
148@rem ---------------------------------------------------------------------------
149@rem Locate the staged payload
150@rem ---------------------------------------------------------------------------
151call :find_payload "setupssh381-20040709.exe" SSH_EXE
152call :find_payload "python-3.4.4.msi" PY34_MSI
153call :find_payload "python-3.4.4.amd64.msi" PY34_MSI64
154call :find_payload "python-2.7.18.msi" PY27_MSI
155call :find_payload "get-pip.py" GETPIP
156call :find_payload "authorized_keys" AUTHKEYS
157
158if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" if defined PY34_MSI64 set "PY34_MSI=%PY34_MSI64%"
159
160set "MISSING=0"
161if "%DO_SSH%"=="1" if not defined SSH_EXE set "MISSING=1"
162if "%DO_PY34%"=="1" if not defined PY34_MSI set "MISSING=1"
163if "%DO_PY27%"=="1" if not defined PY27_MSI set "MISSING=1"
164if "%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.
180if "%DO_SSH%"=="0" (
181 call :say "[skip] SSH server, because of --no-ssh."
182 goto python
183)
184
185set "OSSH_DIR=%ProgramFiles%\OpenSSH"
186set "OSSH_BIN=%OSSH_DIR%\bin"
187set "OSSH_ETC=%OSSH_DIR%\etc"
188
189call :say "--- OpenSSH for Windows -----------------------------------"
190if 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)
201if 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.
207call :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%"
210if /i not "%TARGET_USER%"=="%USERNAME%" "%OSSH_BIN%\mkpasswd.exe" -l -u "%USERNAME%" >> "%OSSH_ETC%\passwd" 2>>"%LOG%"
211findstr /b /i /c:"%TARGET_USER%:" "%OSSH_ETC%\passwd" >nul 2>&1
212if 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)
216findstr /b /i /c:"%TARGET_USER%:" "%OSSH_ETC%\passwd" >nul 2>&1
217if 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)
222type "%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.
228set "CFG=%OSSH_ETC%\sshd_config"
229if 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.
247if 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.
281call :say "Opening TCP %SSH_PORT% in the Windows Firewall ..."
282call :run netsh firewall add portopening protocol=TCP port=%SSH_PORT% name=OpenSSH mode=ENABLE scope=ALL
283if 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.
295if "%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 ----------------------------------------------------------------
320call :say "Starting the OpenSSHd service ..."
321call :run sc config OpenSSHd start= auto
322net start OpenSSHd >>"%LOG%" 2>&1
323sc query OpenSSHd | findstr /c:"RUNNING" >nul
324if 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)
331call :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
342if "%DO_PY34%"=="0" if "%DO_PY27%"=="0" (
343 call :say "[skip] Python, because of --no-python."
344 goto verify
345)
346
347call :say "--- Python ------------------------------------------------"
348set "PY34_DIR=C:\Python34"
349set "PY27_DIR=C:\Python27"
350
351if "%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
366if "%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)
375call :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.
380if "%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
390call :say "--- Verification ------------------------------------------"
391if "%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)
398if exist "%PY34_DIR%\python.exe" call :run "%PY34_DIR%\python.exe" -V
399if exist "%PY34_DIR%\python.exe" call :run "%PY34_DIR%\python.exe" -m pip --version
400if exist "%PY27_DIR%\python.exe" call :run "%PY27_DIR%\python.exe" -V
401call :say ""
402
403set "VMIP="
404for /f "tokens=2 delims=:" %%I in ('ipconfig ^| findstr /c:"IP Address" /c:"IPv4 Address"') do if not defined VMIP set "VMIP=%%I"
405if defined VMIP set "VMIP=%VMIP: =%"
406if not defined VMIP set "VMIP=vm-ip"
407
408call :say "==========================================================="
409call :say " Done. Warnings: %WARNINGS%"
410call :say " Log: %LOG%"
411call :say "==========================================================="
412call :say ""
413if "%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)
434if "%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)
439popd
440endlocal
441exit /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
457if "%~1"=="" (echo.& >>"%LOG%" echo.) else (echo %~1& >>"%LOG%" echo %~1)
458goto :eof
459
460:warn
461set /a WARNINGS+=1
462echo [warn] %~1
463>>"%LOG%" echo [warn] %~1
464goto :eof
465
466:die
467call :say ""
468call :say "[FATAL] %~1"
469popd
470endlocal
471exit /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
477echo run: %*
478>>"%LOG%" echo run: %*
479%* > "%TMPOUT%" 2>&1
480set "RC=%ERRORLEVEL%"
481if exist "%TMPOUT%" (
482 type "%TMPOUT%"
483 type "%TMPOUT%" >> "%LOG%"
484 del "%TMPOUT%" >nul 2>&1
485)
486exit /b %RC%
487
488@rem :find_payload <filename> <varname> - vendor dir first, then beside the script
489:find_payload
490set "%~2="
491if exist "%VENDOR%\%~1" (set "%~2=%VENDOR%\%~1"& goto :eof)
492if exist "%SCRIPT_DIR%%~1" (set "%~2=%SCRIPT_DIR%%~1"& goto :eof)
493goto :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
500set "ENVKEY=HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment"
501set "SYSPATH="
502set "PATHTYPE=REG_EXPAND_SZ"
503for /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)
507if not defined SYSPATH (
508 call :warn "Could not read the machine PATH; add %~1 to it by hand."
509 goto :eof
510)
511echo ;%SYSPATH%;| findstr /i /c:";%~1;" >nul
512if not errorlevel 1 (
513 call :say "[ok] %~1 already in the machine PATH."
514 goto :eof
515)
516>>"%LOG%" echo [path] previous value: %SYSPATH%
517reg add "%ENVKEY%" /v Path /t %PATHTYPE% /d "%SYSPATH%;%~1" /f >>"%LOG%" 2>&1
518if 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)
525goto :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
534if exist "%~1\Scripts\pip.exe" (
535 call :say "[skip] pip already present in %~1."
536 goto :eof
537)
538if 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)
545if not exist "%~1\Scripts\pip.exe" call :warn "pip bootstrap failed for %~1."
546goto :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.
556call :say "[--] Not running as an administrator."
557call :say ""
558if /i "%SCRIPT_DIR%"=="%STAGE_DIR%\" goto not_admin_hint
559call :say "Staging a local copy in %STAGE_DIR% ..."
560if not exist "%STAGE_DIR%" mkdir "%STAGE_DIR%" 2>nul
561if not exist "%STAGE_DIR%" (
562 set "STAGE_DIR=%TEMP%\xp-setup"
563 if not exist "!STAGE_DIR!" mkdir "!STAGE_DIR!" 2>nul
564)
565copy /y "%SCRIPT%" "%STAGE_DIR%\" >nul 2>&1
566if 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)
570if exist "%SCRIPT_DIR%authorized_keys" copy /y "%SCRIPT_DIR%authorized_keys" "%STAGE_DIR%\" >nul 2>&1
571call :say "[ok] Copied to %STAGE_DIR%."
572
573:not_admin_hint
574call :say ""
575call :say "Now run it as the admin account. From this same window:"
576call :say ""
577echo 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%"
579call :say ""
580call :say "Substitute your own admin account name; runas needs the Secondary"
581call :say "Logon service. Right-clicking the .bat and picking 'Run as...' works"
582call :say "just as well."
583call :say ""
584call :say "The --user %USERNAME% part matters: the elevated run has to be told"
585call :say "which account you will actually be logging in as over SSH."
586popd
587endlocal
588exit /b 2
589
590:payload_missing
591call :say ""
592call :say "[--] The installers are not staged yet."
593call :say ""
594call :say "XP cannot fetch them itself - its TLS stops at 1.0 and every one of"
595call :say "these hosts requires TLS 1.2. Download them on the HOST, drop them in"
596call :say " %VENDOR%"
597call :say "and run this again."
598call :say ""
599call :say "Required:"
600if 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)
605if 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"
606if "%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"
607call :say ""
608call :say "Optional:"
609call :say " get-pip.py https://bootstrap.pypa.io/pip/3.4/get-pip.py"
610call :say " pip 19.1.1, the last release for 3.4"
611call :say " authorized_keys your host public key, for key auth"
612call :say ""
613call :say "On the host, in this folder:"
614call :say ""
615echo 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"
616call :say ""
617call :say "SourceForge hands out a browser interstitial rather than the file, so"
618call :say "grab setupssh381-20040709.exe by hand."
619popd
620endlocal
621exit /b 4