#!/usr/bin/perl
#
# Rotates all the logs associated with all the vhosts in an apache config
#
# $Id: vhost-log-rotator,v 1.3 2004/04/24 20:17:36 nemesis Exp $
#
# Copyright (C) 2001-2004 Kees Cook
# kees@outflux.net, http://outflux.net/
# 
# 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.
# 
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
# http://www.gnu.org/copyleft/gpl.html
#
use Getopt::Std;
use strict;

umask(0022);

our($opt_n,$opt_v,$opt_h,$opt_u,$opt_c);
getopts('nvhu:c:');

if ($opt_h) {
  die "Usage: $0 [-nvh] [-c CYCLES] [-u USER] [HTTPD.CONF] ...\n
-n        No actions: dry run -- don't actually do anything
-v        Run verbose
-h        This help
-u USER   Have new files owned by USER (default 'root')
-c CYCLES How many cycles of rotation to keep (default '3')
";
}

my $DRYRUN =$opt_n;
my $VERBOSE=$opt_v;
my $CYCLES =$opt_c || 3;
my $USER   =$opt_u || "root";

my @CONFS=("/etc/apache/httpd.conf");
if ($ARGV[0]) {
  @CONFS=@ARGV;
}

my $COMPRESS=findProggie("gzip");
my $CP=findProggie("cp");
my $CAT=findProggie("cat");
my $EGREP=findProggie("egrep");
my $MV=findProggie("mv");
my $APACHECTL=findProggie("apachectl");
my $CHOWN=findProggie("chown");
my $TOUCH=findProggie("touch");

my @LOGS=findLogs(@CONFS);
checkLogs(@LOGS);
shuffleLogs(@LOGS);
restartDaemon();
compressLogs(@LOGS);

sub findProggie {
  my($prog)=@_;

  my @LIST = ("/sbin", "/bin", "/usr/sbin",
        "/usr/sbin", "/usr/local/bin",
        "/usr/local/sbin");
  my($path,$exe);

  foreach $path (@LIST) {
    $exe="$path/$prog";
    return $exe if (-x $exe);
  }
  die "Could not find '$prog'\n";
}

sub findLogs {
  my(@confs)=@_;
  my(@lines,%logs,$server_root);

  foreach my $file (@confs) {
    warn "Processing: $file\n" if ($VERBOSE);
    @lines=`$CAT $file`;
    foreach my $line (@lines) {
     chomp($line);
     $line=~s/^\s+//;
     $line=~s/\s+$//;

     # Locate server root for unqualified paths
     if ($line =~ /^ServerRoot\s+\"([^\"]+)/) {
        $server_root=$1;
        # trim trailing slashes
        $server_root=~s#/$##g;
        warn "ServerRoot: $server_root\n" if ($VERBOSE);
     }

     # Find any includes
     if ($line =~ /^Include\s+(\S+)/) {
        my $glob=$1;
        # prepend ServerRoot if not full path
        $glob=$server_root."/".$glob 
          if ($glob!~m#^/#);
        warn "Include: $glob\n" if ($VERBOSE);
        my @moreconf=glob($glob);
        push(@confs,@moreconf);
        grep(warn("Conf: $_\n"),@moreconf)
          if ($VERBOSE);
     }

     if ($line !~ /^(SSL|)LogFormat\s+/ &&
         $line !~ /^(SSL|)LogLevel\s+/ &&
         $line =~ /^(Error|Custom)Log\s+/) {
        my @parts=split(/\s+/,$line);
        # prepend ServerRoot if not full path
        $parts[1]=$server_root."/".$parts[1]
          if ($parts[1]!~m#^/#);
        $logs{$parts[1]}=1;
     }
    }
  }

  return sort keys %logs;
}

sub checkLogs {
  my(@logs)=@_;
  my $file;

  foreach $file (@logs) {
    warn "Log: $file\n" if ($VERBOSE);
    if (! -f $file) {
     System("$TOUCH $file");
     System("$CHOWN $USER $file");
    }
  }
}

sub System {
  my($cmd)=@_;

  if ($VERBOSE) {
    warn "$cmd\n";
  }
  if (!$DRYRUN) {
    system($cmd);
  }
}

sub shuffleLogs {
  my(@logs)=@_;
  my($file,$cycle,$nextlog);
  my($orig,$next);

  foreach $file (@logs) {
    for ($cycle=$CYCLES; $cycle>0; $cycle--) {
     $nextlog=$cycle+1;

     $orig="$file.$cycle";
     $next="$file.$nextlog";
     if (-f $orig) {
        System("$COMPRESS -f -9 $orig");
     }
     else {
        $orig="$orig.gz";
        $next="$next.gz";
     }

     System("$MV -f $orig $next 2>/dev/null");
    }

    System("$MV $file $file.1");
    # compression comes later
    System("$CP /dev/null $file");
    System("$CHOWN $USER $file");
  }
}


sub compressLogs {
  my(@logs)=@_;
  my $file;

  foreach $file (@logs) {
    System("$COMPRESS -f -9 $file.1");
  }
}
  
sub restartDaemon {
  System("${APACHECTL} graceful | ${EGREP} -v 'gracefully restarted|Processing'");
}

