1--- 2-- This module was written by Patrik Karlsson and facilitates communication 3-- with the Citrix XML Service. It is not feature complete and is missing several 4-- functions and parameters. 5-- 6-- The library makes little or no effort to verify that the parameters submitted 7-- to each function are compliant with the DTD 8-- 9-- As all functions handling requests take their parameters in the form of tables, 10-- additional functionality can be added while not breaking existing scripts 11-- 12-- Details regarding the requests/responses and their parameters can be found in 13-- the NFuse.DTD included with Citrix MetaFrame/Xenapp 14-- 15-- This code is based on the information available in: 16-- NFuse.DTD - Version 5.0 (draft 1) 24 January 2008 17-- 18 19 20 21local http = require "http" 22local stdnse = require "stdnse" 23local string = require "string" 24local table = require "table" 25_ENV = stdnse.module("citrixxml", stdnse.seeall) 26 27--- Decodes html-entities to chars eg.   => <space> 28-- 29-- @param xmldata string to convert 30-- @return string an e 31function decode_xml_document(xmldata) 32 33 local hexval 34 35 if not xmldata then 36 return "" 37 end 38 39 local newstr = xmldata 40 local escaped_val 41 42 while string.match(newstr, "(&#%d+;)" ) do 43 escaped_val = string.match(newstr, "(&#%d+;)") 44 hexval = escaped_val:match("(%d+)") 45 46 if ( hexval ) then 47 newstr = newstr:gsub(escaped_val, string.char(hexval)) 48 end 49 50 end 51 52 return newstr 53 54end 55 56--- Sends the request to the server using the http lib 57-- 58-- @param host string or host table of the remote server 59-- @param port number or port table of the remote server 60-- @param xmldata string, the HTTP data part of the request as XML 61-- 62-- @return string with the response body 63-- 64function send_citrix_xml_request(host, port, xmldata) 65 66 local response = http.post( host, port, "/scripts/WPnBr.dll", { header={["Content-Type"]="text/xml"}}, nil, xmldata) 67 68 -- this is *probably* not the right way to do stuff 69 -- decoding should *probably* only be done on XML-values 70 -- this is *probably* defined in the standard, for anyone interested 71 return decode_xml_document(response.body) 72 73end 74 75--- Request information about the Citrix Server Farm 76-- 77-- Consult the NFuse.DTD for a complete list of supported parameters 78-- This function implements all the supported parameters described in: 79-- Version 5.0 (draft 1) 24 January 2008 80-- 81-- @param host string or host table of the remote server 82-- @param port number or port table of the remote server 83-- @return string HTTP response data 84-- 85function request_server_farm_data( host, port ) 86 87 local xmldata = "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\r\n\z 88 <!DOCTYPE NFuseProtocol SYSTEM \"NFuse.dtd\">\r\n\z 89 <NFuseProtocol version=\"1.1\">\z 90 <RequestServerFarmData></RequestServerFarmData>\z 91 </NFuseProtocol>\r\n" 92 93 return send_citrix_xml_request(host, port, xmldata) 94end 95 96--- Parses the response from the request_server_farm_data request 97-- @param response string with the XML response 98-- @return table containing server farm names 99-- 100function parse_server_farm_data_response( response ) 101 102 local farms = {} 103 104 response = response:gsub("\r?\n","") 105 for farm in response:gmatch("<ServerFarmName.->([^<]+)</ServerFarmName>") do 106 table.insert(farms, farm) 107 end 108 109 return farms 110 111end 112 113--- Sends a request for application data to the Citrix XML service 114-- 115-- Consult the NFuse.DTD for a complete list of supported parameters 116-- This function does NOT implement all the supported parameters 117-- 118-- Supported parameters are Scope, ServerType, ClientType, DesiredDetails 119-- 120-- @param host string or host table which is to be queried 121-- @param port number or port table of the XML service 122-- @param params table with parameters 123-- @return string HTTP response data 124-- 125function request_appdata(host, port, params) 126 127 -- setup the mandatory parameters if they're missing 128 local scope = params['Scope'] or "onelevel" 129 local server_type = params['ServerType'] or "all" 130 local client_type = params['ClientType'] or "ica30" 131 local desired_details = params['DesiredDetails'] or nil 132 133 local xmldata = { 134 '<?xml version="1.0" encoding="ISO-8859-1"?>\r\n\z 135 <!DOCTYPE NFuseProtocol SYSTEM "NFuse.dtd">\r\n\z 136 <NFuseProtocol version="5.0"><RequestAppData><Scope traverse="', 137 scope, 138 '" /><ServerType>', 139 server_type, 140 "</ServerType><ClientType>", 141 client_type, 142 "</ClientType>" 143 } 144 145 if desired_details then 146 if type(desired_details) == "string" then 147 xmldata[#xmldata+1] = "<DesiredDetails>" .. desired_details .. "</DesiredDetails>" 148 elseif type(desired_details) == "table" then 149 for _, v in ipairs(desired_details) do 150 xmldata[#xmldata+1] = "<DesiredDetails>" .. v .. "</DesiredDetails>" 151 end 152 else 153 assert(desired_details) 154 end 155 156 end 157 158 xmldata[#xmldata+1] = "</RequestAppData></NFuseProtocol>\r\n" 159 160 return send_citrix_xml_request(host, port, table.concat(xmldata)) 161end 162 163 164--- Extracts the Accesslist section of the XML response 165-- 166-- @param xmldata string containing results from the request app data request 167-- @return table containing settings extracted from the accesslist section of the response 168local function extract_appdata_acls(xmldata) 169 170 local acls = {} 171 local users = {} 172 local groups = {} 173 174 for acl in xmldata:gmatch("<AccessList>(.-)</AccessList>") do 175 176 if acl:match("AnonymousUser") then 177 table.insert(users, "Anonymous") 178 else 179 180 for user in acl:gmatch("<User>(.-)</User>") do 181 local user_name = user:match("<UserName.->(.-)</UserName>") or "" 182 local domain_name = user:match("<Domain.->(.-)</Domain>") or "" 183 184 if user_name:len() > 0 then 185 if domain_name:len() > 0 then 186 domain_name = domain_name .. "\\" 187 end 188 table.insert(users, domain_name .. user_name) 189 end 190 191 end 192 193 for group in acl:gmatch("<Group>(.-)</Group>") do 194 195 196 local group_name = group:match("<GroupName.->(.-)</GroupName>") or "" 197 local domain_name = group:match("<Domain.->(.-)</Domain>") or "" 198 199 if group_name:len() > 0 then 200 if domain_name:len() > 0 then 201 domain_name = domain_name .. "\\" 202 end 203 table.insert(groups, domain_name .. group_name) 204 end 205 206 end 207 208 end 209 210 if #users> 0 then 211 acls['User'] = users 212 end 213 if #groups>0 then 214 acls['Group'] = groups 215 end 216 217 end 218 219 return acls 220 221end 222 223 224--- Extracts the settings section of the XML response 225-- 226-- @param xmldata string containing results from the request app data request 227-- @return table containing settings extracted from the settings section of the response 228local function extract_appdata_settings(xmldata) 229 230 local settings = {} 231 232 settings['appisdisabled'] = xmldata:match("<Settings.-appisdisabled=\"(.-)\".->") 233 settings['appisdesktop'] = xmldata:match("<Settings.-appisdesktop=\"(.-)\".->") 234 235 for s in xmldata:gmatch("<Settings.->(.-)</Settings>") do 236 settings['Encryption'] = s:match("<Encryption.->(.-)</Encryption>") 237 settings['EncryptionEnforced'] = s:match("<Encryption minimum=\"(.-)\">") 238 settings['AppOnDesktop'] = s:match("<AppOnDesktop.-value=\"(.-)\"/>") 239 settings['AppInStartmenu'] = s:match("<AppInStartmenu.-value=\"(.-)\"/>") 240 settings['PublisherName'] = s:match("<PublisherName.->(.-)</PublisherName>") 241 settings['SSLEnabled'] = s:match("<SSLEnabled.->(.-)</SSLEnabled>") 242 settings['RemoteAccessEnabled'] = s:match("<RemoteAccessEnabled.->(.-)</RemoteAccessEnabled>") 243 end 244 245 return settings 246 247end 248 249--- Parses the appdata XML response 250-- 251-- @param xmldata string response from request_appdata 252-- @return table containing nestled tables closely resembling the DOM model of the XML response 253function parse_appdata_response(xmldata) 254 255 local apps = {} 256 xmldata = xmldata:gsub("\r?\n",""):gsub(">%s+<", "><") 257 258 for AppData in xmldata:gmatch("<AppData>(.-)</AppData>") do 259 260 local app_name = AppData:match("<FName.->(.-)</FName>") or "" 261 local app = {} 262 263 app['FName'] = app_name 264 app['AccessList'] = extract_appdata_acls(AppData) 265 app['Settings'] = extract_appdata_settings(AppData) 266 267 table.insert(apps, app) 268 269 end 270 271 return apps 272end 273 274-- 275-- 276-- @param flags string, should be any of following: alt-addr, no-load-bias 277-- 278function request_address(host, port, flags, appname) 279 280 local xmldata = { 281 '<?xml version="1.0" encoding="ISO-8859-1"?>\r\n\z 282 <!DOCTYPE NFuseProtocol SYSTEM "NFuse.dtd">\r\n\z 283 <NFuseProtocol version="4.1"><RequestAddress>' 284 } 285 286 if flags then 287 xmldata[#xmldata+1] = "<Flags>" .. flags .. "</Flags>" 288 end 289 290 if appname then 291 xmldata[#xmldata+1] = "<Name><AppName>" .. appname .. "</AppName></Name>" 292 end 293 294 xmldata[#xmldata+1] = "</RequestAddress></NFuseProtocol>\r\n" 295 296 return send_citrix_xml_request(host, port, table.concat(xmldata)) 297end 298 299--- Request information about the Citrix protocol 300-- 301-- Consult the NFuse.DTD for a complete list of supported parameters 302-- This function implements all the supported parameters described in: 303-- Version 5.0 (draft 1) 24 January 2008 304-- 305-- @param host string or host table which is to be queried 306-- @param port number or port table of the XML service 307-- @param params table with parameters 308-- @return string HTTP response data 309-- 310function request_server_data(host, port, params) 311 312 local params = params or {} 313 local server_type = params.ServerType or {"all"} 314 local client_type = params.ClientType or {"all"} 315 316 local xmldata = { 317 '<?xml version="1.0" encoding="ISO-8859-1"?>\r\n\z 318 <!DOCTYPE NFuseProtocol SYSTEM "NFuse.dtd">\r\n\z 319 <NFuseProtocol version="1.1"><RequestServerData>' 320 } 321 322 for _, srvtype in pairs(server_type) do 323 xmldata[#xmldata+1] = "<ServerType>" .. srvtype .. "</ServerType>" 324 end 325 326 for _, clitype in pairs(client_type) do 327 xmldata[#xmldata+1] = "<ClientType>" .. clitype .. "</ClientType>" 328 end 329 330 xmldata[#xmldata+1] = "</RequestServerData></NFuseProtocol>\r\n" 331 332 return send_citrix_xml_request(host, port, table.concat(xmldata)) 333end 334 335--- Parses the response from the request_server_data request 336-- @param response string with the XML response 337-- @return table containing the server names 338-- 339function parse_server_data_response(response) 340 341 local servers = {} 342 343 response = response:gsub("\r?\n","") 344 for s in response:gmatch("<ServerName>([^<]+)</ServerName>") do 345 table.insert(servers, s) 346 end 347 348 return servers 349 350end 351 352--- Request information about the Citrix protocol 353-- 354-- Consult the NFuse.DTD for a complete list of supported parameters 355-- This function implements all the supported parameters described in: 356-- Version 5.0 (draft 1) 24 January 2008 357-- 358-- @param host string or host table which is to be queried 359-- @param port number or port table of the XML service 360-- @param params table with parameters 361-- @return string HTTP response data 362-- 363function request_protocol_info( host, port, params ) 364 365 local params = params or {} 366 367 local xmldata = { 368 '<?xml version="1.0" encoding="ISO-8859-1"?>\r\n\z 369 <!DOCTYPE NFuseProtocol SYSTEM "NFuse.dtd">\r\n\z 370 <NFuseProtocol version="1.1"><RequestProtocolInfo>' 371 } 372 373 if params['ServerAddress'] then 374 xmldata[#xmldata+1] = ('<ServerAddress addresstype="' .. 375 params['ServerAddress']['attr']['addresstype'] .. '">' .. 376 params['ServerAddress'] .. "</ServerAddress>") 377 end 378 379 xmldata[#xmldata+1] = "</RequestProtocolInfo></NFuseProtocol>\r\n" 380 381 return send_citrix_xml_request(host, port, table.concat(xmldata)) 382end 383 384--- Request capability information 385-- 386-- Consult the NFuse.DTD for a complete list of supported parameters 387-- This function implements all the supported parameters described in: 388-- Version 5.0 (draft 1) 24 January 2008 389-- 390-- @param host string or host table which is to be queried 391-- @param port number or port table of the XML service 392-- @return string HTTP response data 393-- 394function request_capabilities( host, port ) 395 396 local xmldata = '<?xml version="1.0" encoding="ISO-8859-1"?>\r\n\z 397 <!DOCTYPE NFuseProtocol SYSTEM "NFuse.dtd">\r\n\z 398 <NFuseProtocol version="1.1"><RequestCapabilities>\z 399 </RequestCapabilities></NFuseProtocol>\r\n' 400 401 return send_citrix_xml_request(host, port, xmldata) 402end 403 404--- Parses the response from the request_capabilities request 405-- @param response string with the XML response 406-- @return table containing the server capabilities 407-- 408function parse_capabilities_response(response) 409 410 local servers = {} 411 412 response = response:gsub("\r?\n","") 413 for s in response:gmatch("<CapabilityId.->([^<]+)</CapabilityId>") do 414 table.insert(servers, s) 415 end 416 417 return servers 418 419end 420 421--- Tries to validate user credentials against the XML service 422-- 423-- Consult the NFuse.DTD for a complete list of supported parameters 424-- This function implements all the supported parameters described in: 425-- Version 5.0 (draft 1) 24 January 2008 426-- 427-- 428-- @param host string or host table which is to be queried 429-- @param port number or port table of the XML service 430-- @param params table with parameters 431-- @return string HTTP response data 432-- 433function request_validate_credentials(host, port, params ) 434 435 local params = params or {} 436 local credentials = params['Credentials'] or {} 437 438 local xmldata = { 439 '<?xml version="1.0" encoding="ISO-8859-1"?>\r\n\z 440 <!DOCTYPE NFuseProtocol SYSTEM "NFuse.dtd">\r\n\z 441 <NFuseProtocol version="5.0"><RequestValidateCredentials><Credentials>' 442 } 443 444 if credentials['UserName'] then 445 xmldata[#xmldata+1] = "<UserName>" .. credentials['UserName'] .. "</UserName>" 446 end 447 448 if credentials['Password'] then 449 xmldata[#xmldata+1] = '<Password encoding="cleartext">' .. credentials['Password'] .. "</Password>" 450 end 451 452 if credentials['Domain'] then 453 xmldata[#xmldata+1] = '<Domain type="NT">' .. credentials['Domain'] .. "</Domain>" 454 end 455 456 xmldata[#xmldata+1] = "</Credentials></RequestValidateCredentials></NFuseProtocol>\r\n" 457 458 return send_citrix_xml_request(host, port, table.concat(xmldata)) 459 460end 461 462 463--- Parses the response from request_validate_credentials 464-- @param response string with the XML response 465-- @return table containing the results 466-- 467function parse_validate_credentials_response(response) 468 local tblResult = {} 469 470 response = response:gsub("\r?\n","") 471 tblResult['DaysUntilPasswordExpiry'] = response:match("<DaysUntilPasswordExpiry>(.+)</DaysUntilPasswordExpiry>") 472 tblResult['ShowPasswordExpiryWarning'] = response:match("<ShowPasswordExpiryWarning>(.+)</ShowPasswordExpiryWarning>") 473 tblResult['ErrorId'] = response:match("<ErrorId>(.+)</ErrorId>") 474 475 return tblResult 476 477end 478 479--- Sends a request to reconnect session data 480-- 481-- Consult the NFuse.DTD for a complete list of supported parameters 482-- This function does NOT implement all the supported parameters 483---- 484-- @param host string or host table which is to be queried 485-- @param port number or port table of the XML service 486-- @param params table with parameters 487-- 488function request_reconnect_session_data(host, port, params) 489 490 local params = params or {} 491 local Credentials = params.Credentials or {} 492 493 params.ServerType = params.ServerType or {} 494 params.ClientType = params.ClientType or {} 495 496 local xmldata = { 497 '<?xml version="1.0" encoding="ISO-8859-1"?>\r\n\z 498 <!DOCTYPE NFuseProtocol SYSTEM "NFuse.dtd">\r\n\z 499 <NFuseProtocol version="5.0"><RequestReconnectSessionData><Credentials>' 500 } 501 502 if Credentials.UserName then 503 xmldata[#xmldata+1] = "<UserName>" .. Credentials.UserName .. "</UserName>" 504 end 505 506 if Credentials.Password then 507 xmldata[#xmldata+1] = '<Password encoding="cleartext">' .. Credentials.Password .. "</Password>" 508 end 509 510 if Credentials.Domain then 511 xmldata[#xmldata+1] = '<Domain type="NT">' .. Credentials.Domain .. "</Domain>" 512 end 513 514 xmldata[#xmldata+1] = "</Credentials>" 515 516 if params.ClientName then 517 xmldata[#xmldata+1] = "<ClientName>" .. params.ClientName .. "</ClientName>" 518 end 519 520 if params.DeviceId then 521 xmldata[#xmldata+1] = "<DeviceId>" .. params.DeviceId .. "</DeviceId>" 522 end 523 524 for _, srvtype in pairs(params.ServerType) do 525 xmldata[#xmldata+1] = "<ServerType>" .. srvtype .. "</ServerType>" 526 end 527 528 for _, clitype in pairs(params.ClientType) do 529 xmldata[#xmldata+1] = "<ClientType>" .. clitype .. "</ClientType>" 530 end 531 532 xmldata[#xmldata+1] = "</RequestReconnectSessionData></NFuseProtocol>\r\n" 533 534 return send_citrix_xml_request(host, port, table.concat(xmldata)) 535 536 537end 538 539return _ENV; 540