| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- #!/usr/bin/perl -w
- # $Id$
- # $Log$
- # Revision 1.1 2002/11/29 12:02:00 stanleyhopcroft
- # New plugin to check the ICA browse service (used by Citrix Metaframe servers) from
- # Ed Rolison and Tom De Blende.
- #
- # Ed Rolison 15/06/02
- # ed@nightstalker.net
- # If it doesn't work, please let me know, I've only had access to my
- # environment so I'm not 100% sure.
- #
- # If you want to mess around with this script, then please feel free
- # to do so.
- # However, if you add anything 'funky' then I'd really appreciate
- # hearing about it.
- #
- # Oh, and if you do ever make huge amounts of money out of it, cut me
- # in :)
- use strict ;
- use IO::Socket;
- use IO::Select;
- use FileHandle;
- use Getopt::Long ;
- use vars qw($opt_H $opt_B $opt_W $opt_T $debug @citrix_servers $crit_pub_apps $warn_pub_apps $long_list);
- use utils qw(%ERRORS &print_revision &support &usage);
- my $PROGNAME = 'check_citrix' ;
- sub print_help ();
- sub print_usage ();
- sub help ();
- sub version ();
- delete @ENV{'PATH', 'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
- # You might have to change this...
- use constant PACKET_TIMEOUT => 1;
- # Number of seconds to wait for further UDP packets
- use constant TEST_COUNT => 2;
- # Number of datagrams sent without reply
- use constant BUFFER_SIZE => 1500;
- # buffer size used for 'recv' calls.
- use constant LONG_LIST => 0 ;
- # this is for if you have many published applications.
- # if you set it, it won't do any harm, but may slow the test
- # down a little. (Since it does a 'recv' twice instead of
- # once and therefore may have to wait for a timeout).
- use constant ICA_PORT => 1604;
- # what port ICA runs on. Unlikely to change.
- # End user config.
- Getopt::Long::Configure('bundling', 'no_ignore_case');
- GetOptions
- ("V|version" => \&version,
- "h|help" => \&help,
- "d|debug" => \$debug,
- "B|broadcast_addr:s" => \$opt_B,
- "C|citrix_servers:s" => \@citrix_servers,
- "L|long_list" => \$long_list,
- "P|crit_pub_apps:s" => \$crit_pub_apps,
- "T|Packet_timeout:i" => \$opt_T,
- "W|warn_pub_apps:s" => \$warn_pub_apps,
- ) ;
- # configuration section
- my $broadcast_addr = $1 if $opt_B and $opt_B =~ m#(\d+\.\d+\.\d+\.\d+)# ;
- usage("Invalid broadcast address: $opt_B\n") if $opt_B and not defined($broadcast_addr) ;
- usage("You must provide either the names of citrix servers or the broadcast address of the subnet containing them\n")
- unless (@citrix_servers or $broadcast_addr) ;
- my @target = defined $broadcast_addr ? ($broadcast_addr) : @citrix_servers ;
- usage("You must provide the names of the published applications that the Citrix browser should be advertising\n")
- unless $crit_pub_apps or $warn_pub_apps ;
- my $Timeout = $opt_T if defined $opt_T ;
- $Timeout = PACKET_TIMEOUT unless defined $Timeout ;
- $long_list = LONG_LIST unless defined $long_list ;
- my @crit_pub_apps = $crit_pub_apps ? split(/,/, $crit_pub_apps) : () ;
- my @warn_pub_apps = $warn_pub_apps ? split(/,/, $warn_pub_apps) : () ;
- # definitions of query strings. Change at your own risk :)
- # this info was gathered with tcpdump whilst trying to use an ICA client,
- # so I'm not 100% sure of what each value is.
- my @bcast_helo = &tethereal2list(<<'End_of_Tethereal_trace', '1e') ;
- 0020 ff ff 04 d6 06 44 00 26 4a 76 1e 00 01 30 02 fd .....D.&Jv...0..
- 0030 a8 e3 00 02 f5 95 9f f5 30 07 00 00 00 00 00 00 ........0.......
- 0040 00 00 00 00 00 00 01 00
- End_of_Tethereal_trace
- my @bcast_query_app = &tethereal2list(<<'End_of_Tethereal_trace', '24') ;
- 0020 64 17 04 50 06 44 00 2c 85 6a 24 00 01 32 02 fd d..P.D.,.j$..2..
- 0030 a8 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 0040 00 00 00 00 00 00 21 00 02 00 00 00 00 00 ......!......
- End_of_Tethereal_trace
- my @direct_helo = &tethereal2list(<<'End_of_Tethereal_trace', '20') ;
- 0020 64 17 05 0f 06 44 00 28 ab b5 20 00 01 30 02 fd d....D.(.. ..0..
- 0030 a8 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 0040 00 00 00 00 00 00 00 00 00 00
- End_of_Tethereal_trace
- my @direct_query_app = &tethereal2list(<<'End_of_Tethereal_trace', '2c') ;
- 0020 64 17 05 10 06 44 00 34 7a 9a 2c 00 02 32 02 fd d....D.4z.,..2..
- 0030 a8 e3 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 0040 00 00 00 00 00 00 21 00 02 00 01 00 00 00 00 00 ......!.........
- 0050 00 00 00 00 00 00
- End_of_Tethereal_trace
- my $Udp = IO::Socket::INET->new( Proto => 'udp' ) || die "Socket failure: $!";
- # select is here to allow us to set timeouts on the connections. Otherwise they
- # just 'stop' until a server appears.
- my $select = IO::Select->new($Udp) || die "Select failure: $!";
- # helo needs to be broadcast, but query does not.
- $Udp->sockopt(SO_BROADCAST, 1 );
- $Udp->autoflush(1);
- my ($remote_host, $buff, $buff2, $raddr, $rport, $rhost, @remote_response);
- my (@query_message, $send_addr, $this_test) ;
- $buff = $buff2 = '';
- $this_test = 0;
- # If there is no response to the first helo packet it will be resent
- # up to TEST_COUNT (see at the top).
- while ( ++$this_test <= TEST_COUNT && !$buff ) {
- print "Sending helo datagram. datagram number: ", $this_test, "\n" if $debug ;
- # if we have multiple targets, we probe each of them until we get a
- # response...
- foreach my $destination (@target) {
- @query_message = ( $broadcast_addr ? @bcast_helo : @direct_helo) ;
- print "Querying $destination for master browser\n" if $debug ;
- $send_addr = sockaddr_in(ICA_PORT, inet_aton($destination) );
- &dump(pack('C*', @query_message)) if $debug ;
- $Udp->send( pack('C*', @query_message), 0, $send_addr );
- if ( $select->can_read($Timeout) ) {
- $remote_host = $Udp->recv($buff, BUFFER_SIZE, 0 );
- }
- last if $buff ;
- sleep 1 ;
- } # foreach destination
- } # while loop
- # ok we've looped several times, looking for a response. If we don't have one
- # yet, we simply mark the whole lot as being unavailable.
- unless ( $buff ) {
- print "Failed. No response to helo datagram (master browser query) from ", $broadcast_addr ? $broadcast_addr : "@citrix_servers", ".\n" ;
- exit $ERRORS{CRITICAL} ;
- }
- ($rport, $raddr) = sockaddr_in( $remote_host );
- $rhost = gethostbyaddr( $raddr, AF_INET );
- my @tmpbuf = unpack('C*', $buff );
- if ( $debug ) {
- print "$rhost:$rport responded with: ",length($buff), " bytes\n";
- &dump($buff) ;
- } #if debug
- # now we have a response, then we need to figure out the master browser, and
- # query it for published applications...
- my $master_browser = join '.', @tmpbuf[32..35] ;
-
- # ok should probably error check this, because it's remotely possible
- # that a server response might be completely wrong...
-
- print "Master browser = $master_browser\n" if $debug ;
- $send_addr = sockaddr_in(ICA_PORT, inet_aton($master_browser));
- if ( $broadcast_addr ) {
- print "using broadcast query\n" if $debug ;
- @query_message = @bcast_query_app;
- } else {
- print "using directed query\n" if $debug ;
- @query_message = @direct_query_app;
- }
-
- # now we send the appropriate query string, to the master browser we've found.
- $buff = '';
- $this_test = 0 ;
- print "Querying master browser for published application list\n" if $debug ;
- while ( ++$this_test <= TEST_COUNT && !$buff ) {
- print "Sending application query datagram. datagram number: ", $this_test, "\n" if $debug ;
- &dump(pack('C*', @query_message)) if $debug ;
- $Udp->send( pack ('C*', @query_message), 0, $send_addr );
- if ( $select->can_read($Timeout) ) {
- $remote_host = $Udp->recv($buff, BUFFER_SIZE, 0 );
- # $buff = substr($buff, 32) ;
- # Hope that ICA preamble is first 32 bytes
- }
- # long application lists are delivered in multiple packets
-
- my $buff2 = '' ;
- while ( $long_list && $select->can_read($Timeout) ) {
- $remote_host = $Udp->recv($buff2, BUFFER_SIZE, 0 );
- $buff .= $buff2 if $buff2 ;
- # $buff .= substr($buff2, 32) if $buff2 ;
- # Hope that ICA preamble is first 32 bytes
- }
- last if $buff ;
- sleep 1 ;
- } # while test_count
- unless ( $buff ) {
- print "Failed. No response to application query datagram from ", $master_browser, ".\n" ;
- exit $ERRORS{CRITICAL} ;
- }
- # we got a response from a couple of retries of the app query
- ($rport, $raddr) = sockaddr_in ( $remote_host );
- $rhost = gethostbyaddr ( $raddr, AF_INET );
- if ( $debug ) {
- print "$rhost:$rport responded to app query with: ",length($buff), " bytes\n";
- &dump($buff) ;
- } #debug
- my $app_list = $buff ;
- # delete nulls in unicode
- # but only if there is unicode (usually from
- # broadcast query)
- $app_list =~ s/(?:(\w| |-)\x00)/$1/g
- if $app_list =~ /(?:(?:(?:\w| |-)\x00){3,})/ ;
- # FIXME an application name is
- # 3 or more unicoded characters
- # FIXME locale
- # extract null terminated strings
- my (@clean_app_list, $clean_app_list) ;
- $clean_app_list = join(',', @clean_app_list = $app_list =~ m#([A-Za-z](?:\w| |-|[ÄÖÜäöüß])+?(?=\x00))#g ) ;
- # patch for German umlauts et al from Herr Mike Gerber.
- # $clean_app_list = join(',', @clean_app_list = $app_list =~ m#([A-Z](?:\w| |-)+?(?=\x00))#g ) ;
- # FIXME everyones apps don't start with caps
- print qq(Received list of applications: "$clean_app_list".\n) if $debug ;
- if ( scalar @crit_pub_apps and my @missing = &simple_diff(\@clean_app_list, \@crit_pub_apps) ) {
- print qq(Failed. "@missing" not found in list of published applications),
- qq( "$clean_app_list" from master browser "$master_browser".\n) ;
- exit $ERRORS{CRITICAL} ;
- }
- if ( my @missing = &simple_diff(\@clean_app_list, \@warn_pub_apps) ) {
- print qq(Warning. "@missing" not found in list of published applications),
- qq( "$clean_app_list" from master browser "$master_browser".\n) ;
- exit $ERRORS{WARNING} ;
- }
- my @x = (@crit_pub_apps, @warn_pub_apps) ;
- my $blah = ( scalar(@x) == 1 ? 'the published application "' . join(',', @x) . '" is available' :
- 'the published applications "' . join(',', @x) . '" are available' ) ;
-
- print qq(Ok. Citrix master browser "$master_browser" reported that $blah.\n) ;
- exit $ERRORS{OK} ;
- # sleep $Timeout;
- # because otherwise we can get responses from
- # the WRONG servers. DOH
- close $Udp;
- sub print_usage () {
- print "Usage: $PROGNAME (-B <broadcast_address>| -C <citrix_server>..) -W <pub_app1,pub_app2..> -P <pub_app1,pub_app2,>\n";
- }
- sub print_help () {
- print_revision($PROGNAME,'$Revision$ ');
- print "Copyright (c) 2002 Ed Rolison/Tom De Blende/Karl DeBisschop/S Hopcroft
- Perl Check Citrix plugin for NetSaint.
- Returns OK if the Citrix master browser returns a 'published application' list that contain names specified by the -W or -P options
- The plugin works by
- If the -B option is specified, sending a broadcast helo to find the address of the Citrix master browser in the specified subnet.
- return critical if there is no reply;
- Else if the -C option is specified
- send a direct helo to the specified server until there is a response (containing the address of the Citrix master browser)
- Query the master browser (using a 'broadcast published applications query ' if -B) and compare the published applications returned
- to those specified by -W and -P options
- return Critical if the published applications specified by -P is not a subset of the query responses;
- return Warning if the published applications specified by -W is not a subset of the query responses;
- return OK
- ";
- print_usage();
- print '
- -B, --broadcast_address=STRING
- The broadcast address that should contain Citrix master browser. This option takes precedence over -C.
- -C, --citrix_server:STRING
- Optional __name(s)__ of Citrix servers that could be the master browser (used when broadcast not possible).
- -L, --long_list
- Set this if you have heaps of published applications (ie more than will fit in _one_ UDP packet)
- -P, --crit_published_app=STRING
- Optional comma separated list of published application that must be in the response from the master browser.
- Check returns critical otherwise.
- -T, --packet-timeout:INTEGER
- Time to wait for UDP packets (default 1 sec).
- -W, --warn_published_app=STRING
- Optional comma separated list of published application that should be in the response from the master browser.
- Check returns warning otherwise.
- -d, --debug
- Debugging output.
- -h, --help
- This stuff.
- ';
- support();
- }
- sub version () {
- print_revision($PROGNAME,'$Revision$ ');
- exit $ERRORS{'OK'};
- }
- sub help () {
- print_help();
- exit $ERRORS{'OK'};
- }
- sub dump {
- my ($x) = shift @_ ;
- my (@x, @y, $y, $i, $rowcount) ;
- my ($nr, $j, $number_in_row, $number_of_bytes) ;
- my $dump ;
- $number_in_row = 16 ;
- $number_of_bytes = length $x ;
- $nr = 0 ;
- # styled on tethereal.
- foreach $j (1 .. int( $number_of_bytes / $number_in_row) ) {
- $y = substr($x, ($j - 1)*$number_in_row, $number_in_row) ;
- @y = unpack("C*", $y) ;
- $y =~ tr /\x00-\x19/./ ;
- $rowcount = sprintf("%4.4x", ($j - 1) * 0x10 ) ;
- $dump .= sprintf "%s %s %s\n", $rowcount, join(" ", map { sprintf "%2.2x", $_} @y), $y ;
- $nr++ ;
- }
- if ( $number_of_bytes % $number_in_row > 0 ) {
- my $spaces_to_text = $number_in_row * 3 - 1 + 3 ;
- $rowcount = sprintf("%4.4x", $nr * 0x10 ) ;
- $y = substr($x, $nr * $number_in_row ) ;
- @y = unpack("C*", $y) ;
- my $bytes = join(" ", map { sprintf "%2.2x", $_} @y) ;
- my $spaces = ' ' x ($spaces_to_text - length($bytes)) ;
- $dump .= sprintf "%s %s%s%s\n", $rowcount, $bytes, $spaces, $y ;
- }
- print $dump, "\n" ;
-
- }
- sub tethereal2list {
- my ($tethereal_dump, $start_byte) = @_ ;
- # return an array containing qw(0xef 0xab 0x00 ...) from a tethereal trace.
- # skip all stuff until the first byte given by $start_byte.
- return undef unless $tethereal_dump =~ /\d\d\d\d \S\S(?: \S\S){1,15}/ ;
- my $hex_start_byte = hex($start_byte) ;
- my @x = $tethereal_dump =~ m#(.+)#g ;
- my @y = map unpack("x6 a47", $_), @x ;
- my @z = map { my $y = $_; $y =~ s/(\S\S)/hex($1)/eg; my @a = split(' ', $y); @a } @y ;
- shift @z, while $z[0] ne $hex_start_byte ;
- @z ;
- }
- sub simple_diff {
- my ( $a_list, $b_list) = @_ ;
- # simple set difference 'Recipe 4.7 Perl Cookbook', Christiansen and Torkington
- my (%seen, @missing) ;
- @seen{@$a_list} = () ;
- foreach my $item (@$b_list) {
- push @missing, $item unless exists $seen{$item} ;
- }
- @missing ;
- }
|