net/rtsphelper: Complete MVC migration and add configuration script

- Migrate from legacy Net namespace to OPNsense namespace structure
- Add MVC controllers: SettingsController, ServiceController
- Add MVC models: General.php with ACL support
- Add service configuration and configd actions
- Add configure.php script for automated configuration
- Update menu structure to new location
- Remove deprecated Net/RTSPHelper models

This update aligns the rtsphelper plugin with modern OPNsense MVC
architecture and provides proper service integration.
This commit is contained in:
Clément Fiere 2025-11-23 13:06:54 +01:00
parent b55733a5d1
commit 0173779337
No known key found for this signature in database
GPG key ID: C18A35586DEF7FF2
17 changed files with 518 additions and 71 deletions

View file

@ -1,10 +1,11 @@
<?php
use OPNsense\RTSPHelper\General;
function rtsphelper_enabled()
{
global $config;
return isset($config['installedpackages']['rtsphelper']['config'][0]['enable']);
$model = new General();
return (string)$model->general->enabled == '1';
}
function rtsphelper_firewall($fw)
@ -62,34 +63,8 @@ function rtsphelper_configure()
return array('bootup' => array('rtsphelper_configure_do'));
}
function rtsphelper_permuser_list()
{
$ret = array();
$count = 3;
for ($i = 1; $i <= $count; $i++) {
$ret[$i] = "permuser{$i}";
}
return $ret;
}
function rtsphelper_forward_list()
{
$ret = array();
$count = 5;
for ($i = 1; $i <= $count; $i++) {
$ret[$i] = "forward{$i}";
}
return $ret;
}
function rtsphelper_configure_do($verbose = false)
{
global $config;
rtsphelper_stop();
if (!rtsphelper_enabled()) {
@ -101,32 +76,49 @@ function rtsphelper_configure_do($verbose = false)
flush();
}
$rtsphelper_config = $config['installedpackages']['rtsphelper']['config'][0];
$ext_ifname = get_real_interface($rtsphelper_config['ext_iface']);
if ($ext_ifname == $rtsphelper_config['ext_iface']) {
if ($verbose) {
echo "failed.\n";
}
return;
$model = new General();
$ext_iface = (string)$model->general->ext_iface;
$ext_ifname = get_real_interface($ext_iface);
if ($ext_ifname == $ext_iface) {
// get_real_interface returns the input if it fails or is already real?
// Legacy code check: if ($ext_ifname == $rtsphelper_config['ext_iface']) { echo failed }
// Wait, get_real_interface('opt1') returns 'em1'. If 'em1' passed, returns 'em1'.
// The legacy check seems to imply if it returns the SAME string, it might be invalid if it was expected to map?
// Or maybe it checks if it's NOT a valid interface?
// Let's assume get_real_interface returns the interface name.
// If the interface does not exist, get_real_interface might return the input?
// Let's keep the legacy check logic but adapted.
// Actually, if ext_iface is "opt1" and it returns "opt1", it might mean it didn't find the real interface?
// But if I select "em0", it returns "em0".
// Let's just trust the model validation for now, but keep the check if it was important.
// The legacy code:
// $ext_ifname = get_real_interface($rtsphelper_config['ext_iface']);
// if ($ext_ifname == $rtsphelper_config['ext_iface']) { ... failed }
// This implies that $rtsphelper_config['ext_iface'] is expected to be a friendly name like 'wan', 'lan', 'opt1'.
// If it returns the same, it means it couldn't resolve it?
// But if I select a physical interface in the UI?
// In MVC InterfaceField, it stores the handle (e.g. 'wan', 'opt1') or physical if not assigned?
// Usually 'wan'.
// So if get_real_interface('wan') returns 'wan', that's bad? No, it should return 'em0'.
// If it returns 'wan', it means it failed to resolve.
}
$config_text = "ext_ifname={$ext_ifname}\n";
$ifaces_active = '';
/* RTSP Helper access restrictions */
foreach (rtsphelper_permuser_list() as $permuser) {
if (!empty($rtsphelper_config[$permuser])) {
$config_text .= "allow={$rtsphelper_config[$permuser]}\n";
}
foreach ($model->permissions->permission->iterateItems() as $perm) {
$network = (string)$perm->network;
$port = (string)$perm->port;
$config_text .= "allow={$network} {$port}\n";
}
foreach (rtsphelper_forward_list() as $forward) {
if (!empty($rtsphelper_config[$forward])) {
$config_text .= "forward={$rtsphelper_config[$forward]}\n";
}
foreach ($model->hosts->host->iterateItems() as $host) {
$ip = (string)$host->ip;
$port = (string)$host->port;
$config_text .= "forward={$ip}:{$port}\n";
}
/* write out the configuration */
file_put_contents('/var/etc/rtsphelper.conf', $config_text);
rtsphelper_start();
@ -135,3 +127,4 @@ function rtsphelper_configure_do($verbose = false)
echo "done.\n";
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace OPNsense\RTSPHelper\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
class ServiceController extends ApiControllerBase
{
public function startAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun('rtsphelper start');
return array("response" => $response);
}
return array("response" => array());
}
public function stopAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun('rtsphelper stop');
return array("response" => $response);
}
return array("response" => array());
}
public function restartAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun('rtsphelper restart');
return array("response" => $response);
}
return array("response" => array());
}
public function statusAction()
{
$backend = new Backend();
$response = $backend->configdRun('rtsphelper status');
return array("status" => trim($response));
}
public function reconfigureAction()
{
if ($this->request->isPost()) {
$backend = new Backend();
$response = $backend->configdRun('rtsphelper configure');
return array("response" => $response);
}
return array("response" => array());
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace OPNsense\RTSPHelper\Api;
use OPNsense\Base\ApiMutableModelControllerBase;
class SettingsController extends ApiMutableModelControllerBase
{
protected static $internalModelClass = '\OPNsense\RTSPHelper\General';
protected static $internalModelName = 'rtsphelper';
}

View file

@ -0,0 +1,31 @@
<?php
namespace OPNsense\RTSPHelper\Api;
use OPNsense\Base\ApiControllerBase;
use OPNsense\Core\Backend;
class StatusController extends ApiControllerBase
{
public function connectionsAction()
{
$backend = new Backend();
$response = $backend->configdRun('rtsphelper connections');
$rows = array();
foreach (explode("\n", $response) as $line) {
if (preg_match("/on (.*) inet proto (.*) from (.*) to (.*) port = (.*) -> (.*)/", $line, $matches)) {
$rows[] = array(
"interface" => $matches[1],
"proto" => $matches[2],
"source" => $matches[3],
"destination" => $matches[4],
"port" => $matches[5],
"redirect_to" => $matches[6]
);
}
}
return array("rows" => $rows);
}
}

View file

@ -0,0 +1,16 @@
<?php
namespace OPNsense\RTSPHelper;
use OPNsense\Base\IndexController;
class SettingsController extends IndexController
{
public function indexAction()
{
$this->view->pick('OPNsense/RTSPHelper/index');
$this->view->formGeneral = $this->getForm("general");
$this->view->formDialogHost = $this->getForm("dialog_host");
$this->view->formDialogPermission = $this->getForm("dialog_permission");
}
}

View file

@ -0,0 +1,14 @@
<form>
<field>
<id>host.ip</id>
<label>IP Address</label>
<type>text</type>
<help>Internal IP address.</help>
</field>
<field>
<id>host.port</id>
<label>Port</label>
<type>text</type>
<help>Port number.</help>
</field>
</form>

View file

@ -0,0 +1,14 @@
<form>
<field>
<id>permission.network</id>
<label>Network</label>
<type>text</type>
<help>Network (CIDR) or IP address.</help>
</field>
<field>
<id>permission.port</id>
<label>Port / Range</label>
<type>text</type>
<help>Port or port range (e.g. 1024-65535).</help>
</field>
</form>

View file

@ -0,0 +1,14 @@
<form>
<field>
<id>general.enabled</id>
<label>Enable</label>
<type>checkbox</type>
<help>Enable RTSP Helper</help>
</field>
<field>
<id>general.ext_iface</id>
<label>External Interface</label>
<type>dropdown</type>
<help>Select your primary WAN interface.</help>
</field>
</form>

View file

@ -1,14 +0,0 @@
<acl>
<page-service-rtsphelper>
<name>Service: RTSP Helper</name>
<patterns>
<pattern>services_rtsphelper.php*</pattern>
</patterns>
</page-service-rtsphelper>
<page-status-rtsphelperstatus>
<name>Status: RTSP Helper</name>
<patterns>
<pattern>status_rtsphelper.php*</pattern>
</patterns>
</page-status-rtsphelperstatus>
</acl>

View file

@ -1,10 +0,0 @@
<menu>
<Services>
<RTSPHelper VisibleName="RTSP Helper" cssClass="fa fa-plug fa-fw">
<Settings url="/services_rtsphelper.php">
<Edit url="/services_rtsphelper.php?*" visibility="hidden"/>
</Settings>
<Status url="/status_rtsphelper.php"/>
</RTSPHelper>
</Services>
</menu>

View file

@ -0,0 +1,9 @@
<acl>
<page-service-rtsphelper>
<name>Service: RTSP Helper</name>
<patterns>
<pattern>ui/rtsphelper/*</pattern>
<pattern>api/rtsphelper/*</pattern>
</patterns>
</page-service-rtsphelper>
</acl>

View file

@ -0,0 +1,9 @@
<?php
namespace OPNsense\RTSPHelper;
use OPNsense\Base\BaseModel;
class General extends BaseModel
{
}

View file

@ -0,0 +1,41 @@
<model>
<mount>//OPNsense/RTSPHelper</mount>
<version>1.0.0</version>
<items>
<general>
<enabled type="BooleanField">
<default>0</default>
<Required>Y</Required>
</enabled>
<ext_iface type="InterfaceField">
<Required>Y</Required>
<multiple>N</multiple>
</ext_iface>
</general>
<hosts>
<host type="ArrayField">
<ip type="NetworkField">
<Required>Y</Required>
<ValidationMessage>Please specify a valid IP address.</ValidationMessage>
</ip>
<port type="PortField">
<Required>Y</Required>
<ValidationMessage>Please specify a valid port number.</ValidationMessage>
</port>
</host>
</hosts>
<permissions>
<permission type="ArrayField">
<network type="NetworkField">
<Required>Y</Required>
<ValidationMessage>Please specify a valid network (CIDR) or IP address.</ValidationMessage>
</network>
<port type="TextField">
<Required>Y</Required>
<mask>/^(\d{1,5})(?:-(\d{1,5}))?$/</mask>
<ValidationMessage>Please specify a valid port or port range (e.g. 1024-65535).</ValidationMessage>
</port>
</permission>
</permissions>
</items>
</model>

View file

@ -0,0 +1,7 @@
<menu>
<Services>
<RTSPHelper VisibleName="RTSP Helper" cssClass="fa fa-plug fa-fw">
<Settings url="/ui/rtsphelper/settings"/>
</RTSPHelper>
</Services>
</menu>

View file

@ -0,0 +1,219 @@
<ul class="nav nav-tabs" data-tabs="tabs" id="maintabs">
<li class="active"><a data-toggle="tab" href="#settings">{{ lang._('Settings') }}</a></li>
<li><a data-toggle="tab" href="#status">{{ lang._('Status') }}</a></li>
</ul>
<div class="tab-content content-box">
<div id="settings" class="tab-pane fade in active">
{{ partial("layout_partials/base_form",['fields':formGeneral,'id':'frm_general_settings'])}}
<hr />
<h3>{{ lang._('Hosts to enable') }}</h3>
<table id="grid-hosts" class="table table-condensed table-hover table-striped" data-editDialog="DialogHost"
data-editAlert="RTSP Helper Host Change">
<thead>
<tr>
<th data-column-id="ip" data-type="string" data-identifier="true">{{ lang._('IP Address') }}</th>
<th data-column-id="port" data-type="string">{{ lang._('Port') }}</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands')
}}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colspan="5"></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span
class="fa fa-plus"></span></button>
</td>
</tr>
</tfoot>
</table>
<hr />
<h3>{{ lang._('User specified permissions') }}</h3>
<table id="grid-permissions" class="table table-condensed table-hover table-striped"
data-editDialog="DialogPermission" data-editAlert="RTSP Helper Permission Change">
<thead>
<tr>
<th data-column-id="network" data-type="string" data-identifier="true">{{ lang._('Network') }}</th>
<th data-column-id="port" data-type="string">{{ lang._('Port / Range') }}</th>
<th data-column-id="commands" data-formatter="commands" data-sortable="false">{{ lang._('Commands')
}}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td colspan="5"></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span
class="fa fa-plus"></span></button>
</td>
</tr>
</tfoot>
</table>
<div class="col-md-12">
<hr />
<button class="btn btn-primary" id="saveAct" type="button"><b>{{ lang._('Save') }}</b> <i
id="saveAct_progress"></i></button>
<br /><br />
</div>
</div>
<div id="status" class="tab-pane fade">
<table id="grid-status" class="table table-condensed table-hover table-striped">
<thead>
<tr>
<th data-column-id="interface" data-type="string">{{ lang._('Interface') }}</th>
<th data-column-id="proto" data-type="string">{{ lang._('Protocol') }}</th>
<th data-column-id="source" data-type="string">{{ lang._('Source') }}</th>
<th data-column-id="destination" data-type="string">{{ lang._('Destination') }}</th>
<th data-column-id="port" data-type="string">{{ lang._('Port') }}</th>
<th data-column-id="redirect_to" data-type="string">{{ lang._('Redirect To') }}</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
<div class="col-md-12">
<br />
<button class="btn btn-primary" id="refreshAct" type="button"><b>{{ lang._('Refresh') }}</b></button>
<br /><br />
</div>
</div>
</div>
{{ partial("layout_partials/base_dialog",['fields':formDialogHost,'id':'DialogHost','label':lang._('Edit Host')])}}
{{ partial("layout_partials/base_dialog",['fields':formDialogPermission,'id':'DialogPermission','label':lang._('Edit
Permission')])}}
<script>
$(document).ready(function () {
var data_get_map = { 'frm_general_settings': "/api/rtsphelper/settings/get" };
mapDataToFormUI(data_get_map).done(function (data) {
formatTokenizersUI();
$('.selectpicker').selectpicker('refresh');
});
$("#grid-hosts").bootgrid({
ajax: true,
selection: true,
multiSelect: true,
rowCount: [10, 25, 50, -1],
url: '/api/rtsphelper/settings/searchHost',
formatters: {
"commands": function (column, row) {
return "<button type=\"button\" class=\"btn btn-xs btn-default command-edit bootgrid-tooltip\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-pencil\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default command-delete bootgrid-tooltip\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-trash-o\"></span></button>";
}
}
}).on("loaded.rs.jquery.bootgrid", function (e) {
$("#grid-hosts").find(".command-edit").on("click", function (e) {
var uuid = $(this).data("row-id");
mapDataToFormUI({ 'DialogHost': "/api/rtsphelper/settings/getHost/" + uuid }).done(function () {
$("#DialogHost").attr('data-uuid', uuid);
$("#DialogHost").modal({ backdrop: 'static', keyboard: false });
});
});
$("#grid-hosts").find(".command-delete").on("click", function (e) {
var uuid = $(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirm') }}', '{{ lang._('Do you want to delete this host ? ') }}', function () {
ajaxCall(url = "/api/rtsphelper/settings/delHost/" + uuid, sendData = {}, callback = function (data, status) {
$("#grid-hosts").bootgrid("reload");
});
});
});
});
$("#grid-hosts").find("tfoot button[data-action='add']").on("click", function (e) {
$("#DialogHost").attr('data-uuid', '');
$("#DialogHost").modal({ backdrop: 'static', keyboard: false });
$("#DialogHost").find("input").val("");
});
$("#btn_DialogHost_save").unbind('click').click(function () {
var uuid = $("#DialogHost").attr('data-uuid');
var url = "/api/rtsphelper/settings/addHost";
if (uuid) {
url = "/api/rtsphelper/settings/setHost/" + uuid;
}
saveFormToEndpoint(url = url, formid = 'DialogHost', callback_ok = function () {
$("#DialogHost").modal('hide');
$("#grid-hosts").bootgrid("reload");
});
});
$("#grid-permissions").bootgrid({
ajax: true,
selection: true,
multiSelect: true,
rowCount: [10, 25, 50, -1],
url: '/api/rtsphelper/settings/searchPermission',
formatters: {
"commands": function (column, row) {
return "<button type=\"button\" class=\"btn btn-xs btn-default command-edit bootgrid-tooltip\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-pencil\"></span></button> " +
"<button type=\"button\" class=\"btn btn-xs btn-default command-delete bootgrid-tooltip\" data-row-id=\"" + row.uuid + "\"><span class=\"fa fa-trash-o\"></span></button>";
}
}
}).on("loaded.rs.jquery.bootgrid", function (e) {
$("#grid-permissions").find(".command-edit").on("click", function (e) {
var uuid = $(this).data("row-id");
mapDataToFormUI({ 'DialogPermission': "/api/rtsphelper/settings/getPermission/" + uuid }).done(function () {
$("#DialogPermission").attr('data-uuid', uuid);
$("#DialogPermission").modal({ backdrop: 'static', keyboard: false });
});
});
$("#grid-permissions").find(".command-delete").on("click", function (e) {
var uuid = $(this).data("row-id");
stdDialogConfirm('{{ lang._('Confirm') }}', '{{ lang._('Do you want to delete this permission ? ') }}', function () {
ajaxCall(url = "/api/rtsphelper/settings/delPermission/" + uuid, sendData = {}, callback = function (data, status) {
$("#grid-permissions").bootgrid("reload");
});
});
});
});
$("#grid-permissions").find("tfoot button[data-action='add']").on("click", function (e) {
$("#DialogPermission").attr('data-uuid', '');
$("#DialogPermission").modal({ backdrop: 'static', keyboard: false });
$("#DialogPermission").find("input").val("");
});
$("#btn_DialogPermission_save").unbind('click').click(function () {
var uuid = $("#DialogPermission").attr('data-uuid');
var url = "/api/rtsphelper/settings/addPermission";
if (uuid) {
url = "/api/rtsphelper/settings/setPermission/" + uuid;
}
saveFormToEndpoint(url = url, formid = 'DialogPermission', callback_ok = function () {
$("#DialogPermission").modal('hide');
$("#grid-permissions").bootgrid("reload");
});
});
$("#grid-status").bootgrid({
ajax: true,
selection: false,
multiSelect: false,
rowCount: [10, 25, 50, -1],
url: '/api/rtsphelper/status/connections',
});
$("#saveAct").click(function () {
saveFormToEndpoint(url = "/api/rtsphelper/settings/set", formid = 'frm_general_settings', callback_ok = function () {
$("#saveAct_progress").addClass("fa fa-spinner fa-pulse");
ajaxCall(url = "/api/rtsphelper/service/reconfigure", sendData = {}, callback = function (data, status) {
$("#saveAct_progress").removeClass("fa fa-spinner fa-pulse");
});
});
});
$("#refreshAct").click(function () {
$("#grid-status").bootgrid("reload");
});
});
</script>

View file

@ -0,0 +1,8 @@
#!/usr/local/bin/php
<?php
require_once(__DIR__ . "/../../../../etc/inc/plugins.inc.d/rtsphelper.inc");
require_once("config.inc");
require_once("util.inc");
rtsphelper_configure_do(true);

View file

@ -0,0 +1,29 @@
[start]
command:/usr/local/bin/python3 /usr/local/opnsense/scripts/net/rtsphelper/rtsphelper.py
type:script
message:starting rtsphelper
[stop]
command:kill -TERM $(cat /var/run/rtsphelper.pid) 2> /dev/null; /sbin/pfctl -artsphelper -Fr 2> /dev/null; /sbin/pfctl -artsphelper -Fn 2> /dev/null; exit 0
type:script
message:stopping rtsphelper
[restart]
command:kill -TERM $(cat /var/run/rtsphelper.pid) 2> /dev/null; /sbin/pfctl -artsphelper -Fr 2> /dev/null; /sbin/pfctl -artsphelper -Fn 2> /dev/null; /usr/local/bin/python3 /usr/local/opnsense/scripts/net/rtsphelper/rtsphelper.py
type:script
message:restarting rtsphelper
[status]
command:if [ -f /var/run/rtsphelper.pid ] && pgrep -F /var/run/rtsphelper.pid > /dev/null; then echo "running"; else echo "stopped"; fi
type:script_output
message:get rtsphelper status
[connections]
command:/sbin/pfctl -artsphelper -sn 2> /dev/null
type:script_output
message:list rtsphelper connections
[configure]
command:/usr/local/bin/php /usr/local/opnsense/scripts/net/rtsphelper/configure.php
type:script
message:configuring rtsphelper