<?php

/*
 * Copyright (C) 2016 Deciso B.V.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
 * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY,
 * OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */

namespace OPNsense\Firewall;

use OPNsense\Core\Config;

/**
 * Class Plugin
 * @package OPNsense\Firewall
 */
class Plugin
{
    private $gateways = null;
    private $anchors = [];
    private $filterRules = [];
    private $natRules = [];
    private $interfaceMapping = [];
    private $gatewayMapping = [];
    private $systemDefaults = [];
    private $tables = [];
    private $ifconfigDetails = [];

    /**
     * init firewall plugin component
     */
    public function __construct()
    {
        $this->systemDefaults = array("filter" => [], "forward" => [], "nat" => []);
        if (!empty(Config::getInstance()->object()->system->disablereplyto)) {
            $this->systemDefaults['filter']['disablereplyto'] = true;
        }
        if (!empty(Config::getInstance()->object()->system->skip_rules_gw_down)) {
            $this->systemDefaults['filter']['skip_rules_gw_down'] = true;
        }
        if (empty(Config::getInstance()->object()->system->disablenatreflection)) {
            $this->systemDefaults['forward']['natreflection'] = "enable";
        }
        if (!empty(Config::getInstance()->object()->system->enablebinatreflection)) {
            $this->systemDefaults['nat']['natreflection'] = "enable";
        }
        if (!empty(Config::getInstance()->object()->system->enablenatreflectionhelper)) {
            $this->systemDefaults['forward']['enablenatreflectionhelper'] = true;
            $this->systemDefaults['nat']['enablenatreflectionhelper'] = true;
        }
    }

    /**
     * set interface mapping to use
     * @param array $mapping named array
     */
    public function setInterfaceMapping(&$mapping)
    {
        $this->interfaceMapping = $mapping;
        // generate virtual IPv6 interfaces
        foreach ($this->interfaceMapping as $key => &$intf) {
            if (!empty($intf['ipaddrv6']) && ($intf['ipaddrv6'] == '6rd' || $intf['ipaddrv6'] == '6to4')) {
                $device = "{$key}_stf";
                // create new interface
                $this->interfaceMapping[$device] = [];
                $this->interfaceMapping[$device]['ifconfig']['ipv6'] = $intf['ifconfig']['ipv6'];
                $this->interfaceMapping[$device]['gatewayv6'] = $intf['gatewayv6'];
                $this->interfaceMapping[$device]['is_IPv6_override'] = true;
                $this->interfaceMapping[$device]['descr'] = $intf['descr'];
                $this->interfaceMapping[$device]['if'] = $device;
                // link original interface
                $intf['IPv6_override'] = $device;
            }
        }
    }

    /**
     * set defined gateways (route-to)
     * @param  \OPNsense\Routing\Gateways $gateways object
     */
    public function setGateways(\OPNsense\Routing\Gateways $gateways)
    {
        $this->gateways = $gateways;
        foreach ($gateways->gatewaysIndexedByName(false, true) as $key => $gw) {
            if (!empty($gw['gateway_interface']) || Util::isIpAddress($gw['gateway'] ?? null)) {
                $this->gatewayMapping[$key] = [
                    "interface" => $gw['if'],
                    "proto" => $gw['ipprotocol'],
                    "type" => "gateway"
                ];
                if (!empty($gw['gateway']) && Util::isIpAddress($gw['gateway'])) {
                    $this->gatewayMapping[$key]['logic'] = "route-to ( {$gw['if']} {$gw['gateway']} )";
                    $this->gatewayMapping[$key]['gateway'] = $gw['gateway'];
                } else {
                    $this->gatewayMapping[$key]['logic'] = "route-to {$gw['if']}";
                }
            }
        }
    }

    /**
     * @return \OPNsense\Routing\Gateways gateway object
     */
    public function getGateways(): ?\OPNsense\Routing\Gateways
    {
        return $this->gateways;
    }

    /**
     * set defined gateway groups (route-to)
     * @param array $groups named array
     */
    public function setGatewayGroups($groups)
    {
        if (is_array($groups)) {
            foreach ($groups as $key => $gwgr) {
                $routeto = [];
                $proto = null;
                /* All route-to tags must match, either {(if1 ip1), (if2 ip2)} or {if1 if2} */
                $mode = null;
                foreach ($gwgr as $gw) {
                    if ($proto !== null && $proto != $gw['ipprotocol']) {
                        /* protocols must match */
                        continue;
                    }
                    $proto = $gw['ipprotocol'];
                    $gwweight = empty($gw['weight']) ? 1 : $gw['weight'];
                    if ((Util::isIpAddress($gw['gwip']) && !empty($gw['int'])) && ($mode === null || $mode === 'gw')) {
                        $routeto[] = rtrim(str_repeat(" ( {$gw['int']} {$gw['gwip']} ) ,", $gwweight), ',');
                        $mode = 'gw'; /* gateway [ip] mode */
                    } elseif ($mode === null || $mode === 'if') {
                        $routeto[] = rtrim(str_repeat(" {$gw['int']} ,", $gwweight), ',');
                        $mode = 'if'; /* interface [ip] mode */
                    }
                }
                if (count($routeto) > 0) {
                    $routetologic = "route-to {" . implode(' , ', $routeto) . "}";
                    if (!empty($gwgr[0]['poolopts'])) {
                        // Since Gateways->getGroups() returns detail items, we have no other choice than
                        // to copy top level attributes into the details if they matter (poolopts)
                        $routetologic .= " {$gwgr[0]['poolopts']} ";
                    } elseif (count($routeto) > 1) {
                        $routetologic .= " round-robin ";
                        if (!empty(Config::getInstance()->object()->system->lb_use_sticky)) {
                            $routetologic .= " sticky-address ";
                        }
                    }
                    $this->gatewayMapping[$key] = ["logic" => $routetologic, "proto" => $proto, "type" => "group"];
                }
            }
        }
    }

    /**
     *  Fetch gateway
     *  @param string $gw gateway name
     */
    public function getGateway($gw)
    {
        return $this->gatewayMapping[$gw];
    }

    /**
     * @return array
     */
    public function getInterfaceMapping()
    {
        foreach ($this->interfaceMapping as $intfkey => $intf) {
            // suppress virtual ipv6 interfaces
            if (empty($intf['is_IPv6_override'])) {
                yield $intfkey => $intf;
            }
        }
    }

    /**
     * link parsed ifconfig output
     * @param array $ifconfig from legacy_interfaces_details()
     */
    public function setIfconfigDetails($ifconfig)
    {
        $this->ifconfigDetails = $ifconfig;
    }

    /**
     * @return array
     */
    public function getIfconfigDetails()
    {
        return $this->ifconfigDetails;
    }


    /**
     * register anchor
     * @param string $name anchor name
     * @param string $type anchor type (fw for filter, other options are nat,rdr,binat)
     * @param string $priority sort order from low to high
     * @param string $placement placement head,tail
     * @param bool $quick
     * @return null
     */
    public function registerAnchor($name, $type = "fw", $priority = 0, $placement = "tail", $quick = false)
    {
        $anchorKey = sprintf("%s.%s.%08d.%08d", $type, $placement, $priority, count($this->anchors));
        $this->anchors[$anchorKey] = ['name' => $name, 'quick' => $quick];
        ksort($this->anchors);
    }

    /**
     * fetch anchors as text (pf ruleset part)
     * @param string $types anchor types (fw for filter, other options are nat,rdr,binat. comma-separated)
     * @param string $placement placement head,tail
     * @return string
     */
    public function anchorToText($types = "fw", $placement = "tail")
    {
        $result = "";
        foreach (explode(',', $types) as $type) {
            foreach ($this->anchors as $anchorKey => $anchor) {
                if (strpos($anchorKey, "{$type}.{$placement}") === 0) {
                    $result .= $type == "fw" ? "" : "{$type}-";
                    $result .= "anchor \"{$anchor['name']}\"";
                    if ($anchor['quick']) {
                        $result .= " quick";
                    }
                    $result .= "\n";
                }
            }
        }
        return $result;
    }

    /**
     * register a filter rule
     * @param int $prio priority
     * @param array $conf configuration
     * @param array $defaults merge these defaults when provided
     */
    public function registerFilterRule($prio, $conf, $defaults = null)
    {
        if (!empty($this->systemDefaults['filter'])) {
            $conf = array_merge($this->systemDefaults['filter'], $conf);
        }
        if ($defaults != null) {
            $conf = array_merge($defaults, $conf);
        }
        if (empty($conf['label'])) {
            // generated rule, has no label
            $rule_hash = Util::calcRuleHash($conf);
            $conf['label'] = $rule_hash;
        }
        $conf['#priority'] = $prio;
        $rule = new FilterRule($this->interfaceMapping, $this->gatewayMapping, $conf);
        if (empty($this->filterRules[$prio])) {
            $this->filterRules[$prio] = [];
        }
        $this->filterRules[$prio][] = $rule;
    }

    /**
     * register a Forward (rdr) rule
     * @param int $prio priority
     * @param array $conf configuration
     */
    public function registerForwardRule($prio, $conf)
    {
        if (!empty($this->systemDefaults['forward'])) {
            foreach ($this->systemDefaults['forward'] as $key => $val) {
                if (empty($conf[$key])) {
                    $conf[$key] = $val;
                }
            }
        }
        $rule = new ForwardRule($this->interfaceMapping, $conf);
        if (empty($this->natRules[$prio])) {
            $this->natRules[$prio] = [];
        }
        $this->natRules[$prio][] = $rule;
    }

    /**
     * register a destination Nat rule
     * @param int $prio priority
     * @param array $conf configuration
     */
    public function registerDNatRule($prio, $conf)
    {
        if (!empty($this->systemDefaults['nat'])) {
            $conf = array_merge($this->systemDefaults['nat'], $conf);
        }
        $rule = new DNatRule($this->interfaceMapping, $conf);
        if (empty($this->natRules[$prio])) {
            $this->natRules[$prio] = [];
        }
        $this->natRules[$prio][] = $rule;
    }

    /**
     * register a destination Nat rule
     * @param int $prio priority
     * @param array $conf configuration
     */
    public function registerSNatRule($prio, $conf)
    {
        $rule = new SNatRule($this->interfaceMapping, $conf);
        if (empty($this->natRules[$prio])) {
            $this->natRules[$prio] = [];
        }
        $this->natRules[$prio][] = $rule;
    }

    /**
     * register an Npt rule
     * @param int $prio priority
     * @param array $conf configuration
     */
    public function registerNptRule($prio, $conf)
    {
        $rule = new NptRule($this->interfaceMapping, $conf);
        if (empty($this->natRules[$prio])) {
            $this->natRules[$prio] = [];
        }
        $this->natRules[$prio][] = $rule;
    }

    /**
     * filter rules to text
     * @return string
     */
    public function outputFilterRules()
    {
        $output = "";
        ksort($this->filterRules);
        foreach ($this->filterRules as $prio => $ruleset) {
            $output .= "# [prio: {$prio}]\n";
            foreach ($ruleset as $rule) {
                $output .= (string)$rule;
            }
        }
        return $output;
    }

    /**
     * iterate through registered rules
     * @return Iterator
     */
    public function iterateFilterRules()
    {
        ksort($this->filterRules);  /* sort rules by priority */
        foreach ($this->filterRules as $prio => $ruleset) {
            foreach ($ruleset as $rule) {
                 yield $prio => $rule;
            }
        }
    }

    /**
     * filter rules to text
     * @return string
     */
    public function outputNatRules()
    {
        $output = "";
        ksort($this->natRules);
        foreach ($this->natRules as $prio => $ruleset) {
            $output .= "# [prio: {$prio}]\n";
            foreach ($ruleset as $rule) {
                $output .= (string)$rule;
            }
        }
        return $output;
    }
    /**
     * register a pf table
     * @param string $name table name
     * @param boolean $persist persistent
     * @param string $file get table from file
     */
    public function registerTable($name, $persist = false, $file = null)
    {
        $this->tables[] = array('name' => $name, 'persist' => $persist, 'file' => $file);
    }

    /**
     * fetch tables as text (pf tables part)
     * @return string
     */
    public function tablesToText()
    {
        $result = "";
        foreach ($this->tables as $table) {
            $result .= "table <{$table['name']}>";
            if ($table['persist']) {
                $result .= " persist";
            }
            if (!empty($table['file'])) {
                $result .= " file \"{$table['file']}\"";
            }
            $result .= "\n";
        }
        return $result;
    }
}
