I’ve used the Web Capacity Analysis Tool (WCAT) in the past to measure the performance of Citrix Web Interface with some success. So I thought this tool would be perfect for load testing Storefront. I loaded up Fiddler, set up the WCAT extension, captured a web logon and then application launch. The whole process looked like this:
Pretty simple right?
I logged on with Domain Passthrough authentication, I clicked on an application and I was done. I have my customizations added in as well. But what is actually happening? How long does each step of the actual process take? To determine this answer, we go to Fiddler to capture our flow so we can examine each step.
I truncated all the icon resource calling. You can see it calls around 120 individual icon URLs. I have my two custom ‘helpers’ (ADInfo and LogonType) that determine logon preference and whether we should get workspace control enabled or disabled (LogonType.aspx and GroupMembership.aspx).
So the actual calls to Storefront revolve around 7 unique queries to the Storefront server. They are:
1 2 3 4 5 6 7 |
/Home/Configuration /Resources/List /Authentication/GetUserName /ExplicitAuth/AllowSelfServiceAccountManagement /Authentication/GetUserName /Resources/GetLaunchStatus/<string> /Resources/LaunchIca/<string> |
Fortunately, Citrix has documented how these need to be configured to successfully call these services.
With all this setup, I took my scenario file and executed it. Nothing appeared to happen. I broke down the scenario file into the individual calls and found where it was breaking. This Citrix documentation explains it nicely:
Cross-site request forgery token
To protect against cross-site request forgery (CSRF) attacks, the Web Proxy APIs require that a CSRF token be supplied by the client, unless specified otherwise. This is a random string generated by the Web Proxy for the duration of the session and communicated to the client using a session cookie. Clients must read the value of this cookie and send it back to the Web Proxy in most API calls, as either the value of a header named Csrf-Token (note the hyphen) for POST requests, or as the value of a query string parameter named CsrfToken for GET requests.
The part in bold and underlined, is troublesome with WCAT. WCAT does not appear to have this ability (read the value of a cookie and set a header to send it back to the web proxy). What Storefront does, is send back a set-cookie back to the client which WCAT has no problem with… but the data Storefront sends back is multiple values within that set-cookie command. And this is a problem.
This is supposed to take this ‘Set-Cookie’ command and set two different values:
Cookie | Value |
CsrfToken | FE2148E03989CB263CCD82A5888BF039 |
path | /Citrix/StoreWeb/ |
But what WCAT does is create a single cookie that looks like this:
Cookie | Value |
cookie | CsrfToken=FE2148E03989CB263CCD82A5888BF039; path=/Citrix/StoreWeb/; |
WCAT creates a cookie with the entirety of a string value instead of separating them out. Is there a way to parse this Set-Cookie command so that these are stored correctly?
Unfortunately, I was not able to find a way to do this with WCAT.
However, we can use Powershell to accomplish this job. Ryan Butler has created a script to query the Storefront services to generate an ICA file. This script is about 90% of what I need, however I’m not interested in doing an explicit logon, I want to do Domain Passthrough (integrateWindows) authentication, and I want to simulate the process as was captured by Fiddler, so I’ll be calling the additional services (GetUserName, AllowSelfServiceAccountManagement, etc.) and capture the time required for each section.
My Storefront Logon/Stress testing script:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 |
<# .SYNOPSIS This script is a modification from Ryan Butler's get-ICAFile_v3_auth.ps1 file from here: https://github.com/ryancbutler/StorefrontICACreator/blob/master/get-ICAfile_v3_auth.ps1 This script will execute an entire Citrix Storefront logon and application launch process. This script should be run in a loop for continous stress testing. Author: Trentent Tye Version: 2017.05.23 DESCRIPTION A Powershell v5 Script that utilizes invoke-webrequest to connect to a Citrix Storefront server and go through the logon and launch process .PARAMETER store Storefront URL -- eg http://bottheory.local/Citrix/StoreWeb/ .PARAMETER loop Run this script forever or once .PARAMETER stressComponent Loop the calls to StoreFront for one of the components. The list of components available to stress: "Get Auth Methods" "Domain Pass-Through and Smart Card Authentication" "Resource Enumeration" "Get User Name - 1" "AllowSelfServiceAccountManagement" "Get User Name - 2" "ICA Launch.GetLaunchStatus" "ICA Launch.LaunchIca" .EXAMPLE ./Stress_Storefront.ps1 -store "http://bottheory.local/Citrix/StoreWeb/" -loop $false ./Stress_Storefront.ps1 -store "http://storefront2.bottheory.local/Citrix/StoreWeb/" -loop $true ./Stress_Storefront.ps1 -store "http://storefront2.bottheory.local/Citrix/StoreWeb/" -stressComponent "Domain Pass-Through and Smart Card Authentication" #> Param ( [string]$store, [bool]$loop = $true, [string]$stressComponent ) #if $loop == true then this will run forever while ($loop) { #use a default store if not specified in the command line if (-not($store)) { $store = "http://storefront2.bottheory.local/Citrix/StoreWeb/" } #perf enhancement - disable invoke-webrequest progress bar $ProgressPreference = 'SilentlyContinue' #are we using http or https? This is need for the X-Citrix-IsUsingHTTPS cookie. $httpOrhttps = $store.split(":") if ($httpOrhttps[0] -eq "https") { $httpOrhttps = "Yes" } else { $httpOrhttps = "No" } $StartMs = Get-Date #properties for stats to export to CSV $prop = New-Object System.Object $prop | Add-Member -type NoteProperty -name "Runtime" -value $StartMs write-host -ForegroundColor Yellow "Connecting to $store" $stage = "Initial Connection" #First connection to root of site $headers = @{ "Accept"='application/xml, text/xml, */*; q=0.01'; "Content-Length"="0"; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; } $duration = measure-command {Invoke-WebRequest -Uri $store -Method GET -Headers $headers -SessionVariable SFSession -UseBasicParsing} -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds <# https://citrix.github.io/storefront-sdk/requests/#client-configuration Client Configuration #> $stage = "Client Configuration" write-host -ForegroundColor Yellow "$stage" $headers = @{ "Accept"='application/xml, text/xml, */*; q=0.01'; "Content-Length"="0"; "X-Requested-With"="XMLHttpRequest"; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; "Referer"=$store; } $duration = measure-command {Invoke-WebRequest -Uri ($store + "Home/Configuration") -Method POST -Headers $headers -ContentType "application/x-www-form-urlencoded" -WebSession $sfsession -UseBasicParsing} -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds # csrf cookie $csrf = $sfsession.cookies.GetCookies($store)|where{$_.name -like "CsrfToken"} $cookiedomain = $csrf.Domain <# https://citrix.github.io/storefront-sdk/requests/#authentication-methods Note The client must first make a POST request to /Resources/List. Since the user is not yet authenticated, this returns a challenge in the form of a CitrixWebReceiver- Authenticate header with the GetAuthMethods URL in the location field. #> $stage = "Get Authentication Methods" write-host -ForegroundColor Yellow "$stage" $headers = @{ "Content-Type"='application/x-www-form-urlencoded; charset=UTF-8'; "Accept"='application/json, text/javascript, */*; q=0.01'; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; "Csrf-Token"=$csrf.value; "Referer"=$store; "format"='json&resourceDetails=Default'; } $duration = measure-command {Invoke-WebRequest -Uri ($store + "Resources/List") -Method POST -Headers $headers -WebSession $SFSession} -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds <# https://citrix.github.io/storefront-sdk/requests/#example-get-auth-methods #> $stage = "Get Auth Methods" write-host -ForegroundColor Yellow "$stage" #Gets authentication methods $headers = @{ "Accept"='application/xml, text/xml, */*; q=0.01'; "Content-Length"="0"; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; "Referer"=$store; "Csrf-Token"=$csrf.value; } if ($stressComponent -eq $stage) { write-host -ForegroundColor Yellow "Stressing Component $stage" write-host -ForegroundColor Yellow "Duration of task:" while ($true) { (measure-command {Invoke-WebRequest -Uri ($store + "Authentication/GetAuthMethods") -Method POST -Headers $headers -WebSession $sfsession -UseBasicParsing}).TotalSeconds } } else { $duration = measure-command {Invoke-WebRequest -Uri ($store + "Authentication/GetAuthMethods") -Method POST -Headers $headers -WebSession $sfsession -UseBasicParsing} -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds } <# https://citrix.github.io/storefront-sdk/requests/#domain-pass-through-and-smart-card-authentication Domain Pass-Through and Smart Card Authentication #> $stage = "Domain Pass-Through and Smart Card Authentication" write-host -ForegroundColor Yellow "$stage" #Start Login Process $headers = @{ "Accept"="application/xml, text/xml, */*; q=0.01"; "Csrf-Token"=$csrf.Value; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; "Content-Length"="0"; } #Add cookies that would normally prompt $cookie = New-Object System.Net.Cookie $cookie.Name = "CtxsUserPreferredClient" $cookie.Value = "Native" $cookie.Domain = $cookiedomain $sfsession.Cookies.Add($cookie) $cookie = New-Object System.Net.Cookie $cookie.Name = "CtxsClientDetectionDone" $cookie.Value = "true" $cookie.Domain = $cookiedomain $sfsession.Cookies.Add($cookie) $cookie = New-Object System.Net.Cookie $cookie.Name = "CtxsHasUpgradeBeenShown" $cookie.Value = "true" $cookie.Domain = $cookiedomain $sfsession.Cookies.Add($cookie) if ($stressComponent -eq $stage) { write-host -ForegroundColor Yellow "Stressing Component $stage" write-host -ForegroundColor Yellow "Duration of task:" while ($true) { (measure-command {Invoke-WebRequest -Uri ($store + "DomainPassthroughAuth/Login") -Method POST -Headers $headers -WebSession $SFSession -UseDefaultCredentials -UseBasicParsing}).TotalSeconds } } else { $duration = measure-command { $content = Invoke-WebRequest -Uri ($store + "DomainPassthroughAuth/Login") -Method POST -Headers $headers -WebSession $SFSession -UseDefaultCredentials -UseBasicParsing } -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds } <# https://citrix.github.io/storefront-sdk/how-the-api-works/#cookies CtxsAuthId - HttpOnly - Response indicating successful authentication - Protects against session fixation attacks #> #set CtxsAuthId cookie because we authenticated. foreach ($item in $content.Headers.'Set-Cookie'.Split(";")) { $values = $item.split("=") if ($values[0] -eq "CtxsAuthId") { $cookie = New-Object System.Net.Cookie $cookie.Name = "$($values[0])" $cookie.Value = "$($values[1])" $cookie.Domain = $cookiedomain $sfsession.Cookies.Add($cookie) } } <# https://citrix.github.io/storefront-sdk/requests/#resource-enumeration Typically, this request requires an authenticated session, indicated by the cookies ASP.NET_SessionId and CtxsAuthId. However, when the Web Proxy is configured to use an unauthenticated Store, an authenticated session is not required. The Web Proxy always performs a fresh enumeration for the user by communicating with the StoreFront Store service to pick up any changes that may have occurred. #> $stage = "Resource Enumeration" write-host -ForegroundColor Yellow "$stage" #Gets resources and required ICA URL $headers = @{ "Content-Type"='application/x-www-form-urlencoded; charset=UTF-8'; "Accept"='application/json, text/javascript, */*; q=0.01'; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; "Csrf-Token"=$csrf.value; "Referer"=$store; "X-Requested-With"="XMLHttpRequest"; } $body = @{ "format"='json'; "resourceDetails"='Default'; } if ($stressComponent -eq $stage) { write-host -ForegroundColor Yellow "Stressing Component $stage" write-host -ForegroundColor Yellow "Duration of task:" while ($true) { (measure-command {Invoke-WebRequest -Uri ($store + "Resources/List") -Method POST -Headers $headers -body $body -WebSession $SFSession -UseBasicParsing}).TotalSeconds } } else { $duration = measure-command { $content = Invoke-WebRequest -Uri ($store + "Resources/List") -Method POST -Headers $headers -body $body -WebSession $SFSession -UseBasicParsing } -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds } #save the list of applications we got from Storefront $resources = $content.content | ConvertFrom-Json write-host -ForegroundColor Yellow "Found $($resources.resources.count) applications" <# https://citrix.github.io/storefront-sdk/requests/#get-user-name Use this request to obtain the full user name, as configured in Active Directory. If the full user name is unavailable, the user's logon name is returned instead. This request requires an authenticated session, indicated by the cookies ASP.NET_SessionId and CtxsAuthId. When using an unauthenticated Store, no user has actually logged on and an HTTP 403 response is returned. The Web Proxy uses the StoreFront Token Validation service to obtain the user name from the authentication token. #> $stage = "Get User Name - 1" write-host -ForegroundColor Yellow "$stage" #getUserName $headers = @{ "Accept"='text/plain, */*; q=0.01'; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; "Csrf-Token"=$csrf.value; "Referer"=$store; "X-Requested-With"="XMLHttpRequest"; } if ($stressComponent -eq $stage) { write-host -ForegroundColor Yellow "Stressing Component $stage" write-host -ForegroundColor Yellow "Duration of task:" while ($true) { (measure-command {Invoke-WebRequest -Uri ($store + "Authentication/GetUserName") -Method POST -Headers $headers -WebSession $SFSession -UseBasicParsing}).TotalSeconds } } else { $duration = measure-command { $content = Invoke-WebRequest -Uri ($store + "Authentication/GetUserName") -Method POST -Headers $headers -WebSession $SFSession -UseBasicParsing } -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds } <# undocumented? For password self reset? #> $stage = "AllowSelfServiceAccountManagement" write-host -ForegroundColor Yellow "$stage" #AllowSelfServiceAccountManagement? $headers = @{ "Accept"='text/plain, */*; q=0.01'; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; "Csrf-Token"=$csrf.value; "Referer"=$store; "X-Requested-With"="XMLHttpRequest"; } if ($stressComponent -eq $stage) { write-host -ForegroundColor Yellow "Stressing Component $stage" write-host -ForegroundColor Yellow "Duration of task:" while ($true) { (measure-command {Invoke-WebRequest -Uri ($store + "ExplicitAuth/AllowSelfServiceAccountManagement") -Method POST -Headers $headers -WebSession $SFSession -UseBasicParsing}).TotalSeconds } } else { $duration = measure-command { $content = Invoke-WebRequest -Uri ($store + "ExplicitAuth/AllowSelfServiceAccountManagement") -Method POST -Headers $headers -WebSession $SFSession -UseBasicParsing } -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds } <# https://citrix.github.io/storefront-sdk/requests/#get-user-name Use this request to obtain the full user name, as configured in Active Directory. If the full user name is unavailable, the user's logon name is returned instead. This request requires an authenticated session, indicated by the cookies ASP.NET_SessionId and CtxsAuthId. When using an unauthenticated Store, no user has actually logged on and an HTTP 403 response is returned. The Web Proxy uses the StoreFront Token Validation service to obtain the user name from the authentication token. #> $stage = "Get User Name - 2" write-host -ForegroundColor Yellow "$stage" $headers = @{ "Accept"='text/plain, */*; q=0.01'; "X-Citrix-IsUsingHTTPS"="$httpOrhttps"; "Csrf-Token"=$csrf.value; "Referer"=$store; "X-Requested-With"="XMLHttpRequest"; } if ($stressComponent -eq $stage) { write-host -ForegroundColor Yellow "Stressing Component $stage" write-host -ForegroundColor Yellow "Duration of task:" while ($true) { (measure-command {Invoke-WebRequest -Uri ($store + "Authentication/GetUserName") -Method POST -Headers $headers -WebSession $SFSession -UseBasicParsing}).TotalSeconds } } else { $duration = measure-command { $content = Invoke-WebRequest -Uri ($store + "Authentication/GetUserName") -Method POST -Headers $headers -WebSession $SFSession -UseBasicParsing } -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds } #Download all the icons $stage = "Download all the icons" write-host -ForegroundColor Yellow "$stage" $duration = measure-command { foreach ($iconurl in $resources.resources.iconurl) { Invoke-WebRequest -Uri ($store + $iconurl) -Method GET -Headers $headers -WebSession $SFSession -UseBasicParsing } } -ErrorAction Stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds <# https://citrix.github.io/storefront-sdk/requests/#ica-launch LaunchIca is a GET instead of a POST. /Resources/ GetLaunchStatus/{id} POST Request whether the specified resource is ready to launch or not. /Resources/LaunchIca/{id} GET Request an ICA file for launching the specified resource. #> $stage = "ICA Launch" write-host -ForegroundColor Yellow "$stage" #launch application $stage = "ICA Launch.GetLaunchStatus" if ($stressComponent -eq $stage) { write-host -ForegroundColor Yellow "Stressing Component $stage" write-host -ForegroundColor Yellow "Duration of task:" while ($true) { #Randomly select an application to launch like a random set of users $appToLaunch = $resources.resources[(Get-Random -Minimum 0 -Maximum ($resources.resources.count))] write-host -ForegroundColor Yellow "GetLaunchStatus for application: $($appToLaunch.name)" (measure-command {Invoke-WebRequest -Uri ($store + $appToLaunch.launchstatusurl) -Method POST -Headers $headers -body $body -WebSession $SFSession -UseBasicParsing}).TotalSeconds } } else { #Randomly select an application to launch like a random set of users $appToLaunch = $resources.resources[(Get-Random -Minimum 0 -Maximum ($resources.resources.count))] write-host -ForegroundColor Yellow "Launching application: $($appToLaunch.name)" $duration = measure-command { Invoke-WebRequest -Uri ($store + $appToLaunch.launchstatusurl) -Method POST -Headers $headers -body $body -WebSession $SFSession -UseBasicParsing } -ErrorAction stop $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds } $stage = "ICA Launch.LaunchIca" if ($stressComponent -eq $stage) { write-host -ForegroundColor Yellow "Stressing Component $stage" write-host -ForegroundColor Yellow "Duration of task:" while ($true) { (measure-command {Invoke-WebRequest -Uri ($store + $appToLaunch.launchurl) -Method GET -Headers $headers -WebSession $SFSession -UseBasicParsing}).TotalSeconds } } else { $duration = measure-command { Invoke-WebRequest -Uri ($store + $appToLaunch.launchurl) -Method GET -Headers $headers -WebSession $SFSession -UseBasicParsing } $prop | Add-Member -type NoteProperty -name "$stage" -value $duration.TotalSeconds } <# Export information to CSV #> $EndMs = Get-Date write-host "Loop took $($EndMs - $StartMs)" $prop | Add-Member -type NoteProperty -name "Total Runtime" -value $($EndMs - $StartMs) $prop | export-csv StressStorefront.csv -NoTypeInformation -Append } |
And this is the output:
We can now run this script concurrently to simulate multiple clients. Or we could run it with a command like so:
1 |
powershell.exe -executionpolicy bypass -file "Stress_StoreFront.ps1" -stressComponent "Get Auth Methods" |
To stress an individual component. By stressing the individual component we can actually determine which process on the Storefront server deals with which service. So which components equals which service?
Get Auth Methods:
This component stresses “Citrix Receiver for Web”
Domain Pass-Through and Smart Card Authentication:
This component stresses “Citrix Receiver for Web” and “Citrix Delivery Services Authentication”
Resource Enumeration:
This component stresses “Citrix Receiver for Web” and “Citrix Delivery Services Resources”
Get User Name:
This component stresses “Citrix Receiver for Web”
AllowSelfServiceAccountManagement:
This component stresses “Citrix Receiver for Web” and “Citrix Delivery Services Authentication”
“GetLaunchStatus”
This component stresses “Citrix Delivery Services Resources”
LaunchIca
This component stresses “Citrix Delivery Services Resources”
Now that we have this script and we can see the where individual components cause stress, we can begin to push our Storefront server to find its limits and how the different configurations are impacted. I’ll write up my findings on the limits of Storefront next.
Pingback: Citrix Storefront – Performance and Tuning – Part 2 – PNA Traffic simulation – Trentent Tye – Microsoft MVP
Pingback: Citrix Logon Simulator’s – Part 1 – Trentent Tye – Microsoft MVP