Monday, February 22, 2010

Auto elevating batchfiles

Having moved to Windows7 I found myself confronted with a small application no longer working: adding routes for VPN connection. Application wasn't mine, so recompiling not an option. What to do? Well, there used to be something called Batch files in Windows. They're still there.... As is PowerShell, but I'm not using that right now.

So, Batch file it is. Got some code from a colleague to get me started, built on it, and ran it from another application I use: mRemote. Alas, no dice. The Batch file needs elevated permissions to start.

There are several ways of doing that, depending on the context you're in, but it would be nice if it would elevate itself. So here's an example of how to do just that. The batch file checks if it has elevated rights, and when not, creates VBScript on the fly and lauches it. The VBScript calls the very same Batch file again with elevated permissions (you still get the prompt for permissions though).

Now, when I forget to run mRemote in elevated mode, the batch file will elevate itself.

The major problem I was/am facing doing this is the fact that it is rather difficult to pass parameters with spaces in it. Fact: I didn't manage. So I resolved to separate paramters with hashes (#) instead.

Here's the code to do all this (I hope nothing got stripped out when posting this).
The interesting bit from elevation point of view are:
  • :testUAC
  • :doElevation

@echo off
SetLocal enabledelayedexpansion

rem --- Usage ---
rem parameters must be separated by hash signs
rem e.g.: My VPN#My Name#My Password#1
rem Parameters:
rem VPNNAME e.g. My VPN
rem VPNUSERNAME e.g. erp234
rem VPNPASSWORD e.g. xyz123
rem NOPAUSE 0 / 1 ==> whether or not to pause at the end of the script

rem --- Push directory ---
rem In case we are called from withing other hosts
cd /D "%~dp0"

rem --- Deal with parameters ---
for /f "tokens=1-5 delims=#" %%a in ("%*") do (
set VPNNAME=%%a
set VPNUSERNAME=%%b
set VPNPASSWORD=%%c
set NOPAUSE=%%d
)
:testUAC
rem --- Test uac ---
rem We assume %windir% environment variable exists
rem Redirect error messages to NUL
rem THIS WILL FAIL IF DIRECTORY ALREADY EXISTS.
rem IS THERE A BETTER WAY OF CHECKING IF WE ARE ALREADY ELEVATED
mkdir %windir%\uactest 2>nul
if !ERRORLEVEL! == 1 (
goto :doElevation
) else (
rd %windir%\uactest /s /q
)

rem --- START MAIN section ---

:skipElevation

rem --- Stop the VPN ---
rem echo * Bestaande verbinding verbreken (indien aanwezig) : %VPNNAME%
rem rasdial "%VPNNAME%" /disconnect
rem echo.

:startVPN
rem --- Start the VPN ---
echo * Starting VPN : %VPNNAME%
rasdial "%VPNNAME%" %VPNUSERNAME% %VPNPASSWORD%
echo.
if !ERRORLEVEL! == 1 GOTO couldNotStartVPN

rem Reset VPNIP
set VPNIP=

rem Reset ERRORLEVEL
CD > NUL

echo * Identifying received IP from started connection....

ver | find "2003" > nul
if !ERRORLEVEL! == 0 goto getVPNIP_2003

ver | find "XP" > nul
if !ERRORLEVEL! == 0 goto getVPNIP_XP

ver | find "2000" > nul
if !ERRORLEVEL! == 0 goto getVPNIP_2000

ver | find "NT" > nul
if !ERRORLEVEL! == 0 goto getVPNIP_NT

rem --- Speed ---
rem Reg is way faster than SystemInfo
if exist %SystemRoot%\system32\reg.exe goto doReg
if exist %SystemRoot%\system32\systeminfo.exe goto doSysInfo
goto warnOSThenExit

:doReg
set vers=
for /f "tokens=3*" %%i IN ('reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion" /v ProductName ^| Find "ProductName"') DO set vers=%%i %%j
echo %vers%
goto continueOS

:doSysInfo
set vers=
for /f "delims=: tokens=2" %%i IN ('systeminfo ^| find "OS Name"') DO set vers=%%i

goto continueOS

:continueOS
echo %vers% | find "Windows 7" > nul
if !ERRORLEVEL! == 0 goto getVPNIP_7

echo %vers% | find "Windows Server 2008" > nul
if !ERRORLEVEL! == 0 goto getVPNIP_2008

echo %vers% | find "Windows Vista" > nul
if !ERRORLEVEL! == 0 goto getVPNIP_VISTA

goto warnOSThenExit

rem Reset ERRORLEVEL
CD > NUL

:getVPNIP_nt
goto warnOSThenExit

:getVPNIP_2000
goto warnOSThenExit

:getVPNIP_XP
rem Windows XP
for /f "skip=3 tokens=3" %%a in ('netsh diag show ip Miniport*') do (
set VPNIP=%%a
GOTO createRoutes
)
GOTO couldNotGetVPNIP

:getVPNIP_2003
goto warnOSThenExit

:getVPNIP_VISTA
goto warnOSThenExit

:getVPNIP_7
rem Windows 7
for /f "tokens=3" %%a in ('netsh interface ip show addresses "%VPNNAME%" ^| find "IP Address"') do (
set VPNIP=%%a
GOTO createRoutes
)
GOTO couldNotGetVPNIP

:getVPNIP_2008
goto warnOSThenExit

:couldNotGetVPNIP
echo ! Failed to identify IP address from connection.
GOTO END

:couldNotStartVPN
echo ! Failed to start VPN : %VPNNAME%
GOTO END

:warnOSThenExit
echo ! No implementation for your OS
GOTO END

:createRoutes
echo * Routeringen worden aangelegt voor 'qqq'
route add 'ip1' MASK 255.255.0.0 %VPNIP%
route add 'ip2' MASK 255.255.0.0 %VPNIP%
echo.

:OK
echo * Connection established : %VPNNAME%

:END
if !NOPAUSE! == 1 GOTO ENDNOPAUSE
echo * This window may be closed
pause

:ENDNOPAUSE
set VPNIP=
set VPNNAME=
set VPNUSERNAME=
set VPNPASSWORD=
set NOPAUSE=EndLocal

exit

rem --- elevation routine ---

:doElevation
set str_ElevName="%temp%\elevate.vbs"
echo ' // On the fly created for elevating Batch files > %str_ElevName%
echo ' // (Should also work on other types) >> %str_ElevName%
echo Set objShell = CreateObject("Shell.Application") >> %str_ElevName%
echo Set objWshShell = WScript.CreateObject("WScript.Shell") >> %str_ElevName%
echo Set objWshProcessEnv = objWshShell.Environment("PROCESS") >> %str_ElevName%
echo strDir = "" ^& Wscript.Arguments(0) >> %str_ElevName%
echo strApp = "" ^& Wscript.Arguments(1) >> %str_ElevName%
echo strParam = "" >> %str_ElevName%
echo For i = 3 to Wscript.Arguments.Count >> %str_ElevName%
echo strParam = strParam ^& Wscript.Arguments(i-1) ^& " " >> %str_ElevName%
echo next >> %str_ElevName%
echo objShell.ShellExecute strApp, "" ^& strParam, strDir, "runas" >> %str_ElevName%
start wscript //nologo "%temp%\elevate.vbs" "%cd%" "%~nx0" %*
goto :ENDNOPAUSE

No comments: