#!/usr/bin/perl 

use utf8;
use warnings;
use Encode;
use open qw(:std :encoding(UTF-8));
no warnings 'utf8';

use English;
use LWP::UserAgent;
use JSON;
use File::Find;
use File::Path qw(make_path);
use File::Basename;
use Socket qw(inet_aton);
use Net::Patricia;
#use Data::Dumper;

my $api_user='USER';
my $api_key ='PASSWORD';
my $host    ='HOST';
my $user_id = $ARGV[0];

exit if (!$user_id);

# Конфигурация логгирования
my $log_file = '/var/log/openvpn/sync_ccd_'.$user_id.'.log';
my $log_level = 'INFO'; # DEBUG, INFO, WARN, ERROR

# Конфигурационные параметры
my $api_url = 'https://'.$host.'/api.php?login='.$api_user.'&api_key='.$api_key.'&get=user&id='.$user_id;
my $ovpn_dir = '/etc/openvpn/server';

# Создаем backup и log директории если не существуют
make_path(dirname($log_file)) unless -d dirname($log_file);

# 1. Выполняем API запрос и получаем JSON
my $json_data = fetch_json($api_url);
unless ($json_data) {
    log_message("ERROR", "Не удалось получить данные от API");
    die "Не удалось получить данные от API\n";
}

# 2. Парсим JSON и извлекаем массив auth
my $auth_data = parse_auth_data($json_data);
if (!$auth_data and !scalar @{$auth_data}) {
    log_message("ERROR", "Не найдены данные auth в JSON ответе");
    die "Не найдены данные auth в JSON ответе\n";
    }

# 3. Ищем конфиги OpenVPN и анализируем их
my @configs = find_ovpn_configs($ovpn_dir);

# 4. Обрабатываем каждый конфиг
foreach my $config (@configs) {
    process_ovpn_config($config, $auth_data);
}

log_message("INFO", "Обработка завершена успешно!");
print "Обработка завершена успешно!\n";

# Функция логирования
sub log_message {
    my ($level, $message) = @_;

    # Уровни логирования: DEBUG < INFO < WARN < ERROR
    my %levels = (
        'DEBUG' => 1,
        'INFO'  => 2,
        'WARN'  => 3,
        'ERROR' => 4
    );

    return unless $levels{$level} >= $levels{$log_level};

    my ($sec,$min,$hour,$mday,$mon,$year) = (localtime())[0,1,2,3,4,5];
    $mon += 1; $year += 1900;

    open my $fh, '>>', $log_file or do {
        warn "Не могу открыть лог файл $log_file: $!\n";
        return;
    };
    printf $fh "%04d%02d%02d-%02d%02d%02d [%d] [%s] %s\n",$year,$mon,$mday,$hour,$min,$sec,$$,$level,$message;
    close $fh;
}

sub ip_and_mask_to_cidr {
    my ($ip, $mask) = @_;

    # Преобразуем IP и маску в 32-битные числа (в сетевом порядке)
    my $ip_n   = unpack("N", inet_aton($ip));
    my $mask_n = unpack("N", inet_aton($mask));

    # Проверка: маска должна быть корректной (последовательность 1, затем 0)
    my $binary_mask = sprintf("%032b", $mask_n);
    if ($binary_mask !~ /^1*0*$/) {
        log_message("ERROR", "Некорректная маска подсети: $mask");
        die "Некорректная маска подсети: $mask\n";
    }

    # Считаем количество единиц в маске — это /24, /16 и т.д.
    my $prefix_len = ($binary_mask =~ tr/1/1/);

    # Применяем маску к IP, чтобы получить сетевой адрес
    my $network_n = $ip_n & $mask_n;

    # Преобразуем обратно в dotted-decimal
    my $network_ip = join '.', map { ($network_n >> (24 - 8 * $_)) & 255 } 0..3;

    # Возвращаем в формате CIDR
    return "$network_ip/$prefix_len";
}

sub fetch_json {
    my $url = shift;

    my $ua = LWP::UserAgent->new;
    $ua->timeout(30);
    $ua->env_proxy;

    log_message("DEBUG", "Выполняем запрос к API: $url");
    my $response = $ua->get($url);

    if ($response->is_success) {
        log_message("DEBUG", "API запрос успешен");
        return $response->decoded_content;
    } else {
        my $error = "Ошибка запроса: " . $response->status_line;
        log_message("ERROR", $error);
        warn "$error\n";
        return undef;
    }
}

sub parse_auth_data {
    my $json_str = shift;
    my @result;

    eval {
        my $json = decode_json($json_str);
        @result = @{$json->{auth}} if $json && ref $json eq 'HASH' && $json->{auth};
    };

    if ($@) {
        my $error = "Ошибка парсинга JSON: $@";
        log_message("ERROR", $error);
        warn "$error\n";
        return undef;
    }

    log_message("DEBUG", "Найдено " . scalar(@result) . " записей auth");
    return \@result;
}

sub find_ovpn_configs {
    my $dir = shift;
    my @configs;

    find(sub {
        return unless -f && /\.conf$/;
        push @configs, $File::Find::name;
    }, $dir);

    log_message("DEBUG", "Найдено " . scalar(@configs) . " конфигурационных файлов OpenVPN");
    return @configs;
}

sub process_ovpn_config {
    my $config_file = shift;
    my $ip_list = shift;

    log_message("INFO", "Обрабатываем конфиг: $config_file");
    print "Обрабатываем конфиг: $config_file\n";

    # Читаем конфиг и находим ccd directory и сеть
    my ($ccd_dir, $network, $network_mask) = parse_ovpn_config($config_file);

    unless ($ccd_dir && $network) {
        log_message("WARN", "Не найдены ccd directory или network в $config_file");
        return;
    }

    my $ServerNet = new Net::Patricia;
    $ServerNet->add_string($network);
    log_message("INFO", "Found server network: $network (mask: $network_mask) and ccd: $ccd_dir");
    print "Found server network: $network (mask: $network_mask) and ccd: $ccd_dir\n";

    # Преобразуем относительный пути в абсолютный
    unless ($ccd_dir =~ m{^/}) {
        my $config_dir = dirname($config_file);
        $ccd_dir = "$config_dir/$ccd_dir";
    }

    # Создаем CCD директорию если не существует
    make_path($ccd_dir) unless -d $ccd_dir;

    # Обрабатываем каждый auth record
    foreach my $auth (@$ip_list) {
        next unless $auth->{description} && $auth->{ip};
        next if (!$ServerNet->match_string($auth->{ip}));
        my $username = $auth->{description};
        my $ip = $auth->{ip};
        process_ccd_file($ccd_dir, $username, $ip, $network_mask);
    }
}

sub parse_ovpn_config {
    my $config_file = shift;
    my ($ccd_dir, $network, $network_mask);

    open my $fh, '<', $config_file or do {
        my $error = "Не могу открыть $config_file: $!";
        log_message("ERROR", $error);
        warn "$error\n";
        return (undef, undef);
    };

    while (my $line = <$fh>) {
        chomp $line;

        # Ищем client-config-dir
        if ($line =~ /^\s*client-config-dir\s+(\S+)/i) {
            $ccd_dir = $1;
        }

        # Ищем server directive
        if ($line =~ /^\s*server\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)/i) {
            $network = ip_and_mask_to_cidr($1,$2) if ($1 and $2);
            $network_mask = $2 if ($2);
        }

        # Ищем server directive
        if ($line =~ /^\s*ifconfig\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)/i) {
            $network = ip_and_mask_to_cidr($1,$2) if ($1 and $2);
            $network_mask = $2 if ($2);
        }

        last if $ccd_dir && $network;
    }

    close $fh;

    return ($ccd_dir, $network, $network_mask);
}

sub process_ccd_file {
    my $ccd_dir = shift;
    my $username = shift;
    my $ip = shift;
    my $network_mask = shift;

    return if (!$username or !$ip or !$network_mask);

    my $ccd_file = "$ccd_dir/$username";

    my $log_msg = "Обрабатываем пользователя: $username, IP: $ip";
    log_message("INFO", $log_msg);
    print "$log_msg ...";


    # Читаем существующий файл или создаем новый
    my @lines;
    if (-f $ccd_file) {
        open my $fh, '<', $ccd_file or do {
            my $error = "Не могу открыть $ccd_file: $!";
            log_message("ERROR", $error);
            warn "$error\n";
            return;
        };
        @lines = <$fh>;
        close $fh;
    }

    my $changed = 0;

    # Ищем или добавляем ifconfig-push
    my $found = 0;
    my $new_ifconfig = "ifconfig-push $ip $network_mask";

    for my $i (0..$#lines) {
        if ($lines[$i] =~ /^ifconfig-push/) {
            if ($lines[$i] !~ /^\s*$new_ifconfig\s*$/) {
                my $replace_msg = "Заменяем: $lines[$i] на: $new_ifconfig";
                log_message("INFO", $replace_msg);
                print "$replace_msg\n";
                $lines[$i] = "$new_ifconfig\n";
                $changed = 1;
            } else {
                my $current_msg = "ifconfig-push уже актуален для $username";
                log_message("INFO", $current_msg);
                print "$current_msg\n";
            }
            $found = 1;
            last;
        }
    }

    # Если не нашли, добавляем новую строку
    unless ($found) {
        my $add_msg = "Добавляем ifconfig-push для $username";
        log_message("INFO", $add_msg);
        print "$add_msg\n";
        $changed = 1;
        push @lines, "$new_ifconfig\n";
    }

    if ($changed) {
        # Записываем обратно в файл
        open my $fh, '>', $ccd_file or do {
            my $error = "Не могу записать в $ccd_file: $!";
            log_message("ERROR", $error);
            warn "$error\n";
            return;
        };
        print $fh @lines;
        close $fh;
        # Ставим права на конфиг
        chmod 0664, $ccd_file or do {
            my $error = "chmod failed: $!";
            log_message("ERROR", $error);
            die "$error\n";
        };
        my $ccd_user = 'nobody';
        my $ccd_group = 'www-data';
        my $uid = getpwnam($ccd_user) or do {
            my $error = "Пользователь $ccd_user не найден";
            log_message("ERROR", $error);
            die "$error\n";
        };
        my $gid = getgrnam($ccd_group) or do {
            my $error = "Группа $ccd_group не найдена";
            log_message("ERROR", $error);
            die "$error\n";
        };
        chown $uid, $gid, $ccd_file or do {
            my $error = "chown failed: $!";
            log_message("ERROR", $error);
            die "$error\n";
        };

        my $success_msg = "Файл $ccd_file успешно обновлен";
        log_message("INFO", $success_msg);
        print "$success_msg\n";
        }
}

# Обработка ошибок
$SIG{__DIE__} = sub {
    my $error = shift;
    warn "Критическая ошибка: $error\n";
    exit 1;
};

