check_citrix 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431
  1. #!/usr/bin/perl -w
  2. # $Id$
  3. # $Log$
  4. # Revision 1.1 2002/11/29 12:02:00 stanleyhopcroft
  5. # New plugin to check the ICA browse service (used by Citrix Metaframe servers) from
  6. # Ed Rolison and Tom De Blende.
  7. #
  8. # Ed Rolison 15/06/02
  9. # ed@nightstalker.net
  10. # If it doesn't work, please let me know, I've only had access to my
  11. # environment so I'm not 100% sure.
  12. #
  13. # If you want to mess around with this script, then please feel free
  14. # to do so.
  15. # However, if you add anything 'funky' then I'd really appreciate
  16. # hearing about it.
  17. #
  18. # Oh, and if you do ever make huge amounts of money out of it, cut me
  19. # in :)
  20. use strict ;
  21. use IO::Socket;
  22. use IO::Select;
  23. use FileHandle;
  24. use Getopt::Long ;
  25. use vars qw($opt_H $opt_B $opt_W $opt_T $debug @citrix_servers $crit_pub_apps $warn_pub_apps $long_list);
  26. use utils qw(%ERRORS &print_revision &support &usage);
  27. my $PROGNAME = 'check_citrix' ;
  28. sub print_help ();
  29. sub print_usage ();
  30. sub help ();
  31. sub version ();
  32. delete @ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
  33. # You might have to change this...
  34. use constant PACKET_TIMEOUT => 1;
  35. # Number of seconds to wait for further UDP packets
  36. use constant TEST_COUNT => 2;
  37. # Number of datagrams sent without reply
  38. use constant BUFFER_SIZE => 1500;
  39. # buffer size used for 'recv' calls.
  40. use constant LONG_LIST => 0 ;
  41. # this is for if you have many published applications.
  42. # if you set it, it won't do any harm, but may slow the test
  43. # down a little. (Since it does a 'recv' twice instead of
  44. # once and therefore may have to wait for a timeout).
  45. use constant ICA_PORT => 1604;
  46. # what port ICA runs on. Unlikely to change.
  47. # End user config.
  48. Getopt::Long::Configure('bundling', 'no_ignore_case');
  49. GetOptions
  50. ("V|version" => \&version,
  51. "h|help" => \&help,
  52. "d|debug" => \$debug,
  53. "B|broadcast_addr:s" => \$opt_B,
  54. "C|citrix_servers:s" => \@citrix_servers,
  55. "L|long_list" => \$long_list,
  56. "P|crit_pub_apps:s" => \$crit_pub_apps,
  57. "T|Packet_timeout:i" => \$opt_T,
  58. "W|warn_pub_apps:s" => \$warn_pub_apps,
  59. ) ;
  60. # configuration section
  61. my $broadcast_addr = $1 if $opt_B and $opt_B =~ m#(\d+\.\d+\.\d+\.\d+)# ;
  62. usage("Invalid broadcast address: $opt_B\n") if $opt_B and not defined($broadcast_addr) ;
  63. usage("You must provide either the names of citrix servers or the broadcast address of the subnet containing them\n")
  64. unless (@citrix_servers or $broadcast_addr) ;
  65. my @target = defined $broadcast_addr ? ($broadcast_addr) : @citrix_servers ;
  66. usage("You must provide the names of the published applications that the Citrix browser should be advertising\n")
  67. unless $crit_pub_apps or $warn_pub_apps ;
  68. my $Timeout = $opt_T if defined $opt_T ;
  69. $Timeout = PACKET_TIMEOUT unless defined $Timeout ;
  70. $long_list = LONG_LIST unless defined $long_list ;
  71. my @crit_pub_apps = $crit_pub_apps ? split(/,/, $crit_pub_apps) : () ;
  72. my @warn_pub_apps = $warn_pub_apps ? split(/,/, $warn_pub_apps) : () ;
  73. # definitions of query strings. Change at your own risk :)
  74. # this info was gathered with tcpdump whilst trying to use an ICA client,
  75. # so I'm not 100% sure of what each value is.
  76. my @bcast_helo = &tethereal2list(<<'End_of_Tethereal_trace', '1e') ;
  77. 0020 ff ff 04 d6 06 44 00 26 4a 76 1e 00 01 30 02 fd .....D.&Jv...0..
  78. 0030 a8 e3 00 02 f5 95 9f f5 30 07 00 00 00 00 00 00 ........0.......
  79. 0040 00 00 00 00 00 00 01 00
  80. End_of_Tethereal_trace
  81. my @bcast_query_app = &tethereal2list(<<'End_of_Tethereal_trace', '24') ;
  82. 0020 64 17 04 50 06 44 00 2c 85 6a 24 00 01 32 02 fd d..P.D.,.j$..2..
  83. 0030 a8 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
  84. 0040 00 00 00 00 00 00 21 00 02 00 00 00 00 00 ......!......
  85. End_of_Tethereal_trace
  86. my @direct_helo = &tethereal2list(<<'End_of_Tethereal_trace', '20') ;
  87. 0020 64 17 05 0f 06 44 00 28 ab b5 20 00 01 30 02 fd d....D.(.. ..0..
  88. 0030 a8 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
  89. 0040 00 00 00 00 00 00 00 00 00 00
  90. End_of_Tethereal_trace
  91. my @direct_query_app = &tethereal2list(<<'End_of_Tethereal_trace', '2c') ;
  92. 0020 64 17 05 10 06 44 00 34 7a 9a 2c 00 02 32 02 fd d....D.4z.,..2..
  93. 0030 a8 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
  94. 0040 00 00 00 00 00 00 21 00 02 00 01 00 00 00 00 00 ......!.........
  95. 0050 00 00 00 00 00 00
  96. End_of_Tethereal_trace
  97. my $Udp = IO::Socket::INET->new( Proto => 'udp' ) || die "Socket failure: $!";
  98. # select is here to allow us to set timeouts on the connections. Otherwise they
  99. # just 'stop' until a server appears.
  100. my $select = IO::Select->new($Udp) || die "Select failure: $!";
  101. # helo needs to be broadcast, but query does not.
  102. $Udp->sockopt(SO_BROADCAST, 1 );
  103. $Udp->autoflush(1);
  104. my ($remote_host, $buff, $buff2, $raddr, $rport, $rhost, @remote_response);
  105. my (@query_message, $send_addr, $this_test) ;
  106. $buff = $buff2 = '';
  107. $this_test = 0;
  108. # If there is no response to the first helo packet it will be resent
  109. # up to TEST_COUNT (see at the top).
  110. while ( ++$this_test <= TEST_COUNT && !$buff ) {
  111. print "Sending helo datagram. datagram number: ", $this_test, "\n" if $debug ;
  112. # if we have multiple targets, we probe each of them until we get a
  113. # response...
  114. foreach my $destination (@target) {
  115. @query_message = ( $broadcast_addr ? @bcast_helo : @direct_helo) ;
  116. print "Querying $destination for master browser\n" if $debug ;
  117. $send_addr = sockaddr_in(ICA_PORT, inet_aton($destination) );
  118. &dump(pack('C*', @query_message)) if $debug ;
  119. $Udp->send( pack('C*', @query_message), 0, $send_addr );
  120. if ( $select->can_read($Timeout) ) {
  121. $remote_host = $Udp->recv($buff, BUFFER_SIZE, 0 );
  122. }
  123. last if $buff ;
  124. sleep 1 ;
  125. } # foreach destination
  126. } # while loop
  127. # ok we've looped several times, looking for a response. If we don't have one
  128. # yet, we simply mark the whole lot as being unavailable.
  129. unless ( $buff ) {
  130. print "Failed. No response to helo datagram (master browser query) from ", $broadcast_addr ? $broadcast_addr : "@citrix_servers", ".\n" ;
  131. exit $ERRORS{CRITICAL} ;
  132. }
  133. ($rport, $raddr) = sockaddr_in( $remote_host );
  134. $rhost = gethostbyaddr( $raddr, AF_INET );
  135. my @tmpbuf = unpack('C*', $buff );
  136. if ( $debug ) {
  137. print "$rhost:$rport responded with: ",length($buff), " bytes\n";
  138. &dump($buff) ;
  139. } #if debug
  140. # now we have a response, then we need to figure out the master browser, and
  141. # query it for published applications...
  142. my $master_browser = join '.', @tmpbuf[32..35] ;
  143. # ok should probably error check this, because it's remotely possible
  144. # that a server response might be completely wrong...
  145. print "Master browser = $master_browser\n" if $debug ;
  146. $send_addr = sockaddr_in(ICA_PORT, inet_aton($master_browser));
  147. if ( $broadcast_addr ) {
  148. print "using broadcast query\n" if $debug ;
  149. @query_message = @bcast_query_app;
  150. } else {
  151. print "using directed query\n" if $debug ;
  152. @query_message = @direct_query_app;
  153. }
  154. # now we send the appropriate query string, to the master browser we've found.
  155. $buff = '';
  156. $this_test = 0 ;
  157. print "Querying master browser for published application list\n" if $debug ;
  158. while ( ++$this_test <= TEST_COUNT && !$buff ) {
  159. print "Sending application query datagram. datagram number: ", $this_test, "\n" if $debug ;
  160. &dump(pack('C*', @query_message)) if $debug ;
  161. $Udp->send( pack ('C*', @query_message), 0, $send_addr );
  162. if ( $select->can_read($Timeout) ) {
  163. $remote_host = $Udp->recv($buff, BUFFER_SIZE, 0 );
  164. # $buff = substr($buff, 32) ;
  165. # Hope that ICA preamble is first 32 bytes
  166. }
  167. # long application lists are delivered in multiple packets
  168. my $buff2 = '' ;
  169. while ( $long_list && $select->can_read($Timeout) ) {
  170. $remote_host = $Udp->recv($buff2, BUFFER_SIZE, 0 );
  171. $buff .= $buff2 if $buff2 ;
  172. # $buff .= substr($buff2, 32) if $buff2 ;
  173. # Hope that ICA preamble is first 32 bytes
  174. }
  175. last if $buff ;
  176. sleep 1 ;
  177. } # while test_count
  178. unless ( $buff ) {
  179. print "Failed. No response to application query datagram from ", $master_browser, ".\n" ;
  180. exit $ERRORS{CRITICAL} ;
  181. }
  182. # we got a response from a couple of retries of the app query
  183. ($rport, $raddr) = sockaddr_in ( $remote_host );
  184. $rhost = gethostbyaddr ( $raddr, AF_INET );
  185. if ( $debug ) {
  186. print "$rhost:$rport responded to app query with: ",length($buff), " bytes\n";
  187. &dump($buff) ;
  188. } #debug
  189. my $app_list = $buff ;
  190. # delete nulls in unicode
  191. # but only if there is unicode (usually from
  192. # broadcast query)
  193. $app_list =~ s/(?:(\w| |-)\x00)/$1/g
  194. if $app_list =~ /(?:(?:(?:\w| |-)\x00){3,})/ ;
  195. # FIXME an application name is
  196. # 3 or more unicoded characters
  197. # FIXME locale
  198. # extract null terminated strings
  199. my (@clean_app_list, $clean_app_list) ;
  200. $clean_app_list = join(',', @clean_app_list = $app_list =~ m#([A-Za-z](?:\w| |-|[ÄÖÜäöüß])+?(?=\x00))#g ) ;
  201. # patch for German umlauts et al from Herr Mike Gerber.
  202. # $clean_app_list = join(',', @clean_app_list = $app_list =~ m#([A-Z](?:\w| |-)+?(?=\x00))#g ) ;
  203. # FIXME everyones apps don't start with caps
  204. print qq(Received list of applications: "$clean_app_list".\n) if $debug ;
  205. if ( scalar @crit_pub_apps and my @missing = &simple_diff(\@clean_app_list, \@crit_pub_apps) ) {
  206. print qq(Failed. "@missing" not found in list of published applications),
  207. qq( "$clean_app_list" from master browser "$master_browser".\n) ;
  208. exit $ERRORS{CRITICAL} ;
  209. }
  210. if ( my @missing = &simple_diff(\@clean_app_list, \@warn_pub_apps) ) {
  211. print qq(Warning. "@missing" not found in list of published applications),
  212. qq( "$clean_app_list" from master browser "$master_browser".\n) ;
  213. exit $ERRORS{WARNING} ;
  214. }
  215. my @x = (@crit_pub_apps, @warn_pub_apps) ;
  216. my $blah = ( scalar(@x) == 1 ? 'the published application "' . join(',', @x) . '" is available' :
  217. 'the published applications "' . join(',', @x) . '" are available' ) ;
  218. print qq(Ok. Citrix master browser "$master_browser" reported that $blah.\n) ;
  219. exit $ERRORS{OK} ;
  220. # sleep $Timeout;
  221. # because otherwise we can get responses from
  222. # the WRONG servers. DOH
  223. close $Udp;
  224. sub print_usage () {
  225. print "Usage: $PROGNAME (-B <broadcast_address>| -C <citrix_server>..) -W <pub_app1,pub_app2..> -P <pub_app1,pub_app2,>\n";
  226. }
  227. sub print_help () {
  228. print_revision($PROGNAME,'$Revision$ ');
  229. print "Copyright (c) 2002 Ed Rolison/Tom De Blende/Karl DeBisschop/S Hopcroft
  230. Perl Check Citrix plugin for NetSaint.
  231. Returns OK if the Citrix master browser returns a 'published application' list that contain names specified by the -W or -P options
  232. The plugin works by
  233. If the -B option is specified, sending a broadcast helo to find the address of the Citrix master browser in the specified subnet.
  234. return critical if there is no reply;
  235. Else if the -C option is specified
  236. send a direct helo to the specified server until there is a response (containing the address of the Citrix master browser)
  237. Query the master browser (using a 'broadcast published applications query ' if -B) and compare the published applications returned
  238. to those specified by -W and -P options
  239. return Critical if the published applications specified by -P is not a subset of the query responses;
  240. return Warning if the published applications specified by -W is not a subset of the query responses;
  241. return OK
  242. ";
  243. print_usage();
  244. print '
  245. -B, --broadcast_address=STRING
  246. The broadcast address that should contain Citrix master browser. This option takes precedence over -C.
  247. -C, --citrix_server:STRING
  248. Optional __name(s)__ of Citrix servers that could be the master browser (used when broadcast not possible).
  249. -L, --long_list
  250. Set this if you have heaps of published applications (ie more than will fit in _one_ UDP packet)
  251. -P, --crit_published_app=STRING
  252. Optional comma separated list of published application that must be in the response from the master browser.
  253. Check returns critical otherwise.
  254. -T, --packet-timeout:INTEGER
  255. Time to wait for UDP packets (default 1 sec).
  256. -W, --warn_published_app=STRING
  257. Optional comma separated list of published application that should be in the response from the master browser.
  258. Check returns warning otherwise.
  259. -d, --debug
  260. Debugging output.
  261. -h, --help
  262. This stuff.
  263. ';
  264. support();
  265. }
  266. sub version () {
  267. print_revision($PROGNAME,'$Revision$ ');
  268. exit $ERRORS{'OK'};
  269. }
  270. sub help () {
  271. print_help();
  272. exit $ERRORS{'OK'};
  273. }
  274. sub dump {
  275. my ($x) = shift @_ ;
  276. my (@x, @y, $y, $i, $rowcount) ;
  277. my ($nr, $j, $number_in_row, $number_of_bytes) ;
  278. my $dump ;
  279. $number_in_row = 16 ;
  280. $number_of_bytes = length $x ;
  281. $nr = 0 ;
  282. # styled on tethereal.
  283. foreach $j (1 .. int( $number_of_bytes / $number_in_row) ) {
  284. $y = substr($x, ($j - 1)*$number_in_row, $number_in_row) ;
  285. @y = unpack("C*", $y) ;
  286. $y =~ tr /\x00-\x19/./ ;
  287. $rowcount = sprintf("%4.4x", ($j - 1) * 0x10 ) ;
  288. $dump .= sprintf "%s %s %s\n", $rowcount, join(" ", map { sprintf "%2.2x", $_} @y), $y ;
  289. $nr++ ;
  290. }
  291. if ( $number_of_bytes % $number_in_row > 0 ) {
  292. my $spaces_to_text = $number_in_row * 3 - 1 + 3 ;
  293. $rowcount = sprintf("%4.4x", $nr * 0x10 ) ;
  294. $y = substr($x, $nr * $number_in_row ) ;
  295. @y = unpack("C*", $y) ;
  296. my $bytes = join(" ", map { sprintf "%2.2x", $_} @y) ;
  297. my $spaces = ' ' x ($spaces_to_text - length($bytes)) ;
  298. $dump .= sprintf "%s %s%s%s\n", $rowcount, $bytes, $spaces, $y ;
  299. }
  300. print $dump, "\n" ;
  301. }
  302. sub tethereal2list {
  303. my ($tethereal_dump, $start_byte) = @_ ;
  304. # return an array containing qw(0xef 0xab 0x00 ...) from a tethereal trace.
  305. # skip all stuff until the first byte given by $start_byte.
  306. return undef unless $tethereal_dump =~ /\d\d\d\d \S\S(?: \S\S){1,15}/ ;
  307. my $hex_start_byte = hex($start_byte) ;
  308. my @x = $tethereal_dump =~ m#(.+)#g ;
  309. my @y = map unpack("x6 a47", $_), @x ;
  310. my @z = map { my $y = $_; $y =~ s/(\S\S)/hex($1)/eg; my @a = split(' ', $y); @a } @y ;
  311. shift @z, while $z[0] ne $hex_start_byte ;
  312. @z ;
  313. }
  314. sub simple_diff {
  315. my ( $a_list, $b_list) = @_ ;
  316. # simple set difference 'Recipe 4.7 Perl Cookbook', Christiansen and Torkington
  317. my (%seen, @missing) ;
  318. @seen{@$a_list} = () ;
  319. foreach my $item (@$b_list) {
  320. push @missing, $item unless exists $seen{$item} ;
  321. }
  322. @missing ;
  323. }