<?php
/********************************************************************
KeaKeeper

Copyright (C) 2017 DesigNET, INC.

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
********************************************************************/


define('CONF', '/etc/kea/kea.conf');

/*****************************************************************************
* Class:  KeaConf
*
* [Description]
*   Class to read and use kea.conf
*****************************************************************************/
class KeaConf {
    public $result = false;
    public $all;
    public $dhcp4;
    public $dhcp6;
    public $err = ['e_msg' => '', 'e_log' => ''];
    private $path;

    /************************************************************************
    * Method         : __construct
    * Description    : Check kea.conf file and set property
    * args           : None
    * return         : None
    ************************************************************************/
    public function __construct()
    {
        global $appini;

        /* check */
        $ret = $this->_check_file($appini);

        if ($ret === false) {
            return;
        }

        /* read */
        $this->_decode_file($this->path);
    }

    /************************************************************************
    * Method         : search_subnet4
    * Description    : Check subnet4 and forward search
    * args           : $subnet
    * return         : $result
    ************************************************************************/
    public function search_subnet4($cond = null, $mode = 'all')
    {
        /* check presence dhcp4 configuration */
        if (empty($this->dhcp4) || empty($this->dhcp4['subnet4'][0])) {
            $form = _("No subnet4 setting(%s).");
            $this->err['e_msg'] = sprintf($form, $this->path);
            $log_msg = "No subnet4 setting(%s).";
            $this->err['e_log'] = sprintf($log_msg, $this->path);

            return false;
        }

        /* loop dhcp4 subnet4 */
        $result = [];
        $key_list = ["id" => "", "pools"=>"", "subnet"=>""];

        foreach ($this->dhcp4['subnet4'] as $i => $sub) {
            /* skip subnet4 without subnet key */
            if (!array_key_exists('subnet', $sub)) {
                continue;
            }

            switch ($mode) {
                case "foward":
                    /* quote regular expression characters */
                    $esc_cond = preg_quote($cond);

                    if (preg_match("#^$esc_cond#", $sub['subnet']) != 0) {
                        $result[] =
                            array_merge($key_list, $this->dhcp4['subnet4'][$i]);
                    }
                    break;

                case "exact":
                    if ($cond === $sub['subnet']) {
                        $result[] =
                            array_merge($key_list, $this->dhcp4['subnet4'][$i]);
                    }
                    break;

                case "all":
                default:
                    $result[] =
                            array_merge($key_list, $this->dhcp4['subnet4'][$i]);
                    break;
            }
        }

        /* check result */
        if (empty($result)) {
            $this->err['e_msg'] = _('No result.');
            $this->err['e_log'] = 'No search result for subnet4.';

            return false;
        }

        return $result;
    }

    /************************************************************************
    * Method         : search_subnet6
    * Description    : Check subnet6 and forward search
    * args           : $subnet
    * return         : $result
    ************************************************************************/
    public function search_subnet6($cond = null, $mode = 'all')
    {
        /* check presence dhcp6 configuration */
        if (empty($this->dhcp6) || empty($this->dhcp6['subnet6'][0])) {
            $form = _("No subnet6 setting(%s).");
            $this->err['e_msg'] = sprintf($form, $this->path);
            $log_msg = "No subnet6 setting(%s).";
            $this->err['e_log'] = sprintf($log_msg, $this->path);

            return false;
        }

        /* loop dhcp6 subnet6 */
        $result = [];
        $key_list = ["id" => "", "pools"=>"", "subnet"=>""];

        foreach ($this->dhcp6['subnet6'] as $i => $sub) {
            /* skip subnet6 without subnet key */
            if (!array_key_exists('subnet', $sub)) {
                continue;
            }

            list($addr, $mask) = explode("/", $sub['subnet']);
            $addr = inet_ntop(inet_pton($addr));
            $converted_subnet = $addr . "/" . $mask;

            switch ($mode) {
                case "foward":
                    /* quote regular expression characters */
                    $esc_cond = preg_quote($cond);

                    if (preg_match("#^$esc_cond#", $converted_subnet) != 0) {
                        $result[] =
                            array_merge($key_list, $this->dhcp6['subnet6'][$i]);
                    }
                    break;

                case "exact":

                    if ($cond === $converted_subnet) {
                        $result[] =
                            array_merge($key_list, $this->dhcp6['subnet6'][$i]);
                    }
                    break;

                case "all":
                default:
                    $result[] =
                            array_merge($key_list, $this->dhcp6['subnet6'][$i]);
                    break;
            }
        }

        /* check result */
        if (empty($result)) {
            $this->err['e_msg'] = _('No result.');
            $this->err['e_log'] = 'No search result for subnet6.';

            return false;
        }

        return $result;
    }

    /************************************************************************
    * Method         : check_subnet4
    * Description    : Investigate whether subnet is in keaconf(DHCPv6)
    * args           : $subnet
    * return         : true/false
    ************************************************************************/
    public function check_subnet4($subnet)
    {
        /* When reading of keaconf succeeded */
        foreach ($this->dhcp4['subnet4'] as $one) {

            /* When a matching subnet is in keaconf */
            if (array_key_exists('subnet', $one) && $one['subnet'] == $subnet) {

                /* Check if there is subnet id */
                if (array_key_exists('id', $one)) {
                    return true;
                }
            }
            /* When matching subnet is not in keaconf */
            $form = _('No such subnet(%s)');
            $this->err['e_msg'] = sprintf($form, $subnet);
            $this->err['e_log'] = "No such subnet(" . $subnet . ").";
            return false;
        }
        return false;
    }

    /************************************************************************
    * Method         : check_subnet6
    * Description    : Investigate whether subnet is in keaconf(DHCPv6)
    * args           : $subnet
    * return         : true/false
    ************************************************************************/
    public function check_subnet6($subnet)
    {
        /* When reading of keaconf succeeded */
        foreach ($this->dhcp6['subnet6'] as $one) {

            /* When a matching subnet is in keaconf */
            if (array_key_exists('subnet', $one) && $one['subnet'] == $subnet) {

                /* Check if there is subnet id */
                if (array_key_exists('id', $one)) {
                    return true;
                }
            }
            /* When matching subnet is not in keaconf */
            $form = _('No such subnet(%s)');
            $this->err['e_msg'] = sprintf($form, $subnet);
            $this->err['e_log'] = "No such subnet(" . $subnet . ").";
            return false;
        }
        return false;
    }

    /************************************************************************
    * Method         : check_id_subnet4
    * Description    : Check proper id and subnet(DHCPv4)
    * args           : $subnet
    * return         : $result
    ************************************************************************/
    public function check_id_subnet4($id, $subnet)
    {
        /* When reading of keaconf succeeded */
        foreach ($this->dhcp4['subnet4'] as $one) {

            if (array_key_exists('id', $one) && $one['id'] == $id) {
                /* When matching subnet_id is in keaconf */
                if ($one['subnet'] == $subnet) {

                    /* When a matching subnet is in keaconf */
                    return true;
                }
                /* When matching subnet is not in keaconf */
                $form = _('No such subnet(%s)');
                $this->err['e_msg'] = sprintf($form, $subnet);
                $this->err['e_log'] = "No such subnet(" . $subnet . ").";

                return false;
            }
        }

        /* When matching subnet_id is not in keaconf */
        $form = _('No such subnet id(%s)');
        $this->err['e_msg'] = sprintf($form, $id);
        $this->err['e_log'] = "No such subnet id(" . $id . ").";

        return false;
    }

    /************************************************************************
    * Method         : check_id_subnet6
    * Description    : Check proper id and subnet(DHCPv6)
    * args           : $subnet
    * return         : $result
    ************************************************************************/
    public function check_id_subnet6($id, $subnet)
    {
        /* When reading of keaconf succeeded */
        foreach ($this->dhcp6['subnet6'] as $one) {

            if (array_key_exists('id', $one) && $one['id'] == $id) {
                /* When matching subnet_id is in keaconf */


                $num = substr_count($one['subnet'], "/");
                if ($num != 1) {
                    return false;
                }

                list($addr, $mask) = explode("/", $one['subnet']);

                $ret = filter_var($addr, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6);
                if ($ret === false) {
                    return false;
                }

                $addr = inet_ntop(inet_pton($addr));
                $one['subnet'] = $addr . "/" . $mask;

                if ($one['subnet'] == $subnet) {

                    /* When a matching subnet is in keaconf */
                    return true;
                }
                /* When matching subnet is not in keaconf */
                $form = _('No such subnet(%s)');
                $this->err['e_msg'] = sprintf($form, $subnet);
                $this->err['e_log'] = "No such subnet(" . $subnet . ").";

                return false;
            }
        }

        /* When matching subnet_id is not in keaconf */
        $form = _('No such subnet id(%s)');
        $this->err['e_msg'] = sprintf($form, $id);
        $this->err['e_log'] = "No such subnet id(" . $id . ").";

        return false;
    }

    /************************************************************************
    * Method         : get_subnet_id
    * Description    : Get subnet4 from keaconf
    * args           : $subnet
    * return         : $subnet_id
    ************************************************************************/
    public function get_subnet_id($subnet)
    {
        /* When reading of keaconf succeeded */
        foreach ($this->dhcp4['subnet4'] as $one) {

            /* When matching subnet_id is in keaconf */
            if (array_key_exists('subnet', $one) && $one['subnet'] == $subnet) {

                if (array_key_exists('id', $one)) {
                    $subnet_id = $one['id'];
                    return $subnet_id;
                }
            }
        }
        return NULL;
    }

    /************************************************************************
    * Method         : get_subnet_idv6
    * Description    : Get subnet4 from keaconf
    * args           : $subnet
    * return         : $subnet_id
    ************************************************************************/
    public function get_subnet_idv6($subnet)
    {
        /* When reading of keaconf succeeded */
        foreach ($this->dhcp6['subnet6'] as $one) {

            /* When matching subnet_id is in keaconf */
            if (array_key_exists('subnet', $one)) {
                list($addr, $mask) = explode("/", $one['subnet']);
                $addr = inet_ntop(inet_pton($addr));
                $converted_subnet = $addr . "/" . $mask;

                if ($converted_subnet == $subnet) {
                    if (array_key_exists('id', $one)) {
                        $subnet_id = $one['id'];
                        return $subnet_id;
                    }
                }

            }
        }
        return NULL;
    }

    /************************************************************************
    * Method         : get_pools
    * Description    : Check subnet4 and get subnet4's pools
    * args           : $subnet
    * return         : $result
    ************************************************************************/
    public function get_pools($subnet)
    {
        $sub4section = $this->search_subnet4($subnet, 'exact');

        $pools = $sub4section[0]['pools'];
        if ($pools === "") {
            return true;
        }

        /* check format */
        if (is_array($sub4section)) {
            foreach ($pools as $pool_key => $pool_val) {
                $pool_arr = explode(' - ', $pool_val['pool']);
                if (count($pool_arr) != 2) {
                    $this->err['e_msg'] = _('Invalid pool.');
                    $this->err['e_log'] = 'The pool format is invalid.';
                    return false;
                }

                /* check ipv4 format(min) */
                $ret = ipv4Validate::run($pool_arr[0]);
                if ($ret === false) {
                     $this->err['e_msg'] = _('Invalid pool.');
                    $this->err['e_log'] = 'The pool format is invalid.';
                    return false;
                }

                /* check ipv4 format(max) */
                $ret = ipv4Validate::run($pool_arr[1]);
                if ($ret === false) {
                    $this->err['e_msg'] = _('Invalid pool.');
                    $this->err['e_log'] = 'The pool format is invalid.';
                    return false;
                }
            }
        }
        return $pools;
    }

    /************************************************************************
    * Method         : get_pools6
    * Description    : Check subnet6 and get subnet6's pools
    * args           : $subnet
    * return         : $result
    ************************************************************************/
    public function get_pools6($subnet)
    {
        $sub6section = $this->search_subnet6($subnet, 'exact');

        $pools = $sub6section[0]['pools'];
        if ($pools === "") {
            return true;
        }

        $new_pools = [];
        /* check format */
        if (is_array($sub6section)) {
            foreach ($pools as $pool_key => $pool_val) {
                $pool_arr = explode(' - ', $pool_val['pool']);
                if (count($pool_arr) != 2) {
                    $this->err['e_msg'] = _('Invalid pool.');
                    $this->err['e_log'] = 'The pool format is invalid.';
                    return false;
                }

                /* check ipv6 format(min) */
                $ret = ipv6Validate::run($pool_arr[0]);
                if ($ret === false) {
                     $this->err['e_msg'] = _('Invalid pool.');
                    $this->err['e_log'] = 'The pool format is invalid.';
                    return false;
                }

                /* check ipv6 format(max) */
                $ret = ipv6Validate::run($pool_arr[1]);
                if ($ret === false) {
                    $this->err['e_msg'] = _('Invalid pool.');
                    $this->err['e_log'] = 'The pool format is invalid.';
                    return false;
                }
                /* Fit the address format of the pool in kea.conf */
                $new_pools[$pool_key]['pool'] = inet_ntop(inet_pton($pool_arr[0])) . " - " . inet_ntop(inet_pton($pool_arr[1]));
            }
        }
        return $new_pools;
    }

    /************************************************************************
    * Method         : _check_file
    * Description    : Check kea.conf's readability
    * args           : $appini
    * return         : true or false
    ************************************************************************/
    private function _check_file ($appini)
    {
        /* check config key */
        if (!array_key_exists('conf', $appini)) {
            $this->path = CONF;
        } else if (!array_key_exists('path', $appini['conf'])) {
            $this->path = CONF;
        }

        /* replace variable */
        if (empty($this->path)) {
            $this->path = $appini['conf']['path'];
        }

        /* check presence of file */
        if (!is_file($this->path)) {
            $form = _("No such file(%s).");
            $this->err['e_msg'] = sprintf($form, $this->path);
            $log_msg = "No such file(%s).";
            $this->err['e_log'] = sprintf($log_msg, $this->path);

            return false;
        }

        /* check readable file */
        if (!is_readable($this->path)) {
            $form = _("Cannot read file(%s).");
            $this->err['e_msg'] = sprintf($form, $this->path);
            $log_msg = "Cannot read file(%s).";
            $this->err['e_log'] = sprintf($log_msg, $this->path);

            return false;
        }

        $this->result = true;
        return true;
    }

    /************************************************************************
    * Method         : _decode_file
    * Description    : Decode kea.conf and keep config
    * args           : $path - kea.conf's file path
    * return         : false (when decode error)
    ************************************************************************/
    private function _decode_file ($path)
    {
        /* open config file */
        $file = fopen($path, 'r');

        if ($file === false) {
            $log_msg = "failed to open config file($path).";
            throw new SyserrException($log_msg);
        }

        /* read file and remove coment line */
        $conf = '';
        while ($line = fgets($file)) {
            $trimed = trim($line);

            if (preg_match('/^#/', $trimed) === 0) {
                $conf = $conf . $line;
            }
        }

        if (!feof($file)) {
            $log_msg = "failed to read config file($path).";
            throw new SyserrException($log_msg);
        }

        fclose($file);

        /* JSON to associative array */
        $this->all = json_decode($conf, true);
        if ($this->all === null) {
            $this->result = false;
            $form = _("Cannot read file. Syntax error?(%s)");
            $this->err['e_msg'] = sprintf($form, $this->path);
            $log_msg = "Cannot read file. Syntax error?(%s)";
            $this->err['e_log'] = sprintf($log_msg, $this->path);

            return false;
        }

        /* split dhcp4 and dhcp6 and keep other properties */
        if (array_key_exists('Dhcp4', $this->all)) {
            $this->dhcp4 = $this->all['Dhcp4'];
        }

        if (array_key_exists('Dhcp6', $this->all)) {
            $this->dhcp6 = $this->all['Dhcp6'];
        }
    }
}
