#!/usr/bin/perl

# This program modifys a standard version of Smoothwall V3.0 Polar
# to use a Compact Flash adaptor instead of a hard disk.
#
# It is distributed under GPL V3.0 license which should be present.
# I have no concerns with anybody doing what they like with this but
# want to ensure that everybody benefits from this work.

# Do not change this unless you know exactly what you are doing.
# I wrote the code and I don't know what I'm doing so take that as a hint!
my $testmode=0;

# Variables that the user can change.
# How much information is printed from running lilo. Useful for debugging things.
my $lilodebuglevel = 1;

# What percentage of RAM should be reserved for a ram disk. 10 = 10% 50 = 50%. 10%
# is fine for 128MB of RAM, 25% should be ok for 512MB, 50% should be ok for 1GB.
# Note that you can never have a ramdisk size bigger than the swap disk size.
my $ramdiskpercentage = 10;

use strict;
use warnings;
use File::Path;
use File::Basename;

$SIG{'INT'} = 'CatchInterrupt';

my $ROOT_DIR= $testmode == 1 ? "/cf" : "";
my $backupdir="/root/cfbackup";
my $backupdevice = "";
my $outputdir="/tmp/cf.$$";
my $md5sum="/usr/bin/md5sum";

my @smoothwallfiles = ("/etc/lilo.conf" , "/etc/fstab" , "/httpd/cgi-bin/advstatus.cgi" ,
                    "/etc/rc.d/rc.halt" , "/etc/rc.d/rc.sysinit" , "/etc/logrotate.conf");

my $intro =
"Smoothwall V3.0 Compact Flash Install V0.9.4 (c) 2007 Rob Willett GPL V3.0 License

This is the installation program for the Compact Flash installer.

This program automatically modifies relevant files so that
you can boot off and use a Compact Flash based hard disk adaptor.

It only runs on a standard install of Smoothwall V3.0 Express Polar. It *may*
work on later versions but at the moment it is only tested on this version. If
you have modified Smoothwall this may not work at all. You have been warned!

No guarantees whatsover are made for the use of this program.

1) Install the modifications. The program checks to see if it has been run
   already and if so, will not run again.

8) After each update, use this function to check whether the files needed for
   the CF mod have changed.

9) Uninstall and return to the previous state. If you have edited any of the
   files this installer uses the complete uninstall will fail.

Choose an option:

1) Install
8) Check installation
9) Uninstall
";

# This mod makes a ramdisk for use with /var/log.
# We cannot (easily) use /var to mount as /var is a bigger directory in V3 than in V2.
# We would have to syncronise very carefully to ensure we didn't lose anything.

my $ramdisk_mod =
"echo \"Making ramdisk for /var to use\"
mkfs.ext3 -m 0 /dev/ram0
mount /dev/ram0 /var/log
mkdir -p /mnt/log
# /mnt/log is not available at this time so we have to manually mount it.
mount /dev/hda2 /mnt/log
(cd /mnt/log; tar cfpB - *) | (cd /var/log ; tar xfpB -)

# Check to see if all the directory system in /var/log looks OK. Create directories if needed but echo the error
for i in /var/log/httpd /var/log/snort /var/log/imspector /var/log/squid
do
        if [ ! -d \$i ]; then
                echo \"Directory \$i does not exist. Creating it\"
                mkdir -p \$i
        fi
done

# Now we manually umount it!
umount /dev/hda2
";

my $shutdownscript =
"echo \"Backing up log files to CF Card...\"
rm -rf /mnt/log/*
(cd /var/log ; tar cfpB - *) | (cd /mnt/log ; tar xfpB -)
sleep 5
";

my $advstatusmod =
"
 if (\$mtotal > 0)
 {
   \$mperc = int(((\$mused/\$mtotal)*100));
 }
 else
 {
   \$mperc = 0;
 }
# End of mod for Compact Flash use\n";

# Clean out all the backup directories
# and setup new directories for testing.
if ($testmode == 1)
{
   mkpath "/cf/etc/rc.d";
   mkpath "/cf/httpd/cgi-bin";
   `rm -rf $backupdir`;
}

# We open and close the log file each time we write to make sure
# we capture all the information. Is this inefficient? Yes but
# it's easy and makes sure we don't miss valuable information.

sub Log {
    open(LOGFILE , ">>/root/cfmod.log") || die("Can't open logfile for writing\n");
    print @_;
    print LOGFILE @_;
    close(LOGFILE);
}

sub CleanUp {
   my ($value) = @_;

   Log "\n\nCleaning up temporary files\n";
   ExecuteCommand("rm -rf $outputdir" , "" , "");

}

sub Exit {
   my ($value , $s) = @_;

   print STDERR $s;

   CleanUp($value);

   exit($value);
}

sub CatchInterrupt {
   CleanUp(1);

   exit(1);
}

# $ROOT_DIR is used for debugging and should not need to be changed.
# This is a debug routine to create a series of dummy files based on the real files
# to check these routines against.

sub SetupDummyfiles {
   my $file;

   foreach $file (@smoothwallfiles)
   {
      my $orig = $file;
      $orig =~ s/$ROOT_DIR//;

      Log "cp $orig $file\n";
      `cp $orig $file`;
   }
}

SetupDummyfiles() if ($testmode == 1);

sub SetBackupDevice {
   ($backupdevice) = @_;
}

sub GetBackupDevice {
   return $backupdevice;
}

my $executedebugflag = 0;

sub SetExecuteDebug {
   ($executedebugflag) = @_;
}

sub ExecuteCommand {
   my ($cmd , $start , $end) = @_;

   if ($executedebugflag == 1)
   {
      Log "$cmd\n";
      return;
   }

   Log "$start\n" if ($start ne "");
   open(CMDIN , "$cmd |") || Exit(1 , "Can't execute $cmd\n");
   while (<CMDIN>)
   {
      Log "$_";
   }
   close(CMDIN);
   Log "$end\n" if ($end ne "");
}

sub Backup {
   my ($file) = @_;

   Exit(1 , "Can't find file '$file'. Exiting...\n") if (! -f $file);

   mkpath "$backupdir";

   my $destfile = "$backupdir/$file";
   my $dn = dirname $destfile;
   mkpath $dn;

   ExecuteCommand("cp $file $backupdir$file" , "" , "");

   return 0;
}

sub GetMD5Checksum {
   my ($file) = @_;

   my $checksum = `$md5sum $file`;
   chop ($checksum);
   my @tmp = split(/ / , $checksum);

   return $tmp[0];
}

sub WriteOutputFile {
   my @lines = @_;

   my $dir = shift(@lines);
   my $file = shift(@lines);

   Log "WriteOutputFile: Dir '$dir' File '$file'\n" if ($testmode);

   mkpath $dir;
   # Now write the file back out
   open(OUT , ">$dir/$file") || Exit(1 , "Can't open '$dir/$file' for modification\n");
   
   my $line = "";
   foreach $line (@lines)
   {
      print OUT $line;
   }
   close(OUT);

   return GetMD5Checksum("$dir/$file");
}

sub Modify_fstab {
   my ($outputdir , $file) = @_;

   my @lines = ();
   my $device = GetBackupDevice();
   my $dir = dirname($file);
   $file = basename($file);

   if ($device eq "")
   {
      Exit(1 , "Backupdevice has not been set. Aborting...");
   }

   open(IN , "$dir/$file") || Exit(1 , "Can't open $dir/$file for reading\n");

   while (<IN>)
   {
      if (/^\s*\/dev.+swap\s+swap\s+defaults\s+0\s+0/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         s/^/# /;
         push(@lines , $_);
         push(@lines , "$device\t\t/mnt/log\t\text3\tdefaults\t\t1 3\n");
         push(@lines , "# End of mod for Compact Flash use\n");
      } elsif (/^\s*(\/dev[^\s]+)\s+\/var\/log\s+ext3\s+defaults\s+\d+\s+\d+\s*$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         s/^/# /;
         push(@lines , $_);
         push(@lines , "# End of mod for Compact Flash use\n");
                } else
      {
         push(@lines , $_);
      }
   }

   close(IN);

   return WriteOutputFile("$outputdir$dir" , $file , @lines);
}

sub Modify_rcsysinit {
   my ($outputdir , $file) = @_;

   my $dir = dirname($file);
   $file = basename($file);

   my @lines = ();
   open(IN , "$dir/$file") || Exit(1 , "Can't open $dir/$file for reading\n");

   while (<IN>)
   {
      if (/^echo "Turning on swap"$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         s/^/# /;
         push(@lines , $_);
         push(@lines , "# End of mod for Compact Flash use\n");
      } elsif (/^\/sbin\/swapon\s+-a$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         s/^/# /;
         push(@lines , $_);
         push(@lines , "echo \"Turning off swap\"\n");
         push(@lines , "/sbin/swapoff -a\n");
         push(@lines , "# End of mod for Compact Flash use\n");
      } elsif (/^echo \"Mounting other filesystems\"$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         push(@lines , $ramdisk_mod);
         push(@lines , "# End of mod for Compact Flash use\n");
         push(@lines , $_);
      } elsif (/^echo \"Setting drive DMA settings\"$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         s/^/# /;
         push(@lines , $_);
         push(@lines , "# End of mod for Compact Flash use\n");
      } elsif (/^\/sbin\/hdparm -d 1 -c 1 -u 1 \/dev\/harddisk$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         s/^/# /;
         push(@lines , $_);
         push(@lines , "# End of mod for Compact Flash use\n");
      } else
      {
         push(@lines , $_);
      }
   }

   close(IN);

   return WriteOutputFile("$outputdir$dir" , $file , @lines);
}

sub Modify_advstatuscgi {
   my ($outputdir , $file) = @_;

   my $dir = dirname($file);
   $file = basename($file);

   my @lines = ();

   open(IN , "$dir/$file") || Exit("Can't open $dir/$file for reading\n");
   while (<IN>)
   {
      if (/^\s+\$mperc \= int\(\(\(\$mused\/\$mtotal\)\*100\)\);\s*$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         s/^/# /;
         push(@lines , $_);
         push(@lines , $advstatusmod);
      } else
      {
         push(@lines , $_);
      }
   }

   close(IN);

   return WriteOutputFile("$outputdir$dir" , $file , @lines);
}

sub CalculateSwapsize {
   my ($swapsize , $devname);

   open(FDISK_IN , "fdisk -l |") || Exit(1 , "Can't open fisk -l\n");

   while (<FDISK_IN>)
   {
      # /dev/hda2              42         293      127008   82  Linux swap / Solaris
      $swapsize = $2,$devname=$1 if (/^(\/dev\/[^\s]+)\s+\d+\s+\d+\s+(\d+)\s+82\s+Linux swap \/ Solaris$/);
   }
   close(FDISK_IN);

   return ($swapsize , $devname);
}

sub CalculateMemsize {
   my $memtotal = 0;

   open(MEM_IN , "/proc/meminfo") || Exit(1 , "Can't open /proc/meminfo\n");
   while (<MEM_IN>)
   {
      # MemTotal:       126984 kB

      $memtotal = $1 if (/^MemTotal:\s+(\d+)\s+kB$/);
   }
   close(MEM_IN);

   return $memtotal;
}

sub Modify_liloconf {
   my ($outputdir , $file) = @_;
   my $dir = dirname($file);

   $file = basename($file);

   my @lines = ();

   my ($swapsize , $devname) = CalculateSwapsize();

   # Keep the devname for use elsewhere
   SetBackupDevice($devname);

   my $memsize  = CalculateMemsize();

   # We create a ramdisk with no more than 10% of the total RAM available. This is a guess!
   my $ramdisksize = int($memsize * $ramdiskpercentage/100);

        Log "Ramdisksize $ramdisksize Swapsize $swapsize\n";
   # If ramdisksize is greater than swap then make it the size of swap
   $ramdisksize = ($ramdisksize > $swapsize) ? $swapsize : $ramdisksize;
        Log "Ramdisksize $ramdisksize Swapsize $swapsize\n";

   open(IN , "$dir/$file") || Exit(1 , "Can't open '$dir/$file for reading\n");
   while (<IN>)
   {
                # append="ramdisk_size=18192 no-scroll panic=30"
                if (/^append=\"ramdisk_size=(\d+)\s+(.+)\"$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         # Put our version of the size for the ramdisk in place.
         push(@lines , "append=\"ramdisk_size=$ramdisksize $2\"\n");
         push(@lines , "# End of mod for Compact Flash use\n");
      } else
      {
         push(@lines , $_);
      }
   }

   close(IN);

   return WriteOutputFile("$outputdir$dir" , $file , @lines);
}

sub Modify_rchalt {
   my ($outputdir , $file) = @_;
   my $dir = dirname($file);

   $file = basename($file);

   my @lines = ();
   open(IN , "$dir/$file") || Exit(1 , "Can't open $dir/$file for reading\n");

   while (<IN>)
   {
      if (/^echo \"Shutting down...\"$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         push(@lines , $shutdownscript);
         push(@lines , "# End of mod for Compact Flash use\n");
      }
      push(@lines , $_);
   }

   close(IN);

   return WriteOutputFile("$outputdir$dir" , $file , @lines);
}

sub Modify_logrotateconf {
   my ($outputdir , $file) = @_;
   my $dir = dirname($file);

   $file = basename($file);

   my @lines = ();
   open(IN , "$dir/$file") || Exit(1 , "Can't open $dir/$file for reading\n");

   while (<IN>)
   {
      if (/^monthly$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         push(@lines , "# monthly\n");
         push(@lines , "daily\n");
         push(@lines , "# End of mod for Compact Flash use\n");
      } elsif (/^rotate (\d+)$/)
      {
         push(@lines , "# Start of mod for Compact Flash use\n");
         push(@lines , "# $_");
         push(@lines , "rotate 1\n");
         push(@lines , "# End of mod for Compact Flash use\n");
                } else
                {
              push(@lines , $_);
                }
   }

   close(IN);

        # Copy the weekly logroatate function to do daily. /mnt/log is so much smaller now we have
        # to be radical with logs.
        `cp /etc/cron.weekly/logrotate /etc/cron.daily/logrotate`;

   return WriteOutputFile("$outputdir$dir" , $file , @lines);
}

sub DoInstall {
   my $file;
   # Check to see if this is the first time we have run this. We only run this once!
   Exit(1 , "We've already made the modifications to this machine. No point in running again\n") if (-d "$backupdir");

   # Create the output dir for holding temporary copies of the files.
   mkpath "$outputdir";

   my %checksum;
   # The order we do this in is important. Modify_lilo.conf needs to be first.
   $file = "/etc/lilo.conf";
   $checksum{"$file"} = Modify_liloconf($outputdir , "$ROOT_DIR/$file");
   Exit("Can't modify '$file'. No files have been changed. Exiting\n") if ($checksum{$file} eq "");

   $file = "/etc/fstab";
   $checksum{$file} = Modify_fstab($outputdir , "$ROOT_DIR/$file");
   Exit("Can't modify '$file'. No files have been changed. Exiting\n") if ($checksum{$file} eq "");

   $file = "/etc/rc.d/rc.sysinit";
   $checksum{$file} = Modify_rcsysinit($outputdir , "$ROOT_DIR/$file");
   Exit("Can't modify '$file'. No files have been changed. Exiting\n") if ($checksum{$file} eq "");

   $file = "/httpd/cgi-bin/advstatus.cgi";
   $checksum{$file} = Modify_advstatuscgi($outputdir , "$ROOT_DIR/$file");
   Exit("Can't modify '$file'. No files have been changed. Exiting\n") if ($checksum{$file} eq "");

   $file = "/etc/rc.d/rc.halt";
   $checksum{$file} = Modify_rchalt($outputdir , "$ROOT_DIR/$file");
   Exit("Can't modify '$file'. No files have been changed. Exiting\n") if ($checksum{$file} eq "");

        $file = "/etc/logrotate.conf";
   $checksum{$file} = Modify_logrotateconf($outputdir , "$ROOT_DIR/$file");
   Exit("Can't modify '$file'. No files have been changed. Exiting\n") if ($checksum{$file} eq "");

   # Backup all the files we use.
   Log "Backing up all the files we use\n";
   foreach $file (@smoothwallfiles)
   {
      Backup($file);

      Log sprintf("  %-50s [ OK ]\n" , $file);
   }

   # Now we have to execute certain commands to make the work stick.
   # SetExecuteDebug(1);

   # Format the old swap partition for use as a backup device.
   # Turn swapoff first
   ExecuteCommand("swapoff -a" , "Turning swap off" , "");
   ExecuteCommand("/sbin/mkfs.ext3 ".GetBackupDevice() , "Starting format of old swap space" , "Finished format of old swap space");
   ExecuteCommand("mkdir -p /mnt/log" , "" , "");
   ExecuteCommand("mount ".GetBackupDevice()." /mnt/log" , "" , "");

   # No errors to here. So now do a simple copy of all the files back to
   # their real locations.
   for $file (@smoothwallfiles)
   {
      ExecuteCommand("cp $outputdir$ROOT_DIR/$file $file\n" , "" , "");
   }

   # Now we rerun lilo just to make the changes stick and
   # display the information back to the user.
   ExecuteCommand("cat /etc/lilo.conf" , "-----------/etc/lilo.conf------------" , "-------------------------------------");
   ExecuteCommand("/sbin/lilo -v $lilodebuglevel -C /etc/lilo.conf" , "Rerunning lilo" , "Finished running lilo");

   # Write out the md5sums of the files we have changed. If we want to uninstall then we need to check
   # that the files have been untouched with human hands. we check the md5sums of the files against this
   # to see they are OK.
   open(OUT , ">$backupdir/md5sums") || Exit(1 , "Can't open $backupdir/md5sums for writing\n");
   for $file (keys %checksum)
   {
      print OUT "$file ".$checksum{$file}."\n";
   }
   close(OUT);

   CleanUp();

   for (my $i = 10 ; $i > 0 ; $i--)
   {
      Log "The system will automatically be rebooted in $i seconds\n";
      sleep 1;
   }

   Log "System is rebooting...\n";
   `reboot`;
}

sub CheckIfFilesModified {
   my @restorefiles = ();
   my $count = 0;
   my $totalcount = 0;
   my $file;

        if (! -d $backupdir)
        {
              Log "\n\n------------------- Warning/Error ---------------------------------\n";
              Log "The Compact Flash mod doesn't appear to have been installed\n";
              Log "No backup directory '$backupdir' found. If you have installed\n";
              Log "this mod, then the lack of a backup directory indicates a\n";
              Log "serious problem\n";
              Log "------------------- Warning/Error ---------------------------------\n\n";
              Exit(1 , "Now exiting\n");
        }
   open(IN , "$backupdir/md5sums") || Exit(1 , "Can't open $backupdir/md5sums for reading\n");
   while (<IN>)
   {
      my ($file , $checksum) = split;

      if (GetMD5Checksum($file) eq $checksum)
      {
         $count++;
      } else
      {
         Exit(1 , "'$file' has been edited.\n");
      }
      $totalcount++;
      push(@restorefiles , $file);
   }
   close(IN);

   # Check to see if we have all the backed up files OK.
   for $file (@restorefiles)
   {
      Log "Checking $backupdir$file\n";
      Exit(1 , "Can't find backup file $backupdir$file\n") if (! -f "$backupdir$file");
   }

   if ($count == $totalcount)
   {
      Log "All the files modified by the Compact Flash modification are untouched.\n";
      Log "\nThis means there is no need to install this modification as nothing\n";
                Log "has changed.\n\n";
      Log "\nYou are advised NOT to try and reinstall\n";

      return 0;
   }

   return 1;
}

sub DoUninstall {
   my @restorefiles = ();
   my $count = 0;
   my $totalcount = 0;
   my $file;

   open(IN , "$backupdir/md5sums") || Exit(1 , "Can't open $backupdir/md5sums for reading\n");
   while (<IN>)
   {
      my ($file , $checksum) = split;

      if (GetMD5Checksum($file) eq $checksum)
      {
         $count++;
      } else
      {
         Exit(1 , "'$file' has been edited.\nCannot uninstall as system in inconsistent state\n");
      }
      $totalcount++;
      push(@restorefiles , $file);
   }
   close(IN);

   # Check to see if we have all the backed up files OK.
   for $file (@restorefiles)
   {
      Log "Checking $backupdir$file\n";
      Exit(1 , "Can't find backup file $backupdir$file\n") if (! -f "$backupdir$file");
   }

   # We only restore all the files if everyone is untouched.
   if ($count == $totalcount)
   {
      my $devicename;

      open(IN , "mount |") || Exit(0 , "Can't execute mount command\n");
      while (<IN>)
      {
         Log;
         # /dev/hda2 on /mnt/log type ext3 (rw)
         if (/^(\/dev\/[^\s]+)\s+on\s+\/mnt\/log\s+type\s+ext3\s+\(rw\)$/)
         {
            $devicename = $1;
         }
      }
      close(IN);
      ExecuteCommand("umount /mnt/log" , "" , "");
      ExecuteCommand("mkswap $devicename" , "" , "");

      for $file (@restorefiles)
      {
         ExecuteCommand("cp $backupdir$file $file" , "" , "");
      }

      ExecuteCommand("rm -rf $backupdir" , "Removing backup directory" , "");

                # remove the additional cron job we created
                `rm -f /etc/cron.daily/logrotate`;

      for (my $i = 10 ; $i > 0 ; $i--)
      {
         Log "The system will automatically be rebooted in $i seconds\n";
         sleep 1;
      }

      Log "System is rebooting...\n";
      `reboot`;
   }
}

Log $intro;

my $notfinished = 1;

while ($notfinished)
{
   Log "[1 for install/8 to check install/9 for uninstall] --> ";

   my $keypress = <STDIN>;
   chop($keypress);
   Log "\n\nKeypress = $keypress\n";

   if ($keypress eq "1")
   {
      $notfinished = DoInstall();
   } elsif ($keypress eq "8")
   {
      $notfinished = CheckIfFilesModified();
   } elsif ($keypress eq "9")
   {
      $notfinished = DoUninstall();
   } else
   {
      Log "Unrecognised keypress ($keypress)\n";
   }
}

exit(0); 
