VPN: IPsec: Status Overview - refactor to MVC master/detail form.

This commit is contained in:
Ad Schellevis 2022-07-11 21:04:04 +02:00
parent a1b63db4b0
commit 09deaa81cd
6 changed files with 355 additions and 250 deletions

View File

@ -0,0 +1,155 @@
<?php
/*
* Copyright (C) 2022 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\Backend;
use OPNsense\Core\Config;
/**
* Class SessionsController
* @package OPNsense\IPsec\Api
*/
class SessionsController extends ApiControllerBase
{
private function list_status()
{
return json_decode((new Backend())->configdRun('ipsec list status'), true);
}
/**
* Search phase 1 session entries
* @return array
*/
public function searchPhase1Action()
{
$this->sessionClose();
$records = [];
$config = Config::getInstance()->object();
$data = $this->list_status();
$phase1s = [];
if (!empty($config->ipsec->phase1)) {
foreach ($config->ipsec->phase1 as $p1) {
if (!empty((string)$p1->ikeid)) {
$phase1s[(string)$p1->ikeid] = $p1;
}
}
}
if (!empty($data)) {
foreach ($data as $conn => $payload) {
$record = $payload;
$record['ikeid'] = substr(explode('-', $conn)[0],3);
$record['phase1desc'] = null;
$record['name'] = $conn;
if (!empty($phase1s[$record['ikeid']])) {
$record['phase1desc'] = (string)$phase1s[$record['ikeid']]->descr;
}
$record['connected'] = !empty($record['sas']);
unset($record['children']);
unset($record['sas']);
$records[] = $record;
}
}
return $this->searchRecordsetBase($records);
}
/**
* Search phase 2 session entries
* @return array
*/
public function searchPhase2Action()
{
$this->sessionClose();
$records = [];
$selected_conn = $this->request->getPost('id', 'string', '');
$config = Config::getInstance()->object();
$data = $this->list_status();
$reqids = [];
if (!empty($config->ipsec->phase2)) {
foreach ($config->ipsec->phase2 as $p2) {
if (!empty((string)$p2->reqid)) {
$reqids[(string)$p2->reqid] = [
"ikeid" => (string)$p2->ikeid,
"phase2desc" => (string)$p2->descr
];
}
}
}
if (!empty($data[$selected_conn]) && !empty($data[$selected_conn]['sas'])) {
foreach ($data[$selected_conn]['sas'] as $sa) {
if (!empty($sa['child-sas'])) {
foreach ($sa['child-sas'] as $conn => $csa) {
$record = $csa;
$record['remote-host'] = $sa['remote-host'];
if (!empty($reqids[$csa['reqid']])) {
$record = array_merge($record, $reqids[$csa['reqid']]);
}
foreach ($record as $key => $val) {
if (is_array($val)) {
$record[$key] = implode(' , ', $val);
}
}
$records[] = $record;
}
}
}
}
return $this->searchRecordsetBase($records);
}
/**
* connect a session
* @param string $id md 5 hash to identify the spd entry
* @return array
*/
public function connectAction($id)
{
if ($this->request->isPost()) {
$this->sessionClose();
(new Backend())-> configdpRun('ipsec connect', [$id]);
return ["result" => "ok"];
}
return ["result" => "failed"];
}
/**
* disconnect a session
* @param string $id md 5 hash to identify the spd entry
* @return array
*/
public function disconnectAction($id)
{
if ($this->request->isPost()) {
$this->sessionClose();
(new Backend())-> configdpRun('ipsec disconnect', [$id]);
return ["result" => "ok"];
}
return ["result" => "failed"];
}
}

View File

@ -0,0 +1,41 @@
<?php
/*
* Copyright (C) 2022 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;
/**
* Class SessionsController
* @package OPNsense\IPsec
*/
class SessionsController extends \OPNsense\Base\IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/IPsec/sessions');
}
}

View File

@ -52,7 +52,8 @@
<page-status-ipsec>
<name>Status: IPsec</name>
<patterns>
<pattern>diag_ipsec.php*</pattern>
<pattern>ui/ipsec/sessions</pattern>
<pattern>api/ipsec/sessions</pattern>
</patterns>
</page-status-ipsec>
<page-status-ipsec-leases>

View File

@ -13,9 +13,7 @@
</Keys>
<KeyPairs order="40" VisibleName="RSA Key Pairs" url="/ui/ipsec/key-pairs" />
<Settings order="50" VisibleName="Advanced Settings" url="/vpn_ipsec_settings.php"/>
<Status order="60" VisibleName="Status Overview" url="/diag_ipsec.php">
<Act url="/diag_ipsec.php?*" visibility="hidden"/>
</Status>
<Status order="60" VisibleName="Status Overview" url="/ui/ipsec/sessions"/>
<Leases order="70" VisibleName="Lease Status" url="/diag_ipsec_leases.php"/>
<SAD order="80" VisibleName="Security Association Database" url="/ui/ipsec/sad"/>
<SPD order="90" VisibleName="Security Policy Database" url="/ui/ipsec/spd"/>

View File

@ -0,0 +1,156 @@
{#
# Copyright (c) 2022 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.
#}
<script>
'use strict';
$( document ).ready(function() {
let grid_phase1 = $("#grid-phase1").UIBootgrid({
search:'/api/ipsec/sessions/search_phase1',
options:{
multiSelect: false,
rowSelect: true,
selection: true,
formatters:{
commands: function (column, row) {
if (row['connected']) {
return '<button type="button" class="btn btn-xs btn-default command-disconnect" data-row-id="' + row.name + '"><span class="fa fa-remove fa-fw"></span></button>';
} else {
return '<button type="button" class="btn btn-xs btn-default command-connect" data-row-id="' + row.name + '"><span class="fa fa-play fa-fw text-success"></span></button>';
}
}
}
}
});
grid_phase1.on('loaded.rs.jquery.bootgrid', function() {
$('[data-toggle="tooltip"]').tooltip();
let ids = $("#grid-phase1").bootgrid("getCurrentRows");
if (ids.length > 0) {
$("#grid-phase1").bootgrid('select', [ids[0].name]);
}
$('.command-disconnect').click(function(){
ajaxCall("/api/ipsec/sessions/disconnect/" + $(this).data('row-id'), {}, function(){
$('#grid-phase1').bootgrid('reload');
});
});
$('.command-connect').click(function(){
ajaxCall("/api/ipsec/sessions/connect/" + $(this).data('row-id'), {}, function(){
$('#grid-phase1').bootgrid('reload');
});
});
});
grid_phase1.on("selected.rs.jquery.bootgrid", function(e, rows) {
$("#grid-phase2").bootgrid('reload');
});
grid_phase1.on("deselected.rs.jquery.bootgrid", function(e, rows) {
$("#grid-phase2").bootgrid('reload');
});
let grid_phase2 = $("#grid-phase2").UIBootgrid({
search:'/api/ipsec/sessions/search_phase2',
options:{
formatters:{
addresses: function (column, row) {
if (typeof row[column.id] === 'string') {
return row[column.id].replaceAll(/,/g, "<br/>");
}
}
},
useRequestHandlerOnGet: true,
requestHandler: function(request) {
let ids = $("#grid-phase1").bootgrid("getSelectedRows");
request['id'] = ids.length > 0 ? ids[0] : "__not_found__";
return request;
}
}
});
grid_phase2.on('loaded.rs.jquery.bootgrid', function() {
if (grid_phase2.bootgrid("getTotalRowCount") > 0) {
$("#box-phase2").show();
} else {
$("#box-phase2").hide();
}
});
$("div.actionBar").each(function(){
let heading_text = "";
if ($(this).closest(".bootgrid-header").attr("id").includes("phase1")) {
heading_text = "{{ lang._('Phase 1') }}";
} else {
heading_text = "{{ lang._('Phase 2') }}";
}
$(this).parent().prepend($('<td class="col-sm-2 theading-text">'+heading_text+'</div>'));
$(this).removeClass("col-sm-12");
$(this).addClass("col-sm-10");
});
updateServiceControlUI('ipsec');
});
</script>
<div class="tab-content content-box __mb">
<table id="grid-phase1" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="name" data-type="string" data-sortable="false" data-identifier="true" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="phase1desc" data-type="string">{{ lang._('Connection') }}</th>
<th data-column-id="version" data-type="string">{{ lang._('Version') }}</th>
<th data-column-id="local-id" data-type="string">{{ lang._('Local ID') }}</th>
<th data-column-id="local-addrs" data-type="string">{{ lang._('Local IP') }}</th>
<th data-column-id="remote-id" data-type="string">{{ lang._('Remote ID') }}</th>
<th data-column-id="remote-addrs" data-type="string">{{ lang._('Remote IP') }}</th>
<th data-column-id="local-class" data-type="string">{{ lang._('Local Auth') }}</th>
<th data-column-id="remote-class" data-type="string">{{ lang._('Remote Auth') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="tab-content content-box __mb" id="box-phase2">
<table id="grid-phase2" class="table table-condensed table-hover table-striped table-responsive">
<thead>
<tr>
<th data-column-id="name" data-type="string" data-sortable="false" data-visible="false">{{ lang._('ID') }}</th>
<th data-column-id="local-ts" data-type="string" data-formatter="addresses">{{ lang._('Local subnets ') }}</th>
<th data-column-id="spi-in" data-type="string">{{ lang._('spi-in') }}</th>
<th data-column-id="spi-out" data-type="string">{{ lang._('spi-out') }}</th>
<th data-column-id="remote-ts" data-type="string" data-formatter="addresses">{{ lang._('Remote subnets') }}</th>
<th data-column-id="state" data-type="string">{{ lang._('State') }}</th>
<th data-column-id="install-time" data-type="string">{{ lang._('Time') }}</th>
<th data-column-id="bytes-in" data-type="string">{{ lang._('Bytes in') }}</th>
<th data-column-id="bytes-out" data-type="string">{{ lang._('Bytes out') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>

View File

@ -1,246 +0,0 @@
<?php
/*
* Copyright (C) 2014-2016 Deciso B.V.
* Copyright (C) 2004-2009 Scott Ullrich <sullrich@gmail.com>
* Copyright (C) 2008 Shrew Soft Inc. <mgrooms@shrew.net>
* Copyright (C) 2003-2004 Manuel Kasper <mk@neon1.net>
* 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.
*/
require_once("guiconfig.inc");
require_once("plugins.inc.d/ipsec.inc");
/**
* search config for phase 1 description
* @param string $conn connection string in format conXXX-XX
* @return string description (or blank if none found)
*/
function ipsec_conn_description($conn)
{
global $config;
$ipsec_conn_seq = substr(explode('-', $conn)[0],3);
if (!empty($config['ipsec']['phase1'])) {
foreach ($config['ipsec']['phase1'] as $phase1) {
if ($phase1['ikeid'] == $ipsec_conn_seq && !empty($phase1['descr'])) {
return $phase1['descr'];
}
}
}
return '';
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (isset($_POST['action'])) {
$act = $_POST['action'];
} else {
$act = null;
}
switch ($act) {
case 'connect':
if (!empty($_POST['connid'])) {
configdp_run('ipsec connect', array($_POST['connid']));
}
break;
case 'disconnect':
if (!empty($_POST['connid'])) {
configdp_run('ipsec disconnect', array($_POST['connid']));
}
break;
default:
break;
}
header(url_safe('Location: /diag_ipsec.php'));
exit;
}
$ipsec_status = json_decode(configd_run('ipsec list status'), true);
if ($ipsec_status == null) {
$ipsec_status = array();
}
$service_hook = 'strongswan';
include("head.inc");
?>
<body>
<script>
$( document ).ready(function() {
// show / hide connection details
$(".ipsec_info").click(function(event){
$("#" + $(this).data('target')).toggleClass('hidden visible');
event.preventDefault();
});
// show/hide all info
$("#collapse_all").click(function(){
// use a data attribute to store visibility for all detail items (we can't toggle here, because some items
// might already be expanded)
if ($("#collapse_all").data('status') != 'visible') {
$(".ipsec_info").each(function(){
$("#" + $(this).data('target')).removeClass('hidden');
$("#" + $(this).data('target')).addClass('visible');
});
$("#collapse_all").data('status', 'visible');
} else {
$(".ipsec_info").each(function(){
$("#" + $(this).data('target')).removeClass('visible');
$("#" + $(this).data('target')).addClass('hidden');
});
$("#collapse_all").data('status', 'hidden');
}
});
});
</script>
<?php include("fbegin.inc"); ?>
<section class="page-content-main">
<div class="container-fluid">
<div class="row">
<?php if (isset($input_errors) && count($input_errors) > 0) print_input_errors($input_errors); ?>
<section class="col-xs-12">
<div class="tab-content content-box">
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th><?= gettext("Connection");?></th>
<th class="hidden-xs hidden-sm"><?= gettext("Version");?></th>
<th class="hidden-xs"><?= gettext("Local ID");?></th>
<th class="hidden-xs"><?= gettext("Local IP");?></th>
<th class="hidden-xs"><?= gettext("Remote ID");?></th>
<th><?= gettext("Remote IP");?></th>
<th class="hidden-xs hidden-sm"><?= gettext("Local Auth");?></th>
<th class="hidden-xs hidden-sm"><?= gettext("Remote Auth");?></th>
<th><?= gettext("Status");?>
<div class="pull-right">
<i class="fa fa-expand" id="collapse_all" style="cursor: pointer;" data-toggle="tooltip" title="<?=gettext("collapse/expand all");?>"></i> &nbsp;&nbsp;
</div>
</th>
</tr>
</thead>
<tbody>
<?php foreach ($ipsec_status as $ipsec_conn_key => $ipsec_conn):?>
<tr>
<td><?=ipsec_conn_description($ipsec_conn_key);?> <small>(<?= $ipsec_conn_key;?>)</small></td>
<td class="hidden-xs hidden-sm"><?= $ipsec_conn['version'] ?></td>
<td class="hidden-xs"><?= $ipsec_conn['local-id'] ?></td>
<td class="hidden-xs"><?= $ipsec_conn['local-addrs'] ?></td>
<td class="hidden-xs"><?= $ipsec_conn['remote-id'] ?></td>
<td><?= $ipsec_conn['remote-addrs'] ?></td>
<td class="hidden-xs hidden-sm"><?= $ipsec_conn['local-class'] ?></td>
<td class="hidden-xs hidden-sm"><?= $ipsec_conn['remote-class'] ?></td>
<td>
<?php if (count($ipsec_conn['sas'])):
?>
<form method="post">
<input type="hidden" value="<?=$ipsec_conn_key;?>" name="connid"/>
<button type="submit" class="btn btn-xs" name="action" value="disconnect">
<i class="fa fa-remove fa-fw"></i>
</button>
<button type="submit" class="btn btn-xs" name="action" value="connect">
<i class="fa fa-play fa-fw text-success"></i>
</button>
<button type="none" class="btn btn-xs ipsec_info" data-target="info_<?=$ipsec_conn_key?>">
<i class="fa fa-info-circle fa-fw"></i>
</button>
</form>
<?php else:
?>
<form method="post">
<input type="hidden" value="<?=$ipsec_conn_key;?>" name="connid"/>
<button type="submit" class="btn btn-xs" name="action" value="connect">
<i class="fa fa-play fa-fw text-warning"></i>
</button>
</form>
<?php endif;
?>
</td>
</tr>
<?php if (count($ipsec_conn['sas'])):
?>
<tr>
<td colspan="9" class="hidden" id="info_<?=$ipsec_conn_key?>">
<table class="table table-condensed">
<thead>
<tr>
<th><?= gettext("Remote Host");?></th>
<th><?= gettext("Local subnets");?></th>
<th class="hidden-xs hidden-sm"><?= gettext("SPI(s)");?></th>
<th><?= gettext("Remote subnets");?></th>
<th class="hidden-xs hidden-sm"><?= gettext("State");?></th>
<th><?= gettext("Stats");?></th>
</tr>
</thead>
<tbody>
<?php foreach ($ipsec_conn['sas'] as $sa_key => $sa):?>
<?php foreach ($sa['child-sas'] as $child_sa_key => $child_sa):?>
<tr>
<td>
<?= $sa['remote-host'] ?>
</td>
<td>
<?= implode('<br/>', $child_sa['local-ts'])?>
</td>
<td class="hidden-xs hidden-sm">
<?=gettext('in')?> : <?= $child_sa['spi-in'] ?><br/>
<?=gettext('out')?> : <?= $child_sa['spi-out'] ?>
</td>
<td>
<?= implode('<br/>', $child_sa['remote-ts'])?>
</td>
<td class="hidden-xs hidden-sm">
<?= $child_sa['state']?>
<?= $ipsec_conn['routed'] ? '<br/>'.gettext("Routed") : "";?>
</td>
<td>
<small>
<?= gettext('Time');?> : <?= $child_sa['install-time']?><br/>
<?= gettext('Bytes in');?> : <?= $child_sa['bytes-in']?><br/>
<?= gettext('Bytes out');?> : <?= $child_sa['bytes-out']?>
</small>
</td>
</tr>
<?php endforeach; ?>
<?php endforeach; ?>
</tbody>
</table>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</section>
</div>
</div>
</section>
<?php include("foot.inc"); ?>