#!/usr/bin/perl ##Copyright (C) 2007 Fonality Inc. (fonality.com) # #This program is free software; you can redistribute it and/or #modify it under the terms of the GNU General Public License #as published by the Free Software Foundation; either version 2 #of the License, or (at your option) any later version. # #This program is distributed in the hope that it will be useful, #but WITHOUT ANY WARRANTY; without even the implied warranty of #MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #GNU General Public License for more details. # use strict; use Crypt::CBC; use LWP::UserAgent; use LWP::Simple; use HTTP::Request::Common; use IPC::Open3 qw/open3/; use MIME::Base64 qw/encode_base64/; use DB_File; use Digest::MD5 qw/md5_hex/; use Getopt::Long qw/GetOptions/; use File::Spec; my $VERSION = 9; #Increment to force clients to attempt to update themselves. my $INSTALL_PATH = "/var/adm/bin"; my $CACHE_DB = "$INSTALL_PATH/registry_cache.db"; INSTALL_CRON_JOB(); #If we aren't already in the cron, install ourselves. # This file is design to be executed regularly by an external controller such as cron. # It retrieves a list of commands to be executed from the specified URI and executes them, saving the output # and returning it to the webserver as an encrypted string. my $remote_host; if (-e "/etc/asterisk/fonality/globals.conf") { my $server_id = get_server_id(); if ($server_id >= 100000) { $remote_host = "proregistry.trixbox.com"; } else { $remote_host = "update.fonality.com"; } } else { $remote_host = "registry.trixbox.com"; } my $server_uri = "http://$remote_host/server_monitor.cgi"; my $update_uri = "http://$remote_host/"; my $server_id = get_server_id(); warn "SENDING SERVER ID: '$server_id'\n"; ############################################################################### # Fetch commands to run. my $ua = LWP::UserAgent->new; my $resp = fetch_server_commands( $ua ); my $key = get_key(); chomp $key; my $cipher = Crypt::CBC->new( -cipher => "Blowfish", -key => $key ); my @commands = split /\n/, $cipher->decrypt( $resp->content ); ############################################################################### # Check our version. my $expected_version = shift @commands; if( $expected_version > $VERSION ) { warn "Trying to update: $update_uri/recognition.pl\n"; my $update_resp = $ua->get("$update_uri/recognition.pl"); if( $update_resp->is_success ) { open my $fh, "> $INSTALL_PATH/tmp_recognition.pl" or die $!; print $fh $update_resp->content; close $fh; rename "$INSTALL_PATH/tmp_recognition.pl", "$INSTALL_PATH/recognition.pl" or warn "ERROR couldn't replace old recognition with new one: $!\n"; } else { warn "ERROr, couldn't fetch recognition.pl ", $update_resp->status_line; } $update_resp = $ua->get("$update_uri/registry.pl"); if( $update_resp->is_success ) { my ( $fname ) = (File::Spec->splitpath($0))[-1]; open my $fh, "> $INSTALL_PATH/tmp_$fname" or die "Failed to open $INSTALL_PATH/tmp_$fname: $!"; print $fh $update_resp->content; close $fh; rename "$INSTALL_PATH/tmp_$fname", "$INSTALL_PATH/$fname"; exec "perl", "$INSTALL_PATH/$fname"; exit; #If the exec failed, cleanly die. } else { die "Error, version is out of date and I could not fetch an update.\n"; } } ############################################################################### # Check if we've been assigned a new server-id (for CE servers only) my $new_id = shift @commands; if( length $new_id > 1 and $new_id ne $server_id ) { $server_id = $new_id; store_generated_id( $new_id ); #SERVER_ID\nSERVER_KEY #The key is only there if our ID has changed.. my $new_key = shift @commands; store_generated_key( $new_key ); $cipher = Crypt::CBC->new( -cipher => "Blowfish", -key => $new_key ); #Remove cache file, since we should think we're now running on a brand new server. unlink $CACHE_DB; } ############################################################################### # Execute commands. my @command_outputs; for( @commands ) { my $pid = open3 my $write_handle, my $read_handle, my $err_handle, $_; close $write_handle; chomp( my @output = <$read_handle> ); chomp( my @errors = <$err_handle> ); waitpid $pid, 0; my $child_return_value = $?; my $success = 1; #Default to succeeded. if( $output[0] =~ /open3:/ ) #This matches the exceptions thrown by open3. Example, open3: Failed to exec in child.. { @errors = @output; @output = (); $success = 0; } print "Cmd: $_\n"; print "Received ", scalar @output, " lines of output\n"; print "\t-- '$output[0]'\n"; print "Received ", scalar @errors, " lines of errors\n\n"; push @command_outputs, { stdout => ( join "\n", @output ), stderr => ( join "\n", @errors ), return_value => $child_return_value, succeeded => $success, command => $_, }; } ############################################################################### # Generate report structure. my @xml_outputs; tie my %cached_output, 'DB_File', $CACHE_DB or die $!; for( @command_outputs ) { # my $stdout = encode_base64 join "\n", @{ $_->{stdout} }; # my $stderr = encode_base64 join "\n", @{ $_->{stderr} }; # my $command = encode_base64 $_->{command}; for( values %$_ ) { $_ = encode_base64( $_ ) ; } my $xml = " $_->{command} $_->{stdout} $_->{stderr} $_->{return_value} $_->{succeeded} " ; my $checksum = md5_hex( $xml ); if( $cached_output{ $_->{command} } ne $checksum ) { push @xml_outputs, { encrypted => $cipher->encrypt( $xml ), command => $_->{command}, xml => $xml }; } } ############################################################################### # Upload report. if( @xml_outputs ) { my $post = POST $server_uri, Content_Type => 'form-data', Content => [ m => 'receive_results', server_id => $server_id, map {; "command$_" => [ undef, "command$_", Content => $xml_outputs[$_]->{encrypted} ] } #Note, the semicolon is in fact required. 0 .. $#xml_outputs, ] ; #Actually send request. my $resp = $ua->request( $post ); ############################################################################ # Successful upload, update our cachefile if( $resp->is_success ) { for( @xml_outputs ) { my $checksum = md5_hex( $_->{xml} ); $cached_output{ $_->{command} } = $checksum; } } } ################################################################################ # Begin subs. ################################################################################ sub fetch_server_commands { my( $ua ) = @_; my $resp = $ua->get("$server_uri?m=fetch_commands&sid=$server_id"); warn "$server_uri?m=fetch_commands&sid=$server_id\n"; if( $resp->is_error ) { die "The request to retrieve commands to execute has errored with the following status: ", $resp->code, " -- exiting.\n"; } elsif( $resp->content =~ /ERROR:/ ) { if( $resp->content =~ /invalid server id/ ) { $server_id = server_generate_id(); return fetch_server_commands( $ua ); } } return $resp; } sub get_server_id { my $id = get_globals_user_id(); if( $id ) { return $id; } $id = get_generated_id(); if( $id ) { return $id; } die "Error: Couldn't find or generate a server id\n"; } sub get_globals_user_id { open my $fh, "/etc/asterisk/fonality/globals.conf" or return; while( <$fh> ) { if( /ACCOUNT=(\S+)/ ) { return $1; } } } sub get_generated_id { if( -e "/etc/trixbox/monitor_server_id" ) { open my $fh, "/etc/trixbox/monitor_server_id" or die; #FIXME shouldn't die chomp( my $id = <$fh> ); if( $id ) { return $id; } } return server_generate_id(); } sub server_generate_id { my $key_id = get( "$server_uri?m=generate_id" ); warn "RETURN VALUE: '$key_id'\n"; my( $id, $key ) = split /\n/, $key_id; store_generated_id( $id ); store_generated_key( $key ); return $id; } sub store_generated_id { my( $id ) = @_; warn "STORING ID -- $id\n"; open my $fh, "> /etc/trixbox/monitor_server_id" or die "Error, couldn't open /etc/trixbox/monitor_server_id: $!\n"; print $fh $id; } sub store_generated_key { my( $key ) = @_; mkdir "/etc/trixbox"; warn "STORING KEY -- $key\n"; open my $fh, "> /etc/trixbox/monitor_server_key" or die "Error, couldn't open /etc/trixbox/monitor_server_key: $!\n"; print $fh $key; } sub get_key { my $file; for( qw{ /etc/asterisk/key /etc/trixbox/monitor_server_key } ) { if( -e $_ ) { $file = $_; last; } } open my $fh, "< $file" or die "Error couldn't open keyfile $file: $!\n"; my $key = <$fh>; chomp $key; return $key; } sub INSTALL_CRON_JOB { my $fn = "/tmp/newcron.$$"; system "crontab -l > $fn"; open my $fh, "< $fn" or die $!; while( <$fh> ) { if( /registry.pl/ ) { unlink $fn; return; } } #We didn't exit early.. close $fh; open $fh, ">> $fn" or die $!; my ($minute,$hour ) = (int(rand(60)), int(rand(24))); print $fh "$minute $hour * * * perl $INSTALL_PATH/registry.pl &> /dev/null \n"; close $fh; system( "crontab $fn" ); }