Article: Enabling or Disabling HDX Plug-n-Play for USB Storage Devices

Intro

Sometimes your security team will be your best friend. They will be the “bad guy” in your IT organization and prevent certain applications from being installed or they will require that remote users have reduced access. Recently, I was approached by my security team to prevent access to the clipboard, local printers, and local drives for a certain group of users.  This better secures our environment and  reduces ICA bandwidth and speeds up login times. When I created a new Citrix policy to put these restrictions in place, I found that USB hard-drives were still being mapped.

Solution

Searching the Citrix eDocs site, I came across the following detail at the bottom of the Drives Folder section in the Policy Rules Reference:

Enabling or Disabling HDX Plug-n-Play for USB Storage Devices
HDX Plug-n-Play for USB storage devices is enabled by default. To change the settings for HDX Plug-n-Play for USB storage devices, manually change the key specified below on the XenApp server. Changes apply to all users.

Caution: Using Registry Editor incorrectly can cause serious problems that can require you to reinstall the operating system. Citrix cannot guarantee that problems resulting from incorrect use of Registry Editor can be solved. Use Registry Editor at your own risk. Make sure you back up the registry before you edit it.

Toggle USB drive redirection on and off using the following registry key on the server:

On XenApp 32-bit edition
HKEY_LOCAL_MACHINE\Software\Citrix\Policies\DisableUSBDriveRedirection

On XenApp 64-bit edition
HKEY_LOCAL_MACHINE\Software\Wow6432Node\Citrix\Policies\DisableUSBDriveRedirection

Type: DWORD

Values:
1 = redirection disabled
0 = redirection enabled

Note: HDX Plug-n-Play for USB storage devices is enabled when the registry key is not present.

Once I added the registry setting (and logged back in), I was no longer able to see any mapped USB hard drives. This is also referenced in the following CTX articles: How to Prevent Manual Mapping of Client Connected USB Drives and How to Disable USB Drive Redirection

Thanks,
Alain

PowerShell: Message of the Day

Intro

In an effort to be more communicative with our users on changes made to our XenApp servers we decided to create a message of the day.  Here are the criteria we decided on:

  1. The message would show as part of the user’s startup script when logging into XenApp/XenDesktop
  2. The user would have to click a button to remove the message box and the result could be acted on by the script. In one example, if the user pressed the Yes button, then we could open a page to our intranet.
  3. A hidden flag file would be written to a user’s home directory to note that the user had already seen the message.
  4. The Message file itself should be easily edited by any member of the team and not require any scripting knowledge.

I wore Google out looking for various solutions and examples to get the script working the way I wanted. I do not consider myself an expert in PowerShell by any means, but hopefully this post will give you some ideas to try in your environment.

Assumptions

Our environment supports development, staging and production environments along with different XenApp images depending on software being delivered. As a result, the scripts and the text file that is the message of the day are sync’d to the C: drive of the provisioned image. This allows us to tailor messages to different user populations.

While running, the script will archive the existing message, and since the user does not have permission to write to the C: drive (this script runs during a user’s login as the user) I’m using an admin account to write the archive file to the C: drive.

The Script

I’m using Show-Messagebox.ps1 to create the message box provided by Daniel Sörlöv (original link, his new site):

This script is located in the LocalCache folder along with motd.ps1 on the XenApp server.


param ($title,$text,$buttons="OK",$icon="None")

$FormsAssembly = [System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")

$dialogButtons = @{
 "OK"=[Windows.Forms.MessageBoxButtons]::OK;
 "OKCancel"=[Windows.Forms.MessageBoxButtons]::OKCancel;
 "AbortRetryIgnore"=[Windows.Forms.MessageBoxButtons]::AbortRetryIgnore;
 "YesNoCancel"=[Windows.Forms.MessageBoxButtons]::YesNoCancel;
 "YesNo"=[Windows.Forms.MessageBoxButtons]::YesNo;
 "RetryCancel"=[Windows.Forms.MessageBoxButtons]::RetryCancel }

$dialogIcons = @{
 "None"=[Windows.Forms.MessageBoxIcon]::None
 "Hand"=[Windows.Forms.MessageBoxIcon]::Hand
 "Question"=[Windows.Forms.MessageBoxIcon]::Question
 "Exclamation"=[Windows.Forms.MessageBoxIcon]::Exclamation
 "Asterisk"=[Windows.Forms.MessageBoxIcon]::Asterisk
 "Stop"=[Windows.Forms.MessageBoxIcon]::Stop
 "Error"=[Windows.Forms.MessageBoxIcon]::Error
 "Warning"=[Windows.Forms.MessageBoxIcon]::Warning
 "Information"=[Windows.Forms.MessageBoxIcon]::Information
}

$dialogResponses = @{
 [System.Windows.Forms.DialogResult]::None="None";
 [System.Windows.Forms.DialogResult]::OK="Ok";
 [System.Windows.Forms.DialogResult]::Cancel="Cancel";
 [System.Windows.Forms.DialogResult]::Abort="Abort";
 [System.Windows.Forms.DialogResult]::Retry="Retry";
 [System.Windows.Forms.DialogResult]::Ignore="Ignore";
 [System.Windows.Forms.DialogResult]::Yes="Yes";
 [System.Windows.Forms.DialogResult]::No="No"
}

return $dialogResponses[[Windows.Forms.MessageBox]::Show($text,$title,$dialogButtons[$buttons],$dialogIcons[$icon])]

The Get-Checksum function is from Moe Winnett at the TechMumboJumblog. I use this function to generate a checksum of the current message.txt file that will be displayed to the user. Once the message is displayed, a hidden .flag file is written to the user’s home directory containing the message.txt checksum. When the script checks to see if the user has seen the message, a checksum is generated of the current message.txt and compared to the checksum written to the .flag file. If they match, the script assumes that the user has already seen the message and will not display it.


######################################################################
### Get-Checksum function #############################################

function Get-Checksum($file, $crypto_provider) {
 if ($crypto_provider -eq $null) {
 $crypto_provider = new-object 'System.Security.Cryptography.MD5CryptoServiceProvider';
 }

$file_info = get-item $file;
 trap { ;
 continue } $stream = $file_info.OpenRead();
 if ($? -eq $false) {
 return $null;
 }

$bytes = $crypto_provider.ComputeHash($stream);
 $checksum = '';
 foreach ($byte in $bytes) {
 $checksum += $byte.ToString('x2');
 }

$stream.close() | out-null;

return $checksum;
}
#######################################################################
### Main Code #########################################################

##Set env variables
$hostname = gc env:computername
$lc = (get-item env:localcache).value
$today=get-date
$usrname = [Environment]::UserName
$usrprofile = (get-item env:userprofile).value
$usrPath = "$usrprofile\$usrname"

##Set credentials
$username="DOMAIN\CITRIXADMIN"
$Passwd = "PASSWORD1"

## Set file paths
$MsgBox = "$lc\scripts\Show-MessageBox.ps1"
$MsgPath ="$lc\message"
$MsgFile = "message.txt"
$MsgFilePath = $MsgPath + '\' + $MsgFile
$userhome = (get-item env:USERHOME).Value
$debug = $true

##Check if there is a message file and get its checksum, if not exit the script
if (!(test-path $MsgFilePath)) {
 if ($debug) {write-host "No MOTD file"}
 exit
} else {
##Check whether Message has been archived
 if (Get-Content -LiteralPath $MsgFilePath | Select-String -Pattern "ARCHIVE" -Quiet)
 {
 if ($debug) {write-host "MOTD file was already archived"}
 } else {
 if (!(test-path $MsgPath\archive)) {
 md $MsgPath\archive | out-null
 if ($debug) {write-host "Created archive folder"}
 }
 $ArchiveNote="`r`nARCHIVE - Delete this line if editing file"
 net use \\$Hostname\c$ /user:$username $Passwd
 $tmppath="\\$hostname\c$\localcache\message\message.txt"
 add-content $tmppath $ArchiveNote
 net use \\$Hostname\c$ /delete
 copy $MsgPath\$MsgFile "$MsgPath\archive\MOTD_$((get-date).toString('yyyyMMdd')).txt"
 if ($debug) {write-host "Archived $MsgFilePath to $MsgPath\archive\MOTD_$((get-date).toString('yyyyMMdd')).txt"}
 }
 $MsgChecksum = get-checksum($MsgFilePath)
}

##Check if the user has already seen the message
if (test-path $flexpath\.flag) {
 if ($debug) {write-host "User has seen a message - check if it's current message"}
 $a = get-item $flexpath\.flag -Force
 $flgchecksum = Get-Content $a
 if ($flgchecksum -eq $MsgChecksum) {
 if ($debug) {write-host "User has seen today's message - exit"
 exit
 }
 } else {
 del $a -force
 $flagfile = new-item $flexpath\.flag -type file
 Add-Content $flagfile $MsgChecksum
 Set-ItemProperty -Path $flagfile -Name Attributes -Value ([System.IO.FileAttributes]::Hidden)
 if ($debug) {write-host "User's old .flag file deleted"}
 if ($debug) {write-host "Created hidden flag file: $flagfile and wrote checksum: $MsgChecksum"}
 }
} else {
 $flagfile = new-item $flexpath\.flag -type file
 Add-Content $flagfile $MsgChecksum
 Set-ItemProperty -Path $flagfile -Name Attributes -Value ([System.IO.FileAttributes]::Hidden)
 if ($debug) {write-host "User has not seen the message"}
 if ($debug) {write-host "Created hidden flag file: $flagfile and wrote checksum: $MsgChecksum"}
}

## Create Window Title with date
$dt = get-date -format D
$tit = "Message of the Day for " + $dt

## Display MessageBox
$Msg = [string]::join([environment]::newline, (get-content -LiteralPath $MsgFilePath))
$MsgB = $Msg.Replace("ARCHIVE - Delete this line if editing file","")

$result = & $MsgBox $tit $MsgB "YesNo" "Information"

## Act on result
if ($result -eq "No") {
 Write-Host "User Pressed: $result"
}

if ($result -eq "Yes") {
 Write-Host "User Pressed: $result"
}

Message.txt

Message.txt is the file that is read by the MOTD.ps1 script and displayed to the user.  If you edit the file and see the archive message in the file, you must delete the archive message…

This will ensure the new message is archived by the script.  Currently the script assumes there will be only one message a day, so if a change is made to the script it will overwrite that day’s archived message.  The message.txt is stored in the localcache folder on the server in the Message folder.  The script will create an Archive folder under the Message folder if none is present.

Script Logic Flow

 

Thanks,
Alain

PowerShell: Citrix XenApp Commands, Copying HMR Tests

Intro

Your Citrix farm will have the Health Monitoring and Recovery Tests turned on for all servers by default.  I have found that some tests can cause problems in the daily operation of your farm.  In particular, the XML Service Test will take your XenApp servers out of production since its default action is to remove the server from load balancing if the test fails. Typically you have to run the following command to discover if you have any servers removed this way (unless you love watching the alerts section in the AMC):

QFARM /LBOFF

This command will place the server (CTXSERVER1) back into load balancing:

ENABLELB CTXSERVER1

My advice is to only run the XML Service Test on the servers you have dedicated to XML ticket requests, and if you happen to have a NetScaler I would manage your XML load balancing with it instead of using the HMR tests.  In this post, I will cover some XenApp PowerShell cmdlets that will allow you to remove this test from all the servers in your farm.

PowerShell

First, get your current HMR tests and save them in a variable (NOTE: You should have the Citrix Cmdlets loaded):

Add-PSSnapin -Name Citrix.XenApp.Commands
$HMRTests = Get-XAHmrTest

Which gives us:

PS C:\>$HMRTests
TestName : Citrix IMA Service test
Description : This test queries the service to ensure that it is running by enumerating the applications available on the server.
FilePath : Citrix\IMATest.exe
Arguments :
Interval : 60
Threshold : 5
Timeout : 60
RecoveryAction : AlertOnly
ServerName :

TestName : Logon Monitor test
Description : Logon/logoff cycles are monitored to determine whether there is a problem with session initialization or possibly an application failure. If there are a lot of short cycles within a short time period, a problem is assumed to exist
FilePath : Citrix\LogonMonitor.dll
Arguments : /SessionTime:5 /SessionThreshold:50 /SampleInterval:600
Interval : 1
Threshold : 5
Timeout : 1
RecoveryAction : AlertOnly
ServerName :

TestName : Terminal Services test
Description : This test enumerates the list of sessions running on the server and the session user information, such as user name.
FilePath : Citrix\CheckTermSrv.exe
Arguments :
Interval : 60
Threshold : 5
Timeout : 30
RecoveryAction : AlertOnly
ServerName :

TestName : XML Service test
Description : This test requests a ticket from the XML service running on the server and prints the ticket.
FilePath : Citrix\RequestTicket.exe
Arguments :
Interval : 60
Threshold : 5
Timeout : 60
RecoveryAction : RemoveServerFromLoadBalancing
ServerName :

So we have an object-array full of the HMR tests, but we don’t want the last one, namely the XML Service Test. I’m going to create a new object array variable that will only hold the first 3 objects in $HMRTests.

$HMRNewTests = $HMRTests[0..2]

The default Farm Properties for the Heath Monitoring and Recovery are to Use Farm Settings.

image

Server Properties:

image

In order to turn this off you can use the following command:

Get-XAServer -ServerName CTXSERVER1 | Set-XAServerConfiguration -HmrUseFarmSettings $false -HmrEnabled $true

This changes the server properties to:

image

Now you just have to copy the HMR Tests to the server with this command:

$HMRNewTests | New-XAHmrTest -ServerName CTXSERVER1

Now the server properties shows the new tests without the XML Service Test:

image

Using these commands you can make the changes to all the servers in your farm at once, but please test in a development environment first and use the handy –whatif flag on your commands to confirm the changes are what you intend.

As always, I welcome all comments and questions (especially about a better way to do this with PowerShell).

Thanks,
Alain