#!/usr/bin/perl

=head1 NAME

pfconfig - Serves configuration through a socket

=cut

=head1 SYNOPSIS

pfconfig [options]

 Options:
   -d                 Daemonize
   -h                 Help
   -s SOCK_PATH       The path to the unix socket path - default $PFDIR/var/pfconfig.sock
   -n NAME            The name of process as reported in ps - default pfconfig
   -p NAME            The prefix of pidfile - defaults to the name

=cut

use strict;
use warnings;

BEGIN {
    #Ensure that the file permissions of the log file is correct 0660
    umask (0);
    use constant INSTALL_DIR => '/usr/local/pf';
    use lib INSTALL_DIR . "/lib";
    use pfconfig::log;
}

use IO::Socket::UNIX qw( SOCK_STREAM SOMAXCONN );
use JSON::MaybeXS;
use pfconfig::manager;
use Data::Dumper;
use Time::HiRes;
use pfconfig::timeme;
use List::MoreUtils qw(first_index);
use File::Basename qw(basename);
use Getopt::Std;
use POSIX qw(:signal_h);
use pf::services::util;
use pfconfig::constants;
use Sereal::Encoder;
use Errno qw(EINTR EAGAIN);
use bytes;
use pf::util::networking qw(send_data_with_length);
$pfconfig::timeme::VERBOSE = 0;
our $RUNNING = 1;

my %args = (
    s => $pfconfig::constants::SOCKET_PATH,
    #Name of the process
    n => "pfconfig",
);

getopts( 'dhs:n:p:', \%args );

$args{p} //= $args{n};

our $PROGRAM_NAME = $0 = $args{n};


my $socket_path = $args{s};
unlink($socket_path);

#Ensure that the file permissions of the socket is correct 0770
umask(0007);

my $listner = IO::Socket::UNIX->new(
   Type   => SOCK_STREAM,
   Local  => $socket_path,
   Listen => SOMAXCONN,
)
   or die("Can't create server socket: $!\n");

umask(0);

my $cache = pfconfig::manager->new;
$cache->preload_all();

my $encoder = Sereal::Encoder->new;


my $logger = pfconfig::log::get_logger;

# init signal handlers
POSIX::sigaction(
    &POSIX::SIGHUP,
    POSIX::SigAction->new(
        'normal_sighandler', POSIX::SigSet->new(), &POSIX::SA_NODEFER
    )
) or $logger->logdie("pfconfig: could not set SIGHUP handler: $!");

POSIX::sigaction(
    &POSIX::SIGTERM,
    POSIX::SigAction->new(
        'normal_sighandler', POSIX::SigSet->new(), &POSIX::SA_NODEFER
    )
) or $logger->logdie("pfconfig: could not set SIGTERM handler: $!");

POSIX::sigaction(
    &POSIX::SIGINT,
    POSIX::SigAction->new(
        'normal_sighandler', POSIX::SigSet->new(), &POSIX::SA_NODEFER
    )
) or $logger->logdie("pfconfig: could not set SIGINT handler: $!");


# empty control file directory so subcaches in other processes
# are expired when pfconfig is starting
unlink glob "$pfconfig::constants::CONTROL_FILE_DIR/*";

my $daemonize = $args{d};

# standard signals and daemonize
daemonize($PROGRAM_NAME) if ($daemonize);

our %DISPATCH = (
    'expire'             => \&expire,
    'element'            => \&get_element,
    'hash_element'       => \&get_hash_element,
    'keys'               => \&get_keys,
    'next_key'           => \&get_next_key,
    'key_exists'         => \&get_key_exists,
    'array_element'      => \&get_array_element,
    'array_size'         => \&get_array_size,
    'array_index_exists' => \&get_array_index_exists,
    'sleep'              => \&server_sleep,
);

while($RUNNING) {
    my $socket;
    my $line;
    eval {
        $socket = $listner->accept();
        #Check if a signal was caught
        unless (defined $socket || $! == EINTR) {
            die("Can't accept connection: $!\n");
        }
        if($socket) {
            chomp( $line = <$socket> );

            if($line eq "exit") {exit}

            my $query = decode_json($line);
            #use Data::Dumper;
            #print Dumper($query);

            # we support hash namespaced queries
            # where
            #  - line = 'config' return the whole config hash
            #  - line = 'config;value' return the value in the config hash
            my $method = $query->{method};
            if (exists $DISPATCH{$method}) {
                $DISPATCH{$method}->($query, $socket);
            } else {
                print STDERR "Unknown method $query->{method}";
                print "undef";
            }
        }
    };
    if($@){
        print STDERR "$line : $@";
        send_output(undef, $socket) if $socket;
    }
}

$logger->info("Stop running\n");

END {
    if ( !$args{h} ) {
        deletepid($PROGRAM_NAME);
    }
}

sub expire {
    my ($query, $socket) = @_;
    my $namespace = $query->{namespace};
    my $logger = pfconfig::log::get_logger;
    my $light = $query->{light};
    if($namespace eq "__all__"){
        $cache->expire_all($light);
    }
    else{
        $logger->info("expiring $namespace");
        $cache->expire($namespace, $light);
    }
    send_output({status => "OK."}, $socket);
}

sub get_from_cache {
    my ($what) = @_;
    my $elem;
    # let's get the top namespace element
    $elem = $cache->get_cache($what);

    return $elem;
}

sub get_element {
    my ($query, $socket) = @_;
    my $logger = pfconfig::log::get_logger;
    my $elem = get_from_cache_or_croak($query->{key}, $socket);
    return unless(defined($elem));
    send_output({element => $elem}, $socket);
}

sub get_hash_element {
    my ($query, $socket) = @_;
    my $logger = pfconfig::log::get_logger;

    my @keys = split ';', $query->{key};

    my $elem = get_from_cache_or_croak($keys[0], $socket);
    return unless(defined($elem));

    # if we want a subnamespace we handle it here
    if($keys[1]){
        my $sub_elem = $elem->{$keys[1]};
        if(defined($sub_elem)){
            $elem = {element => $sub_elem};
        }
        else{
            print STDERR "Unknown key $query->{key}";
            $logger->error("Unknown key $query->{key}");
            $elem = undef;
        }
    }
    else{
        $elem = {element => $elem};
    }
    send_output($elem, $socket);
}

sub get_from_cache_or_croak {
    my ($key, $socket) = @_;
    my $elem = get_from_cache($key);

    if(defined($elem)){
        return $elem;
    }
    else{
        print STDERR "Unknown key in cache $key \n";
        $logger->error("Unknown key $key");
        send_output(undef, $socket);
        return undef;
    }

}

sub get_keys {
    my ($query, $socket) = @_;

    my $elem = get_from_cache_or_croak($query->{key}, $socket);
    return unless(defined($elem));

    my @keys = keys(%{$elem});
    send_output(\@keys, $socket);
}

sub get_key_exists {
    my ($query, $socket) = @_;

    my $elem = get_from_cache_or_croak($query->{key}, $socket);
    return unless(defined($elem));

    my @keys = keys(%{$elem});

    my $key = $query->{search};
    if($key ~~ @keys){
        send_output({result => 1}, $socket);
    }
    else {
        send_output({result => 0}, $socket);
    }

}

sub get_next_key {
    my ($query, $socket) = @_;

    my $elem = get_from_cache_or_croak($query->{key}, $socket) || return;

    my @keys = keys(%{$elem});

    my $last_key = $query->{last_key};

    my $next_key;
    unless($last_key){
        $next_key = $keys[0];
    }
    else{
        my $last_index;
        $last_index = first_index { $_ eq $last_key} @keys ;

        if($last_index >= scalar @keys){
            $next_key = undef;
        }

        $next_key = $keys[$last_index+1];
    }
    send_output({next_key => $next_key}, $socket);
}

sub get_array_element {
    my ($query, $socket) = @_;
    my $logger = pfconfig::log::get_logger;

    my @keys = split ';', $query->{key};

    my $elem = get_from_cache_or_croak($keys[0], $socket) || return;

    # if we want an index we handle it here
    if(defined($keys[1])){
        my $sub_elem = $$elem[$keys[1]];
        if(defined($sub_elem)){
            $elem = {element => $sub_elem};
        }
        else{
            print STDERR "Unknown index in $query->{key}";
            $logger->error("Unknown index in $query->{key}");
            $elem = undef;
        }
    }
    else {
        $elem = {element => $elem};
    }

    send_output($elem, $socket);

}

sub get_array_index_exists {
    my ($query, $socket) = @_;
    my $elem = get_from_cache_or_croak($query->{key}, $socket) || return;

    if(exists($$elem[$query->{index}])) {
        send_output({result => 1}, $socket);
    }
    else{
        send_output({result => 0}, $socket);
    }

}

sub get_array_size {
    my ($query, $socket) = @_;
    my $logger = pfconfig::log::get_logger;
    my $elem = get_from_cache_or_croak($query->{key}, $socket) || return;
    my $size = @$elem;
    send_output({size => $size}, $socket);
}

sub encode_output {
    my ($data) = @_;
    $data = $encoder->encode($data);
    return $data;
}

sub send_output {
    my ($data, $socket) = @_;
    my $encoded_data = encode_output($data);
    my $bytes_to_send = length($encoded_data);
    my $bytes_sent  = send_data_with_length($socket,$encoded_data);
    if($bytes_to_send != $bytes_sent) {
        $logger->error("Could not send all bytes the client. $bytes_sent of $bytes_to_send sent");
    }
}

=head2 server_sleep

=cut

sub server_sleep {
    my ($query, $socket) = @_;
    sleep 10;
}


=head2 normal_sighandler

the signal handler to shutdown the service

=cut

sub normal_sighandler {
    $RUNNING = 0;
}

=back

=head1 AUTHOR

Inverse inc. <info@inverse.ca>

=head1 COPYRIGHT

Copyright (C) 2005-2016 Inverse inc.

=head1 LICENSE

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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
USA.

=cut

