<?php

/*
 * Copyright (C) 2021 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\IPsec\Api;

use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Config;

/**
 * Class TunnelController
 * @package OPNsense\IPsec\Api
 */
class TunnelController extends ApiControllerBase
{
    /***
     * search phase 1 entries in legacy config returning a standard structure as we use in the mvc variant
     */
    public function searchPhase1Action()
    {
        $ph1type = ['ikev1' => 'IKE', 'ikev2' => 'IKEv2', 'ike' => 'auto'];
        $ph1algos = [
              'aes' => 'AES',
              'aes128gcm16' => '128 bit AES-GCM with 128 bit ICV',
              'aes192gcm16' => '192 bit AES-GCM with 128 bit ICV',
              'aes256gcm16' => '256 bit AES-GCM with 128 bit ICV',
              'camellia' => 'Camellia',
              'blowfish' => 'Blowfish',
              '3des' => '3DES',
              'cast128' => 'CAST128',
              'des' => 'DES'
        ];
        $ph1authmethods = [
            'hybrid_rsa_server' => 'Hybrid RSA + Xauth',
            'xauth_rsa_server' => 'Mutual RSA + Xauth',
            'xauth_psk_server' => 'Mutual PSK + Xauth',
            'eap-tls' => 'EAP-TLS',
            'psk_eap-tls' => 'RSA (local) + EAP-TLS (remote)',
            'eap-mschapv2' => 'EAP-MSCHAPV2',
            'rsa_eap-mschapv2' => 'Mutual RSA + EAP-MSCHAPV2',
            'eap-radius' => 'EAP-RADIUS',
            'rsasig' => 'Mutual RSA',
            'pubkey' => 'Mutual Public Key',
            'pre_shared_key' => 'Mutual PSK'
        ];
        $items = [];
        $config = Config::getInstance()->object();
        if (!empty($config->ipsec->phase1)) {
            $idx = 0;
            $ifs = [];
            if ($config->interfaces->count() > 0) {
                foreach ($config->interfaces->children() as $key => $node) {
                    $ifs[$key] = !empty((string)$node->descr) ? (string)$node->descr : strtoupper($key);
                }
                if ($config->virtualip->count() > 0) {
                    foreach ($config->virtualip->children() as $node) {
                        if (!empty((string)$node->vhid)) {
                            $key = (string)$node->interface . '_vip' . (string)$node->vhid;
                        } else {
                            $key = (string)$node->subnet;
                        }
                        $ifs[$key] = "$node->subnet ({$node->descr})";
                    }
                }
            }
            foreach ($config->ipsec->phase1 as $p1) {
                $interface = (string)$p1->interface;
                $ph1proposal = $ph1algos[(string)$p1->{"encryption-algorithm"}->name];
                if (!empty((string)$p1->{"encryption-algorithm"}->keylen)) {
                    $ph1proposal .= sprintf(" ({$p1->{"encryption-algorithm"}->keylen} %s)", gettext("bits"));
                }
                $ph1proposal .= " + " . strtoupper((string)$p1->{"hash-algorithm"});
                if (!empty($p1->dhgroup)) {
                    $ph1proposal .= " + " . gettext("DH Group") . " {$p1->dhgroup}";
                }
                $item = [
                    "id" => intval((string)$p1->ikeid), // ikeid should be unique
                    "seqid" => $idx,
                    "enabled" => empty((string)$p1->disabled) ? "1" : "0",
                    "protocol" => $p1->protocol == "inet46" ? "IPv4+6" : ($p1->protocol == "inet6" ? "IPv6" : "IPv4"),
                    "iketype" => $ph1type[(string)$p1->iketype],
                    "interface" => !empty($ifs[$interface]) ? $ifs[$interface] : $interface,
                    "remote_gateway" => (string)$p1->{"remote-gateway"},
                    "mobile" => !empty((string)$p1->mobile),
                    "mode" => (string)$p1->mode,
                    "proposal" => $ph1proposal,
                    "authentication" => $ph1authmethods[(string)$p1->authentication_method],
                    "description" => (string)$p1->descr
                ];
                $item['type'] = "{$item['protocol']} {$item['iketype']}";
                $items[] = $item;
                $idx++;
            }
        }
        return $this->searchRecordsetBase($items);
    }

    /***
     * search phase 2 entries in legacy config returning a standard structure as we use in the mvc variant
     */
    public function searchPhase2Action()
    {
        $selected_ikeid = intval($this->request->getPost('ikeid', 'int', -1));
        $ph2algos = [
            'aes' => 'AES',
            'aes128' => 'AES128',
            'aes192' => 'AES192',
            'aes256' => 'AES256',
            'aes128gcm16' => 'aes128gcm16',
            'aes192gcm16' => 'aes192gcm16',
            'aes256gcm16' => 'aes256gcm16',
            'null' => gettext("NULL (no encryption)")
        ];
        $ph2halgos = [
            'hmac_sha1' => 'SHA1',
            'hmac_sha256' => 'SHA256',
            'hmac_sha384' => 'SHA384',
            'hmac_sha512' => 'SHA512',
            'aesxcbc' => 'AES-XCBC'
        ];
        $items = [];
        $config = Config::getInstance()->object();
        if (!empty($config->ipsec->phase2)) {
            $p2idx = 0;
            $ifs = [];
            if ($config->interfaces->count() > 0) {
                foreach ($config->interfaces->children() as $key => $node) {
                    $ifs[$key] = !empty((string)$node->descr) ? (string)$node->descr : strtoupper($key);
                }
            }
            foreach ($config->ipsec->phase2 as $p2) {
                $ikeid = intval((string)$p2->ikeid);
                if ($ikeid != $selected_ikeid && $selected_ikeid != -1) {
                    $p2idx++;
                    continue;
                }
                $p2mode = array_search(
                    (string)$p2->mode,
                    [
                      "IPv4 tunnel" => "tunnel",
                      "IPv6 tunnel" => "tunnel6",
                      "transport" => "transport",
                      "Route-based" => "route-based"
                    ]
                );
                if (in_array((string)$p2->mode, ['tunnel', 'tunnel6'])) {
                    foreach (['localid', 'remoteid'] as $id) {
                        $content = (string)$p2->$id->type;
                        switch ((string)$p2->$id->type) {
                            case "address":
                                $content = "{$p2->$id->address}";
                                break;
                            case "network":
                                $content = "{$p2->$id->address}/{$p2->$id->netbits}";
                                break;
                            case "mobile":
                                $content = gettext("Mobile Client");
                                break;
                            case "none":
                                $content = gettext("None");
                                break;
                            default:
                                if (!empty($ifs[(string)$p2->$id->type])) {
                                    $content = $ifs[(string)$p2->$id->type];
                                }
                                break;
                        }
                        if ($id == 'localid') {
                            $local_subnet = $content;
                        } else {
                            $remote_subnet = $content;
                        }
                    }
                } elseif ((string)$p2->mode == "route-based") {
                    $local_subnet = (string)$p2->tunnel_local;
                    $remote_subnet = (string)$p2->tunnel_remote;
                } else {
                    $local_subnet = "";
                    $remote_subnet = "";
                }
                $ph2proposal = "";
                foreach ($p2->{"encryption-algorithm-option"} as $node) {
                    $this_proposal = isset($ph2algos[(string)$node->name]) ? $ph2algos[(string)$node->name] : "";
                    if (!empty($ph2proposal)) {
                        $ph2proposal .= " , ";
                    }
                    if (!empty($this_proposal)) {
                        $ph2proposal .= $this_proposal;
                        if ((string)$node->keylen == 'auto') {
                            $ph2proposal .= " (auto) ";
                        } elseif (!empty((string)$node->keylen)) {
                            $ph2proposal .= sprintf(" ({$node->keylen} %s) ", gettext("bits"));
                        }
                    } else {
                        // Unsupported algorithm, will be ignored
                        $ph2proposal .= "!!" . (string)$node->name;
                    }
                }
                $ph2proposal .= " + ";
                $idx = 0;
                foreach ($p2->{"hash-algorithm-option"} as $node) {
                    if (isset($ph2halgos[(string)$node])) {
                        $ph2proposal .= ($idx++) > 0 ? " , " : "";
                        $ph2proposal .= $ph2halgos[(string)$node];
                    }
                }
                if (!empty((string)$p2->pfsgroup)) {
                    $ph2proposal .= sprintf("+ %s %s", gettext("DH Group"), "{$p2->pfsgroup}");
                }
                $item = [
                    "id" => $p2idx,
                    "uniqid" => (string)$p2->uniqid, // XXX: a bit convoluted, should probably replace id at some point
                    "ikeid" => $ikeid,
                    "reqid" => (string)$p2->reqid,
                    "enabled" => empty((string)$p2->disabled) ? "1" : "0",
                    "protocol" => $p2->protocol == "esp" ? "ESP" : "AH",
                    "mode" => $p2mode,
                    "local_subnet" => $local_subnet,
                    "remote_subnet" => $remote_subnet,
                    "proposal" => $ph2proposal,
                    "description" => (string)$p2->descr
                ];
                $items[] = $item;
                $p2idx++;
            }
        }
        return $this->searchRecordsetBase($items);
    }

    /**
     * delete phase 1 including associated phase 2 entries
     */
    public function delPhase1Action($ikeid)
    {
        if ($this->request->isPost()) {
            $phase_ids = [];
            Config::getInstance()->lock();
            $config = Config::getInstance()->object();
            foreach ([$config->ipsec->phase1, $config->ipsec->phase2] as $phid => $phase) {
                $phase_ids[$phid] = [];
                if (!empty($phase)) {
                    $idx = 0;
                    foreach ($phase as $p) {
                        if (intval((string)$p->ikeid) == intval($ikeid)) {
                            $phase_ids[$phid][] = $idx;
                        }
                        $idx++;
                    }
                    foreach (array_reverse($phase_ids[$phid]) as $idx) {
                        unset($phase[$idx]);
                    }
                }
            }
            Config::getInstance()->save();
            if (!empty($phase_ids[0])) {
                @touch("/tmp/ipsec.dirty");
            }
            return [
              'status' => 'ok',
              'phase1count' => count($phase_ids[0]), // should be 1 as ikeid is unique
              'phase2count' => count($phase_ids[1]),
            ];
        }
        return ['status' => 'failed'];
    }

    /**
     * toggle if phase 1 is enabled
     */
    public function togglePhase1Action($ikeid, $enabled = null)
    {
        if ($this->request->isPost()) {
            Config::getInstance()->lock();
            $config = Config::getInstance()->object();
            if (!empty($config->ipsec->phase1)) {
                $idx = 0;
                foreach ($config->ipsec->phase1 as $p1) {
                    if (intval((string)$p1->ikeid) == intval($ikeid)) {
                        if ($enabled == "0" || $enabled == "1") {
                            $new_status = $enabled == "1" ? "0" : "1";
                        } else {
                            $new_status = $config->ipsec->phase1[$idx]->disabled == "1" ? "0" : "1";
                        }
                        if ($new_status == "1") {
                            $config->ipsec->phase1[$idx]->disabled = $new_status;
                        } elseif (isset($config->ipsec->phase1[$idx]->disabled)) {
                            unset($config->ipsec->phase1[$idx]->disabled);
                        }

                        Config::getInstance()->save();
                        @touch("/tmp/ipsec.dirty");
                        return ['status' => 'ok', 'disabled' => $new_status];
                    }
                    $idx++;
                }
            }
            return ['status' => 'not_found'];
        }
        return ['status' => 'failed'];
    }

    /**
     * delete phase 2 entry
     */
    public function delPhase2Action($seqid)
    {
        if ($this->request->isPost()) {
            Config::getInstance()->lock();
            $config = Config::getInstance()->object();
            if ((string)intval($seqid) == $seqid && isset($config->ipsec->phase2[intval($seqid)])) {
                unset($config->ipsec->phase2[intval($seqid)]);
                Config::getInstance()->save();
                @touch("/tmp/ipsec.dirty");
                return ['status' => 'ok'];
            }
            return ['status' => 'not_found'];
        }
        return ['status' => 'failed'];
    }

    /**
     * toggle if phase 2 is enabled
     */
    public function togglePhase2Action($seqid, $enabled = null)
    {
        if ($this->request->isPost()) {
            Config::getInstance()->lock();
            $config = Config::getInstance()->object();
            if ((string)intval($seqid) == $seqid && isset($config->ipsec->phase2[intval($seqid)])) {
                if ($enabled == "0" || $enabled == "1") {
                    $new_status = $enabled == "1" ? "0" : "1";
                } else {
                    $new_status = $config->ipsec->phase2[intval($seqid)]->disabled == "1" ? "0" : "1";
                }
                if ($new_status == "1") {
                    $config->ipsec->phase2[intval($seqid)]->disabled = $new_status;
                } elseif (isset($config->ipsec->phase2[intval($seqid)]->disabled)) {
                    unset($config->ipsec->phase2[intval($seqid)]->disabled);
                }

                Config::getInstance()->save();
                @touch("/tmp/ipsec.dirty");
                return ['status' => 'ok', 'disabled' => $new_status];
            }
            return ['status' => 'not_found'];
        }
        return ['status' => 'failed'];
    }

    /**
     * toggle if IPsec is enabled
     */
    public function toggleAction($enabled = null)
    {
        if ($this->request->isPost()) {
            Config::getInstance()->lock();
            $config = Config::getInstance()->object();
            if ($enabled == "0" || $enabled == "1") {
                $new_status = $enabled == "1";
            } else {
                $new_status = !isset($config->ipsec->enable);
            }
            if ($new_status) {
                $config->ipsec->enable = true;
            } elseif (isset($config->ipsec->enable)) {
                unset($config->ipsec->enable);
            }
            Config::getInstance()->save();
            @touch("/tmp/ipsec.dirty");
            return ['status' => 'ok'];
        }
        return ['status' => 'failed'];
    }
}
