]> vilimpoc.org git repositories - dotfiles/blob - setup-windows-xp.bat
dotfiles: Windows XP VM provisioning
[dotfiles] / setup-windows-xp.bat
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