#!/usr/bin/perl -s
##
## Razor2::Client:Config
##
## Copyright (c) 2002, Vipul Ved Prakash.  All rights reserved.
## This code is free software; you can redistribute it and/or modify
## it under the same terms as Perl itself.
##
## $Id: Config.pm,v 1.66 2007/05/10 20:32:10 rsoderberg Exp $

package Razor2::Client::Config;
use strict;
use Data::Dumper;
use File::Copy;
use File::Spec;

use Razor2::Logger;

#use base qw(Razor2::Logger);

sub new {
    my ($class) = @_;
    return bless {}, $class;
}

#
# figures out razorhome and razorconf file
#
sub read_conf {
    my ( $self, $params ) = @_;

    my $default_conf_fn = "$self->{global_razorhome}/razor-agent.conf";
    my $conf;
    my $defaults    = $self->default_agent_conf();
    my $use_engines = $defaults->{use_engines};

    if ( $self->{razorconf} ) {
        #
        # cmd-line config file specified
        #
        $conf = $self->read_file( $self->{razorconf}, $defaults )
          unless ( $self->{opt}->{create} && $self->{opt}->{config} );
        if ( $self->{opt}->{razorhome} ) {
            $self->{computed_razorhome} = $self->{razorhome} = $self->{opt}->{razorhome};
        }
        else {
            $self->find_home( $self->{opt}->{razorhome} || $conf->{razorhome} );
        }
    }
    else {

        $self->compute_razorconf();

        if ( $self->{razorconf} ) {
            $conf = $self->read_file( $self->{razorconf}, $defaults );
        }
        else {
            $self->log( 6, "No razor-agent.conf found, using defaults. " );
            $conf = $defaults;
        }
    }

    foreach ( keys %{$defaults} ) {
        next if exists $conf->{$_};
        $conf->{$_} = $defaults->{$_};
    }

    # Override use_engines from defaults. To store use_engines
    # in the config file is a design flaw, since the client
    # supported engines are defined by the razor-agents source,
    # and could potentially be incorrect in the config file
    # after an upgrade.

    $conf->{use_engines} = $use_engines;

    foreach ( keys %{ $self->{opt} } ) {
        next if ( $_ eq '' || $_ eq 'use_engines' || $_ eq 'razorzone' );
        $conf->{$_} = $self->{opt}->{$_};
    }

    if ($params) {
        foreach ( keys %$params ) {
            next if ( $_ eq '' || $_ eq 'use_engines' || $_ eq 'razorzone' );
            $conf->{$_} = $params->{$_};
        }
    }

    $self->{conf} = $conf;

    #
    # post config processing
    # insert things that should not be in conf here
    #

    # turn off run-time warnings unless debug flag passed
    # http://www.perldoc.com/perl5.6.1/pod/perllexwarn.html
    $^W = 0 unless $conf->{debug};

    # add full path to all config values that need them
    #
    if ( $self->{razorhome} ) {
        foreach (
            qw( logfile pidfile listfile_catalogue listfile_nomination
            listfile_discovery whitelist identity)
        ) {
            next unless $conf->{$_};
            next if $conf->{$_} =~ /^\//;
            next if ( $_ eq 'logfile' && ( $conf->{$_} eq 'syslog' || $conf->{$_} eq 'sys-syslog' || $conf->{$_} eq 'none' ) );
            $conf->{$_} = "$self->{razorhome}/$conf->{$_}";
        }
    }
    return $self->{conf};
}

#
#  Figure out which conf to use - user's own, or system conf.
#
#  If no user conf or no system conf, razorconf will be blank
#  but computed_razorconf will be set.
#
#  However, if razorhome is still unknown, computed_razorconf can be blank
#
sub compute_razorconf {
    my $self = shift;

    my $default_conf_fn = "$self->{global_razorhome}/razor-agent.conf";

    $self->{razorconf} = "";
    $self->find_home();
    if ( $self->{razorhome} ) {
        my $mycf = "$self->{razorhome}/razor-agent.conf";
        $self->{computed_razorconf} = $mycf;
        if ( -r $mycf ) {
            $self->{razorconf} = $mycf;
        }
        elsif ( -e $mycf ) {
            $self->log( 5, "Found but can't read $mycf, skipping." );
        }
        else {
            $self->log( 5, "No $mycf found, skipping." );
        }
    }
    if ( !$self->{razorconf} && -e $default_conf_fn ) {
        if ( -r $default_conf_fn ) {
            $self->{razorconf} = $default_conf_fn;
        }
        else {
            $self->log( 5, "Found but can't read $default_conf_fn, skipping." );
            $self->{computed_razorconf} ||= $default_conf_fn;
        }
    }
}

sub write_conf {
    my ( $self, $hash ) = @_;

    unless ( $self->{razorconf} ) {
        $self->log( 5, "Cannot write_conf without razorconf set" );
        return $self->error("Cannot write_conf without razorconf set");
    }
    my $now = localtime();
    my $srcmsg;
    unless ($hash) {
        $hash = $self->default_agent_conf();
        if ( -r $self->{razorconf} ) {
            $hash = $self->read_file( $self->{razorconf}, $hash );
            $srcmsg = "Non-default values taken from $self->{razorconf}";
        }
        else {
            $srcmsg = "Created with all default values";
        }
    }

    my $clientheader = <<EOFCLIENT;
#
# Razor2 config file
# 
# Autogenerated by $self->{name_version} 
# $now
# $srcmsg 
# 
# see razor-agent.conf(5) man page 
#
EOFCLIENT
    return $self->write_file( $self->{razorconf}, $hash, 0, $clientheader );
}

sub find_user {
    my $self = shift;

    return 1 if $self->{user};

    $self->{user} = getpwuid($>) || do {
        $self->log( 1, "Can't figure out who the effective user is: $!" );
        return undef;
    };
    return 1;
}

# compute razorhome.  like so:
#
#    -home=/tmp/razor/              used if readable, else
#    'razorhome' from config file   used if readable, else
#    <home>/.razor/                 used if readable, else
#    <home>/.razor/                 is created.  if that fails, no razorhome.
#    -conf=/foo/razor/razor.conf    if all else fails pick it up from the config file path,
#                                   if one is available

sub find_home {
    my ( $self, $rhome ) = @_;

    my $dotrazor = '.razor';
    $dotrazor = '_razor' if $^O eq 'VMS';

    if ( defined $self->{razorhome} ) {
        $self->{razorhome_computed} = $self->{razorhome};
        return 1;
    }

    if ( defined $self->{opt}->{razorhome} ) {
        $self->{razorhome_computed} = $self->{razorhome};
        return 1;
    }

    # if razorhome is read from config file, its passed as rhome
    unless ($rhome) {

        if ( defined $ENV{HOME} ) {
            $rhome = File::Spec->catdir( "$ENV{HOME}", "$dotrazor" );
        }
        else {
            return unless $self->find_user();
            $rhome = File::Spec->catdir( ( getpwnam( $self->{user} ) )[7], "$dotrazor" ) || "/home/$self->{user}/$dotrazor";
        }
        $rhome = VMS::Filespec::unixify($rhome) if $^O eq 'VMS';
        $self->log( 8, "Computed razorhome from env: $rhome" );
    }
    $self->{razorhome_computed} = $rhome;

    if ( -d $rhome ) {
        if ( -w $rhome ) {
            $self->log( 6, "Found razorhome: $rhome" );
        }
        else {
            $self->log( 6, "Found razorhome: $rhome, however, can't write to it." );
        }
        $self->{razorhome} = $rhome;
        return 1;

    }

    if ( $self->{razorconf} ) {
        my $path = $$self{razorconf};
        if ( $path =~ m:/: ) {
            if ( $path =~ m:(.*)/: ) {
                $self->{razorhome} = $1;
                return 1;
            }
        }
    }

    $self->log( 5, "No razorhome found, using all defaults" );
    $self->{razorhome} = "";
    return 1;
}

sub create_home {
    my ( $self, $rhome ) = @_;

    if ( -d $rhome ) {
        $self->{razorhome} = $rhome;
        return 1;
    }
    if ( mkdir $rhome, 0755 ) {
        $self->log( 6, "Created razorhome: $rhome" );
        $self->{razorhome} = $rhome;
        return 1;
    }
    return $self->error("Could not mkdir $rhome: $!");
}

sub compute_identity {
    my ($self) = @_;
    $self->find_home() or return;

    return 1 if $self->{identity};

    my $id;

    if ( $id = $self->{opt}->{identity} ) {
        $self->{identity} = $self->my_readlink($id);

        # warn we can't read it unless we are registering new identity
        $self->log( 6, "Can't read identity:  $self->{identity}" )
          unless ( $self->{opt}->{register} ) || ( -r $self->{identity} );
        return 1;

        # if not specified via cmd-line, just compute it, don't read it.

    }
    elsif ( $id = $self->{conf}->{identity} ) {
        $self->{identity} = $self->my_readlink($id);
        return 1;

    }
    else {
        $id = $self->{razorhome} ? "$self->{razorhome}/identity" : "";
        $self->{identity} = $self->my_readlink($id);
        return 1;
    }
}

sub get_ident {
    my ($self) = @_;
    $self->find_home() or return;

    my $idfn = $self->{identity};
    return $self->error("Cannot read the identity file: $idfn") unless -r $idfn;

    $idfn = $self->my_readlink($idfn);

    my $mode = ( ( stat($idfn) )[2] ) & 07777;    # mask off file type
    if ( $mode & 0007 ) {
        $self->log( 2, "Please chmod $idfn so it is not world readable." );
    }
    return $self->read_file($idfn);
}

# returns { user => $user, pass => $pass } if success
# returns 2 if error
sub register_identity {
    my ( $self, $user, $pass ) = @_;
    my $ident = $self->register(
        {
            user => $user,
            pass => $pass,
        }
    );
    $self->disconnect() or return 2;
    return $ident || 2;
}

sub ident_fn {
    my ( $self, $ident ) = @_;
    $self->find_home() or return;

    my $orig;
    my $syml;
    my $obase = "identity-$ident->{user}";

    $obase = $1 if $obase =~ /^(\S+)$/;    # untaint obase

    # if it's a user specified identity file, don't symlink
    unless ( $orig = $self->{opt}->{identity} ) {
        $orig = "$self->{razorhome}/$obase";
        $syml = "$self->{razorhome}/identity";

        $orig = $1 if $orig =~ /^(\S+)$/;    # untaint orig
        $syml = $1 if $syml =~ /^(\S+)$/;    # untaint syml
    }

    return ( $orig, $obase, $syml );
}

sub save_ident {
    my ( $self, $ident ) = @_;

    my ( $orig, $obase, $syml ) = $self->ident_fn($ident);

    unless ( length $orig ) {
        return $self->error("couldn't figure out identity filename");
    }

    rename( $orig, "$orig.bak" ) if -s $orig;
    my $umask = umask 0077;    # disable group and all from read/write/execute
    $self->write_file( $orig, $ident ) or return;
    umask $umask;

    # don't create a symlink if user specified identity file from cmd-line
    return $orig unless $syml;

    unless ( $self->{opt}->{symlink} ) {
        return $orig if -e $syml;    # already has another identity
    }

    unlink $syml;
    if ( eval { symlink( "", "" ); 1 } ) {
        $obase = $1 if $obase =~ /^(\S+)$/;    # untaint obase
        $syml  = $1 if $syml =~ /^(\S+)$/;     # untaint syml

        symlink $obase, $syml
          or return $self->error("Created $orig, but could not symlink to it $syml: $!");
    }
    else {
        $self->log( 5, "symlinks don't work on this machine" );
        copy( $orig, $syml );
    }
    return $orig;
}

sub my_readlink {
    my ( $self, $fn ) = @_;

    while (1) {
        return $fn unless -l $fn;

        if ( $fn =~ /^(.*)\/([^\/]+)$/ ) {
            my $dir = $1;
            $fn = readlink $fn;
            $fn = $1 if $fn =~ /^(\S+)$/;           # untaint readlink
            $fn = "$dir/$fn" unless $fn =~ /^\//;
        }
        else {
            $fn = readlink $fn;
            $fn = $1 if $fn =~ /^(\S+)$/;           # untaint readlink
        }
    }
}

sub parse_value {
    my ( $self, $value ) = @_;

    $value =~ s/^\s+//;
    $value =~ s/\s+$//;
    if ( $value =~ m:,: ) {
        my @values = split /,\s*/, $value;
        return [@values];
    }
    else {
        return $value;
    }
}

# given filename, returns hash ref of key = val from file
# if $nothash, than no key && val, just return array ref containing all lines.
#
sub read_file {
    my ( $self, $fn, $h, $nothash ) = @_;

    unless ( defined $fn && length $fn ) {
        $self->log( 5, "Filename not provided to read_file" );
        return;
    }

    my $conf = ref($h) eq 'HASH' ? $h : {};

    if ( $^O eq 'VMS' && $fn !~ /\[/ ) {
        my ( $dir, $file, $ext ) = ( $fn =~ /(^.*\/)(.*)(\..*)$/ );
        $dir =~ s/\./_/g;
        $file =~ s/\./_/g;
        $fn = $dir . $file . $ext;
    }

    $fn = $1 if $fn =~ /^(\S+)$/;    # untaint $fn

    unless ( defined($fn) && ( ( $fn =~ /^\// ) || -e $fn ) ) {
        $self->log( 7, "Can't read file $fn, looking relative to $self->{razorhome}" );
        $fn = "$self->{razorhome}/$fn";
        $fn = $1 if $fn =~ /^(\S+)$/;    # untaint $fn
    }

    my $total = 0;
    my @lines;
    unless ( open CONF, "<$fn" ) {
        $self->log( 5, "Can't read file $fn: $!" );
        return;
    }

    # set $/ to the default in case someone has overwritten $/ elsewhere
    local $/ = "\n";

    for (<CONF>) {
        chomp;
        next if /^\s*#/;
        if ($nothash) {
            next unless s/^\s*(.+?)\s*$/$1/;    # untaint
            $conf->{$_} = 7;
            push @lines, $_;
        }
        else {
            next unless /=/;
            my ( $attribute, $value ) = /^\s*(.+?)\s*=\s*(.+?)\s*$/;    # untaint
            next unless ( defined $attribute && defined $value );
            $conf->{$attribute} = $self->parse_value($value);
        }
        $total++;
    }
    close CONF;
    $self->log( 5, "read_file: $total items read from $fn" );

    return $nothash ? \@lines : $conf;
}

# given hash ref, writes to file key = val
# NOTE: key should not contain '=';
#
# given array ref, writes to file each item
#
# given scalar ref, writes to file
#
sub write_file {
    my ( $self, $fn, $hash, $append, $header, $lock ) = @_;

    $fn = "$self->{razorhome}/$fn" unless ( $fn =~ /^\// );
    $fn = ">$fn" if $append;

    if ( $^O eq 'VMS' && $fn !~ /\[/ ) {
        my ( $dir, $file, $ext ) = ( $fn =~ /(^.*\/)(.*)(\..*)$/ );
        $dir =~ s/\./_/g;
        $file =~ s/\./_/g;
        $fn = $dir . $file . $ext;
    }

    $fn = $1 if $fn =~ /^(\S+)$/;    # untaint $fn

    # check for lock file
    my $lockfile = "$fn.lock";
    $lockfile = "${fn}_lock;1" if $^O eq 'VMS';
    if ($lock) {
        if ( -r "$lockfile" ) {
            return $self->error("File is locked, try again later: $lockfile");
        }
        else {
            unless ( open LOCK, ">$fn.lock" ) {
                return $self->error("Can't create lock file $fn.lock: $!");
            }
            close LOCK;
        }
    }
    unless ( open CONF, ">$fn" ) {
        return $self->error("Can't write file $fn: $!");
    }
    print CONF "$header\n" if $header;
    my $total = 0;
    if ( ref($hash) eq 'HASH' ) {
        foreach ( sort keys %$hash ) {
            return $self->error("Key cannot contain '=': $_") if /=/;
            printf CONF "%-22s = ", $_;
            if ( ref( $hash->{$_} ) eq "ARRAY" ) {
                print CONF join( ',', @{ $hash->{$_} } ) . "\n";
            }
            else {
                print CONF $hash->{$_} . "\n";
            }
            $total++;
        }

    }
    elsif ( ref($hash) eq 'ARRAY' ) {
        foreach (@$hash) {
            next unless /\S/;
            if ( ref($_) eq "ARRAY" ) {
                print CONF join( ', ', @$_ ) . "\n";
            }
            else {
                print CONF $_ . "\n";
            }
            $total++;
        }
    }
    elsif ( ref($hash) eq 'SCALAR' ) {
        printf CONF $$hash;
        $total++;
    }
    close CONF;
    if ($lock) {
        1 while unlink "$lockfile";
    }
    $self->log( 5, "wrote $total " . ref($hash) . " items to file: $fn" );

    #return $total;
    return 1;
}

sub default_server_conf {
    my $self     = shift;
    my $defaults = {
        srl          => -1,
        ep4          => '7542-10',
        bql          => 4,
        ac           => 0,
        bqs          => 128,
        se           => 'C8',                     # engines 4, 8
        dre          => 4,
        zone         => 'razor2.cloudmark.com',
        logic_method => 4,
    };

    # split strings with , into array
    foreach ( keys %$defaults ) {
        $defaults->{$_} = $self->parse_value( $defaults->{$_} );
    }
    return $defaults;
}

sub default_agent_conf {
    my $self = shift;

    #
    # These get overwritten by whatever's in config file,
    # which in turn gets overwritten by cmd-line options.
    #
    my $defaults = {
        debuglevel          => "3",
        logfile             => "razor-agent.log",
        listfile_catalogue  => "servers.catalogue.lst",
        listfile_nomination => "servers.nomination.lst",
        listfile_discovery  => "servers.discovery.lst",
        min_cf              => "ac",
        turn_off_discovery  => "0",
        ignorelist          => "0",
        razordiscovery      => "discovery.razor.cloudmark.com",
        rediscovery_wait    => "172800",
        report_headers      => "1",
        whitelist           => "razor-whitelist",
        use_engines         => "4, 8",
        identity            => "identity",
        logic_method        => 4,
    };

    # 'razorhome' can exist in .conf, but we compute it instead of listing it here
    # 'rlimit' ?

    # split strings with , into array
    foreach ( keys %$defaults ) {
        $defaults->{$_} = $self->parse_value( $defaults->{$_} );
    }
    return $defaults;
}

1;

