www/nginx: streams support, fix config store (#930)

* www/nginx: add streams upstream block to template

* www/nginx: stream proxy work in progress

* www/nginx: fix typo

* www/nginx: work in progress map component mvc

* www/nginx: working prototype of key value map

* www/nginx: implement dropdown

* www/nginx: adjust model to store array elements

* www/nginx: sync model

* www/nginx: working edit

* www/nginx: working delete

* working template

* www/nginx: fix some template code, menu extract

* www/nginx: syntax is now correct

* www/nginx: add contraint to check if the value is theire

* www/nginx: version bump / release note

* www/nginx: fix prev

* www/nginx: working SNI Upstream map

* www/nginx: export keys for streams

* www/nginx: do not fill up the config change log with empty changes

* www/nginx: update release note, fix some conding issues

* www/nginx: WIP IP ACL

* www/nginx: strams - add IP based ACLs

* www/nginx: run bmake style-fix

* www/nginx: add an entry to the release note

* www/nginx: check in the production version, this is smaller

* www/nginx: also support IP ACLs in HTTP Server and HTTP Location

* www/nginx: extract constraint to core

* fix copy and paste issue

* www/ngix: user list is not multiple

* Revert "www/nginx: fix for #981"

This reverts commit 54fc763
This commit is contained in:
Fabian Franz BSc
2018-11-13 19:33:44 +01:00
committed by GitHub
parent 54fc763061
commit 7a49be585c
32 changed files with 1366 additions and 167 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
PLUGIN_NAME= nginx
PLUGIN_VERSION= 1.3
PLUGIN_VERSION= 1.4
PLUGIN_COMMENT= Nginx HTTP server and reverse proxy
PLUGIN_DEPENDS= nginx
PLUGIN_MAINTAINER= franz.fabian.94@gmail.com
+12
View File
@@ -8,6 +8,18 @@ reuse, SSL offload and HTTP media streaming.
Plugin Changelog
================
1.4
* move upstreams from HTTP to their own menu because they are used for TCP load balancing as well
* add TCP load balancing [1]
* add support for IP based ACLs
* change: allow to disable internal bot protection (contributed by @fzoske) [2]
* change: do not save when no change in the list happened to prevent filling the log history
* fix: translate a german string in upstream server to english
[1] https://github.com/opnsense/plugins/pull/930
[2] https://github.com/opnsense/plugins/pull/934
1.3
* bugfix: correctly set upstream header
@@ -236,6 +236,33 @@ class SettingsController extends ApiMutableModelControllerBase
return $this->setBase('httpserver', 'http_server', $uuid);
}
// stream server
public function searchstreamserverAction()
{
return $this->searchBase('stream_server', array('description', 'certificate', 'udp', 'listen_port'));
}
public function getstreamserverAction($uuid = null)
{
$this->sessionClose();
return $this->getBase('streamserver', 'stream_server', $uuid);
}
public function addstreamserverAction()
{
return $this->addBase('streamserver', 'stream_server');
}
public function delstreamserverAction($uuid)
{
return $this->delBase('stream_server', $uuid);
}
public function setstreamserverAction($uuid)
{
return $this->setBase('streamserver', 'stream_server', $uuid);
}
// naxsi rules
public function searchnaxsiruleAction()
{
@@ -405,4 +432,207 @@ class SettingsController extends ApiMutableModelControllerBase
{
return $this->setBase('cache_path', 'cache_path', $uuid);
}
// SNI Forward
public function searchsnifwdAction()
{
return $this->searchBase('sni_hostname_upstream_map', array('description'));
}
public function getsnifwdAction($uuid = null)
{
$this->sessionClose();
$base = $this->getBase('snihostname', 'sni_hostname_upstream_map', $uuid);
return $this->convert_sni_fwd_for_client($base);
}
public function addsnifwdAction()
{
if ($this->request->isPost()) {
$this->regenerate_hostname_map(null);
return $this->addBase('snihostname', 'sni_hostname_upstream_map');
}
return [];
}
public function delsnifwdAction($uuid)
{
$nginx = $this->getModel();
$uuid_attached = $nginx->find_sni_hostname_upstream_map_entry_uuids($uuid);
$ret = $this->delBase('sni_hostname_upstream_map', $uuid);
if ($ret['result'] == 'deleted') {
foreach ($uuid_attached as $old_uuid) {
$this->delBase('sni_hostname_upstream_map_item', $old_uuid);
}
}
return $ret;
}
public function setsnifwdAction($uuid)
{
if ($this->request->isPost()) {
$this->regenerate_hostname_map($uuid);
return $this->setBase('snihostname', 'sni_hostname_upstream_map', $uuid);
}
return [];
}
// IP / Network based ACLs
public function searchipaclAction()
{
return $this->searchBase('ip_acl', array('description'));
}
public function getipaclAction($uuid = null)
{
$this->sessionClose();
$base = $this->getBase('ipacl', 'ip_acl', $uuid);
return $this->convert_ipacl_for_client($base);
}
public function addipaclAction()
{
if ($this->request->isPost()) {
$this->regenerate_ipacl(null);
return $this->addBase('ipacl', 'ip_acl');
}
return [];
}
public function delipaclAction($uuid)
{
$nginx = $this->getModel();
$uuid_attached = $nginx->find_ip_acl_entry_uuids($uuid);
$ret = $this->delBase('ip_acl', $uuid);
if ($ret['result'] == 'deleted') {
foreach ($uuid_attached as $old_uuid) {
$this->delBase('ip_acl_item', $old_uuid);
}
}
return $ret;
}
public function setipaclAction($uuid)
{
if ($this->request->isPost()) {
$this->regenerate_ipacl($uuid);
return $this->setBase('ipacl', 'ip_acl', $uuid);
}
return [];
}
/*
* worker code starts here
*/
private function convert_sni_fwd_for_client($response_data)
{
if (!isset($response_data['snihostname']['data'])) {
return $response_data;
}
$nginx = $this->getModel();
$uuids_map = explode(',', $response_data['snihostname']['data']);
$response_data['snihostname']['data'] = [];
foreach ($uuids_map as $uuid_line) {
$rowdata = $nginx->getNodeByReference('sni_hostname_upstream_map_item.' . $uuid_line);
if ($rowdata != null) {
$response_data['snihostname']['data'][] =
array('hostname' => (string)$rowdata->hostname,
'upstream' => (string)$rowdata->upstream);
}
}
return $response_data;
}
private function convert_ipacl_for_client($response_data)
{
if (!isset($response_data['ipacl']['data'])) {
return $response_data;
}
$nginx = $this->getModel();
$uuids_map = explode(',', $response_data['ipacl']['data']);
$response_data['ipacl']['data'] = [];
foreach ($uuids_map as $uuid_line) {
$rowdata = $nginx->getNodeByReference('ip_acl_item.' . $uuid_line);
if ($rowdata != null) {
$response_data['ipacl']['data'][] =
array('network' => (string)$rowdata->network,
'action' => (string)$rowdata->action);
}
}
return $response_data;
}
/**
* @param null $uuid the uuid which should get cleared before
* @throws \ReflectionException if the model was not found
* @throws \Phalcon\Validation\Exception on validation errors
*/
private function regenerate_hostname_map($uuid = null)
{
$nginx = $this->getModel();
if ($this->request->hasPost('snihostname') && is_array($_POST['snihostname']['data'])) {
if ($uuid != null) {
// for an update, we have to clear it.
$this->delete_uuids(
$nginx->find_sni_hostname_upstream_map_entry_uuids($uuid),
'sni_hostname_upstream_map_item'
);
}
$ids = [];
$postdata = $_POST['snihostname']['data'];
foreach ($postdata as $post_item) {
$item = $nginx->sni_hostname_upstream_map_item->Add();
$ids[] = $item->getAttributes()['uuid'];
$item->hostname = $post_item['hostname'];
$item->upstream = $post_item['upstream'];
}
$nginx->serializeToConfig();
$_POST['snihostname']['data'] = implode(',', $ids);
}
}
/**
* @param null $uuid the uuid which should get cleared before
* @throws \ReflectionException if the model was not found
* @throws \Phalcon\Validation\Exception on validation errors
*/
private function regenerate_ipacl($uuid = null)
{
$nginx = $this->getModel();
if ($this->request->hasPost('ipacl') && is_array($_POST['ipacl']['data'])) {
if ($uuid != null) {
// for an update, we have to clear it.
$this->delete_uuids(
$nginx->find_ip_acl_uuids($uuid),
'ip_acl_item'
);
}
$ids = [];
$postdata = $_POST['ipacl']['data'];
foreach ($postdata as $post_item) {
$item = $nginx->ip_acl_item->Add();
$ids[] = $item->getAttributes()['uuid'];
$item->network = $post_item['network'];
$item->action = $post_item['action'];
}
$nginx->serializeToConfig();
$_POST['ipacl']['data'] = implode(',', $ids);
}
}
/**
* @param $uuids array list of UUIDs
* @param $path string the model prefix from the element to delete
* @throws \Phalcon\Validation\Exception
*/
private function delete_uuids($uuids, $path): void
{
foreach ($uuids as $item_uuid) {
try {
$this->delBase($path, $item_uuid);
} catch (\Exception $e) {
// we don't care about then.
}
}
}
}
@@ -50,6 +50,7 @@ class IndexController extends \OPNsense\Base\IndexController
$this->view->credential = $this->getForm("credential");
$this->view->userlist = $this->getForm("userlist");
$this->view->httpserver = $this->getForm("httpserver");
$this->view->streamserver = $this->getForm("streamserver");
$this->view->httprewrite = $this->getForm("httprewrite");
$this->view->naxsi_rule = $this->getForm("naxsi_rule");
$this->view->naxsi_custom_policy = $this->getForm("naxsi_custom_policy");
@@ -57,6 +58,8 @@ class IndexController extends \OPNsense\Base\IndexController
$this->view->limit_request_connection = $this->getForm("limit_request_connection");
$this->view->limit_zone = $this->getForm("limit_zone");
$this->view->cache_path = $this->getForm("cache_path");
$this->view->sni_hostname_map = $this->getForm("sni_hostname_map");
$this->view->ipacl = $this->getForm("ipacl");
$nginx = new Nginx();
$this->view->show_naxsi_download_button =
count($nginx->custom_policy->__items) == 0 && count($nginx->naxsi_rule->__items) == 0;
@@ -79,6 +79,13 @@
<advanced>true</advanced>
<help>Blocks files like .htaccess files or other files not intended for the public.</help>
</field>
<field>
<id>httpserver.ip_acl</id>
<label>IP ACL</label>
<type>dropdown</type>
<style>selectpicker</style>
<help>If you select an IP ACL, the client can only access this service if it fulfills this requirement.</help>
</field>
<field>
<id>httpserver.naxsi_extensive_log</id>
<label>Extensive Naxsi Log</label>
@@ -0,0 +1,19 @@
<form>
<field>
<id>ipacl.description</id>
<label>Description</label>
<type>text</type>
<help>Enter a short description like a name for this ACL. It will be shown in the drop downs.</help>
</field>
<field>
<id>ipacl.data</id>
<style>json-data</style>
<label>ACL Entries</label>
<type>hidden</type>
</field>
<field>
<id>ipacl.default_action</id>
<label>Default Action</label>
<type>dropdown</type>
</field>
</form>
@@ -152,7 +152,7 @@
<field>
<id>location.authbasicuserfile</id>
<label>Basic Credentials List</label>
<type>select_multiple</type>
<type>dropdown</type>
<help>Select a credential list to use.</help>
</field>
<field>
@@ -161,6 +161,13 @@
<type>checkbox</type>
<help>Send an authentication request to the OPNsense backend for advanced access control.</help>
</field>
<field>
<id>location.ip_acl</id>
<label>IP ACL</label>
<type>dropdown</type>
<style>selectpicker</style>
<help>If you select an IP ACL, the client can only access this service if it fulfills this requirement.</help>
</field>
<field>
<id>location.force_https</id>
<label>Force HTTPS</label>
@@ -0,0 +1,14 @@
<form>
<field>
<id>snihostname.description</id>
<label>Short description (to display)</label>
<type>text</type>
<help>Enter a short description like a name for this redirect.</help>
</field>
<field>
<id>snihostname.data</id>
<style>json-data</style>
<label>Hostname Upstream Map</label>
<type>hidden</type>
</field>
</form>
@@ -0,0 +1,61 @@
<form>
<field>
<id>streamserver.listen_port</id>
<label>Listen Port</label>
<type>text</type>
</field>
<field>
<id>streamserver.udp</id>
<label>UDP Port</label>
<type>checkbox</type>
</field>
<field>
<id>streamserver.certificate</id>
<label>TLS Certificate</label>
<type>dropdown</type>
</field>
<field>
<id>streamserver.ca</id>
<label>CA Certificate</label>
<type>dropdown</type>
</field>
<field>
<id>streamserver.verify_client</id>
<label>Verify Client Certificate</label>
<type>dropdown</type>
<advanced>true</advanced>
<help><![CDATA[<ul><li>On: the certificate is requested and validated. Use this option to protect a service with TLS authentication.</li><li>Off: The certificate is not requested. Choose this option for a normal website.</li><li>Optional: The certificate is requested and validated if existing. Choose this option for websites, with TLS login support or mixed TLS protected API and web content.</li><li>Optional, don't verify: Do accept the certificate and let the application choose what to do. Choose this option, for the same reasons as optional but in this case, the request is passed to the backend without rejecting untrusted certificates.</li></ul>]]></help>
</field>
<field>
<id>streamserver.access_log_format</id>
<label>Access Log Format</label>
<type>dropdown</type>
</field>
<field>
<id>streamserver.route_field</id>
<label>Route With</label>
<type>dropdown</type>
<style>selectpicker</style>
</field>
<field>
<id>streamserver.upstream</id>
<label>Upstream Servers</label>
<type>dropdown</type>
<style>selectpicker</style>
<help>Select an upstream to proxy to.</help>
</field>
<field>
<id>streamserver.sni_upstream_map</id>
<label>SNI Upstream Mapping</label>
<type>dropdown</type>
<style>selectpicker</style>
<help>Select an upstream map to choose the host based on the name given by the client.</help>
</field>
<field>
<id>streamserver.ip_acl</id>
<label>IP ACL</label>
<type>dropdown</type>
<style>selectpicker</style>
<help>If you select an IP ACL, the client can only access this service if it fulfills this requirement.</help>
</field>
</form>
@@ -31,4 +31,35 @@ use OPNsense\Base\BaseModel;
class Nginx extends BaseModel
{
/**
* @param $uuid string UUID of sni_hostname_upstream_map
* @return array list of UUIDs
*/
function find_sni_hostname_upstream_map_entry_uuids($uuid)
{
return $this->find_x_uuids($uuid, 'sni_hostname_upstream_map.');
}
/**
* @param $uuid string UUID of sni_hostname_upstream_map
* @return array list of UUIDs
*/
function find_ip_acl_uuids($uuid)
{
return $this->find_x_uuids($uuid, 'ip_acl.');
}
private function find_x_uuids($uuid, $prefix)
{
$tmp = $this->getNodeByReference($prefix . $uuid);
if ($tmp == null) {
return [];
}
$tmp = (string)$tmp->data;
if (empty($tmp)) {
return [];
}
return explode(',', $tmp);
}
}
@@ -300,9 +300,9 @@
<display>name</display>
</template>
</Model>
<ValidationMessage>Selected server not found</ValidationMessage>
<ValidationMessage>Selected user file not found</ValidationMessage>
<Required>N</Required>
<multiple>Y</multiple>
<multiple>N</multiple>
</authbasicuserfile>
<advanced_acl type="BooleanField">
<default>0</default>
@@ -342,6 +342,18 @@
<Required>Y</Required>
<default>0</default>
</http2_push_preload>
<ip_acl type="ModelRelationField">
<Model>
<template>
<source>OPNsense.Nginx.Nginx</source>
<items>ip_acl</items>
<display>description</display>
</template>
</Model>
<ValidationMessage>Selected ACL not found</ValidationMessage>
<Required>N</Required>
<multiple>N</multiple>
</ip_acl>
</location>
<custom_policy type="ArrayField">
@@ -578,8 +590,194 @@
<Required>N</Required>
<multiple>Y</multiple>
</limit_request_connections>
<ip_acl type="ModelRelationField">
<Model>
<template>
<source>OPNsense.Nginx.Nginx</source>
<items>ip_acl</items>
<display>description</display>
</template>
</Model>
<ValidationMessage>Selected ACL not found</ValidationMessage>
<Required>N</Required>
<multiple>N</multiple>
</ip_acl>
</http_server>
<stream_server type="ArrayField">
<listen_port type="PortField">
<Required>N</Required>
<default>80</default>
</listen_port>
<udp type="BooleanField">
<Required>Y</Required>
<default>0</default>
</udp>
<certificate type="CertificateField">
<Type>cert</Type>
<Required>N</Required>
</certificate>
<ca type="CertificateField">
<Type>ca</Type>
<Required>N</Required>
</ca>
<verify_client type="OptionField">
<default>Off</default>
<OptionValues>
<off>Off</off>
<on>On</on>
<optional>Optional</optional>
<optional_no_ca>Optional, don't verify</optional_no_ca>
</OptionValues>
<Required>Y</Required>
</verify_client>
<access_log_format type="OptionField">
<default>main</default>
<OptionValues>
<main>Default</main>
<anonymized>Anonymized</anonymized>
<disabled>Disabled</disabled>
</OptionValues>
<Required>Y</Required>
</access_log_format>
<route_field type="OptionField">
<default>upstream</default>
<OptionValues>
<upstream>Upstream</upstream>
<sni_upstream_map>SNI Upstream Mapping</sni_upstream_map>
</OptionValues>
<Required>Y</Required>
</route_field>
<upstream type="ModelRelationField">
<Model>
<template>
<source>OPNsense.Nginx.Nginx</source>
<items>upstream</items>
<display>description</display>
</template>
</Model>
<ValidationMessage>Selected upstream not found</ValidationMessage>
<Required>N</Required>
<multiple>N</multiple>
<Constraints>
<check001>
<ValidationMessage>This field must be set.</ValidationMessage>
<type>SetIfConstraint</type>
<field>route_field</field>
<check>upstream</check>
</check001>
</Constraints>
</upstream>
<sni_upstream_map type="ModelRelationField">
<Model>
<template>
<source>OPNsense.Nginx.Nginx</source>
<items>sni_hostname_upstream_map</items>
<display>description</display>
</template>
</Model>
<ValidationMessage>Selected upstream not found</ValidationMessage>
<Required>N</Required>
<multiple>N</multiple>
<Constraints>
<check001>
<ValidationMessage>This field must be set.</ValidationMessage>
<type>SetIfConstraint</type>
<field>route_field</field>
<check>sni_upstream_map</check>
</check001>
</Constraints>
</sni_upstream_map>
<ip_acl type="ModelRelationField">
<Model>
<template>
<source>OPNsense.Nginx.Nginx</source>
<items>ip_acl</items>
<display>description</display>
</template>
</Model>
<ValidationMessage>Selected ACL not found</ValidationMessage>
<Required>N</Required>
<multiple>N</multiple>
</ip_acl>
</stream_server>
<sni_hostname_upstream_map type="ArrayField">
<description type="TextField">
<Required>Y</Required>
</description>
<data type="TextField">
<!-- sorry, the model relation field is broken here
<Model>
<template>
<source>OPNsense.Nginx.Nginx</source>
<items>sni_hostname_upstream_map_items</items>
<display>hostname</display>
</template>
</Model>
<multiple>Y</multiple>
-->
<Required>Y</Required>
</data>
</sni_hostname_upstream_map>
<sni_hostname_upstream_map_item type="ArrayField">
<hostname type="HostnameField">
<Required>Y</Required>
</hostname>
<upstream type="ModelRelationField">
<Model>
<template>
<source>OPNsense.Nginx.Nginx</source>
<items>upstream</items>
<display>description</display>
</template>
</Model>
<Required>Y</Required>
<multiple>N</multiple>
</upstream>
</sni_hostname_upstream_map_item>
<ip_acl type="ArrayField">
<description type="TextField">
<Required>Y</Required>
</description>
<data type="TextField">
<!-- sorry, the model relation field is broken here
<Model>
<template>
<source>OPNsense.Nginx.Nginx</source>
<items>ip_acl_item</items>
<display>description</display>
</template>
</Model>
<multiple>Y</multiple>
-->
<Required>Y</Required>
</data>
<default_action type="OptionField">
<OptionValues>
<deny>Deny Access</deny>
<allow>Allow Access</allow>
</OptionValues>
<Required>N</Required>
</default_action>
</ip_acl>
<ip_acl_item type="ArrayField">
<network type="NetworkField">
<Required>Y</Required>
</network>
<action type="OptionField">
<default>deny</default>
<OptionValues>
<deny>Deny Access</deny>
<allow>Allow Access</allow>
</OptionValues>
<Required>Y</Required>
</action>
</ip_acl_item>
<http_rewrite type="ArrayField">
<description type="TextField">
<Required>Y</Required>
@@ -997,6 +1195,7 @@
<Required>Y</Required>
</description>
</limit_request_connection>
<ban type="ArrayField">
<ip type="NetworkField">
<Required>Y</Required>
@@ -1006,6 +1205,7 @@
<MinimumValue>0</MinimumValue>
</time>
</ban>
<cache_path type="ArrayField">
<path type="TextField">
<Required>Y</Required>
@@ -26,117 +26,63 @@
#}
<script>
$( document ).ready(function() {
let data_get_map = {'frm_nginx':'/api/nginx/settings/get'};
// load initial data
mapDataToFormUI(data_get_map).done(function(){
formatTokenizersUI();
$('select[data-allownew="false"]').selectpicker('refresh')
updateServiceControlUI('nginx');
});
// update history on tab state and implement navigation
if(window.location.hash !== "") {
$('a[href="' + window.location.hash + '"]').click()
}
$('.nav-tabs a').on('shown.bs.tab', function (e) {
history.pushState(null, null, e.target.hash);
});
$('.reload_btn').click(function() {
$(".reloadAct_progress").addClass("fa-spin");
ajaxCall(url="/api/nginx/service/reconfigure", sendData={}, callback=function(data,status) {
$(".reloadAct_progress").removeClass("fa-spin");
});
});
// form save event handlers for all defined forms
$('[id*="save_"]').each(function(){
$(this).click(function() {
let frm_id = $(this).closest("form").attr("id");
let frm_title = $(this).closest("form").attr("data-title");
// save data for General TAB
saveFormToEndpoint(url="/api/nginx/settings/set", formid=frm_id, callback_ok=function(){
// on correct save, perform reconfigure. set progress animation when reloading
$("#"+frm_id+"_progress").addClass("fa fa-spinner fa-pulse");
ajaxCall(url="/api/nginx/service/reconfigure", sendData={}, callback=function(data,status){
// when done, disable progress animation.
$("#"+frm_id+"_progress").removeClass("fa fa-spinner fa-pulse");
if (data !== undefined && (status !== "success" || data['status'] !== 'ok')) {
// fix error handling
BootstrapDialog.show({
type:BootstrapDialog.TYPE_WARNING,
title: frm_title,
message: JSON.stringify(data),
draggable: true
function bind_naxsi_rule_dl_button() {
let naxsi_rule_download_button = $('#naxsiruledownloadbtn');
naxsi_rule_download_button.click(function () {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_INFO,
title: "{{ lang._('Download NAXSI Rules') }}",
message: "{{ lang._('You are about to download the core rules from the Repository of NAXSI. You have to accept its %slicense%s to download the rules.')|format("<a href='https://github.com/nbs-system/naxsi/blob/master/LICENSE' target='_blank'>", "</a>") }}",
buttons: [{
label: "{{ lang._('Accept And Download') }}",
cssClass: 'btn-primary',
icon: 'fa fa-download',
action: function (dlg) {
dlg.close();
ajaxCall(url = "/api/nginx/settings/downloadrules", sendData = {}, callback = function (data, status) {
$('#naxsiruledownloadalert').hide();
// reload view after installing rules
$('#grid-naxsirule').bootgrid('reload');
$('#grid-custompolicy').bootgrid('reload');
});
} else {
updateServiceControlUI('nginx');
}
});
}, {
label: '{{ lang._('Reject') }}',
action: function (dlg) {
dlg.close();
}
}]
});
});
});
['upstream',
'upstreamserver',
'location',
'credential',
'userlist',
'httpserver',
'httprewrite',
'custompolicy',
'security_header',
'limit_zone',
'cache_path',
'limit_request_connection',
'naxsirule'].forEach(function(element) {
$("#grid-" + element).UIBootgrid(
{ 'search':'/api/nginx/settings/search' + element,
'get':'/api/nginx/settings/get' + element + '/',
'set':'/api/nginx/settings/set' + element + '/',
'add':'/api/nginx/settings/add' + element + '/',
'del':'/api/nginx/settings/del' + element + '/',
'options':{selection:false, multiSelect:false}
}
);
});
let naxsi_rule_download_button = $('#naxsiruledownloadbtn');
naxsi_rule_download_button.click(function () {
BootstrapDialog.show({
type: BootstrapDialog.TYPE_INFO,
title: "{{ lang._('Download NAXSI Rules') }}",
message: "{{ lang._('You are about to download the core rules from the Repository of NAXSI. You have to accept its %slicense%s to download the rules.')|format("<a href='https://github.com/nbs-system/naxsi/blob/master/LICENSE' target='_blank'>", "</a>") }}",
buttons: [{
label: "{{ lang._('Accept And Download') }}",
cssClass: 'btn-primary',
icon: 'fa fa-download',
action: function(dlg){
dlg.close();
ajaxCall(url="/api/nginx/settings/downloadrules", sendData={}, callback=function(data,status) {
$('#naxsiruledownloadalert').hide();
// reload view after installing rules
$('#grid-naxsirule').bootgrid('reload');
$('#grid-custompolicy').bootgrid('reload');
});
}
}, {
label: '{{ lang._('Reject') }}',
action: function(dlg){
dlg.close();
}
}]
});
});
});
}
</script>
<script src="{{ cache_safe('/ui/js/nginx/lib/lodash.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/nginx/lib/backbone-min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/nginx/dist/configuration.js') }}"></script>
<style>
#frm_sni_hostname_mapdlg .col-md-4,
#frm_ipacl_dlg .col-md-4 {
width: 50%;
}
#frm_sni_hostname_mapdlg td > input[type="text"],
#frm_ipacl_dlg td > input[type="text"] {
width: 100%;
max-width: 100%;
}
#frm_sni_hostname_mapdlg .col-md-5,
#frm_ipacl_dlg .col-md-5 {
width: 25%;
}
#row_snihostname\.data .row div,
#row_ipacl\.data .row div {
padding: 0;
}
#sni_hostname_mapdlg .bootstrap-select,
#frm_ipacl_dlg .bootstrap-select {
width: 100% !important;
}
</style>
<ul class="nav nav-tabs" role="tablist" id="maintabs">
@@ -160,12 +106,6 @@ $( document ).ready(function() {
<li>
<a data-toggle="tab" id="subtab_item_nginx-http-userlist" href="#subtab_nginx-http-userlist">{{ lang._('User List')}}</a>
</li>
<li>
<a data-toggle="tab" id="subtab_item_nginx-http-upstream-server" href="#subtab_nginx-http-upstream-server">{{ lang._('Upstream Server')}}</a>
</li>
<li>
<a data-toggle="tab" id="subtab_item_nginx-http-upstream" href="#subtab_nginx-http-upstream">{{ lang._('Upstream')}}</a>
</li>
<li>
<a data-toggle="tab" id="subtab_item_nginx-http-server" href="#subtab_nginx-http-httpserver">{{ lang._('HTTP Server')}}</a>
</li>
@@ -186,6 +126,44 @@ $( document ).ready(function() {
</li>
</ul>
</li>
<li role="presentation" class="dropdown">
<a data-toggle="dropdown"
href="#"
class="dropdown-toggle pull-right visible-lg-inline-block visible-md-inline-block visible-xs-inline-block visible-sm-inline-block"
role="button">
<b><span class="caret"></span></b>
</a>
<a data-toggle="tab" onclick="$('#subtab_item_nginx-streams-streamserver').click();"
class="visible-lg-inline-block visible-md-inline-block visible-xs-inline-block visible-sm-inline-block"
style="border-right:0px;"><b>{{ lang._('Data Streams')}}</b></a>
<ul class="dropdown-menu" role="menu">
<li>
<a data-toggle="tab" id="subtab_item_nginx-streams-streamserver" href="#subtab_nginx-streams-streamserver">{{ lang._('Stream Servers')}}</a>
</li>
<li>
<a data-toggle="tab" id="subtab_item_nginx-streams-snifwd" href="#subtab_nginx-streams-snifwd">{{ lang._('SNI Based Routing')}}</a>
</li>
</ul>
</li>
<li role="presentation" class="dropdown">
<a data-toggle="dropdown"
href="#"
class="dropdown-toggle pull-right visible-lg-inline-block visible-md-inline-block visible-xs-inline-block visible-sm-inline-block"
role="button">
<b><span class="caret"></span></b>
</a>
<a data-toggle="tab" onclick="$('#subtab_item_nginx-http-upstream-server').click();"
class="visible-lg-inline-block visible-md-inline-block visible-xs-inline-block visible-sm-inline-block"
style="border-right: 0;"><b>{{ lang._('Upstream')}}</b></a>
<ul class="dropdown-menu" role="menu">
<li>
<a data-toggle="tab" id="subtab_item_nginx-http-upstream-server" href="#subtab_nginx-http-upstream-server">{{ lang._('Upstream Server')}}</a>
</li>
<li>
<a data-toggle="tab" id="subtab_item_nginx-http-upstream" href="#subtab_nginx-http-upstream">{{ lang._('Upstream')}}</a>
</li>
</ul>
</li>
<li role="presentation" class="dropdown">
<a data-toggle="dropdown"
href="#"
@@ -203,6 +181,9 @@ $( document ).ready(function() {
<li>
<a data-toggle="tab" id="subtab_item_nginx-access-request-limit-connection" href="#subtab_nginx-access-request-limit-connection">{{ lang._('Connection Limits')}}</a>
</li>
<li>
<a data-toggle="tab" id="subtab_item_nginx-acl-ip" href="#subtab_nginx-acl-ip">{{ lang._('IP ACLs')}}</a>
</li>
</ul>
</li>
</ul>
@@ -352,6 +333,29 @@ $( document ).ready(function() {
</tfoot>
</table>
</div>
<div id="subtab_nginx-streams-streamserver" class="tab-pane fade">
<table id="grid-streamserver" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="streamserverdlg">
<thead>
<tr>
<th data-column-id="certificate" data-type="string" data-sortable="true" data-visible="true">{{ lang._('Certificate') }}</th>
<th data-column-id="udp" data-type="string" data-sortable="true" data-visible="true">{{ lang._('UDP') }}</th>
<th data-column-id="listen_port" data-type="string" data-sortable="true" data-visible="true">{{ lang._('Port') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button type="button" class="btn btn-xs reload_btn btn-primary"><span class="fa fa-refresh reloadAct_progress"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
<div id="subtab_nginx-http-rewrite" class="tab-pane fade">
<table id="grid-httprewrite" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="httprewritedlg">
<thead>
@@ -528,16 +532,58 @@ $( document ).ready(function() {
</tfoot>
</table>
</div>
<div id="subtab_nginx-streams-snifwd" class="tab-pane fade">
<table id="grid-snifwd" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="sni_hostname_mapdlg">
<thead>
<tr>
<th data-column-id="description" data-type="string" data-sortable="true" data-visible="true">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button type="button" class="btn btn-xs reload_btn btn-primary"><span class="fa fa-refresh reloadAct_progress"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
<div id="subtab_nginx-acl-ip" class="tab-pane fade">
<table id="grid-ipacl" class="table table-condensed table-hover table-striped table-responsive" data-editDialog="ipacl_dlg">
<thead>
<tr>
<th data-column-id="description" data-type="string" data-sortable="true" data-visible="true">{{ lang._('Description') }}</th>
<th data-column-id="commands" data-width="7em" data-formatter="commands" data-sortable="false">{{ lang._('Commands') }}</th>
</tr>
</thead>
<tbody>
</tbody>
<tfoot>
<tr>
<td></td>
<td>
<button data-action="add" type="button" class="btn btn-xs btn-default"><span class="fa fa-plus"></span></button>
<button type="button" class="btn btn-xs reload_btn btn-primary"><span class="fa fa-refresh reloadAct_progress"></span></button>
</td>
</tr>
</tfoot>
</table>
</div>
</div>
{{ partial("layout_partials/base_dialog",['fields': upstream,'id':'upstreamdlg', 'label':lang._('Edit Upstream')]) }}
{{ partial("layout_partials/base_dialog",['fields': upstream_server,'id':'upstreamserverdlg', 'label':lang._('Edit Upstream')]) }}
{{ partial("layout_partials/base_dialog",['fields': location,'id':'locationdlg', 'label':lang._('Edit Location')]) }}
{{ partial("layout_partials/base_dialog",['fields': credential,'id':'credentialdlg', 'label':lang._('Edit Credential')]) }}
{{ partial("layout_partials/base_dialog",['fields': userlist,'id':'userlistdlg', 'label':lang._('Edit User List')]) }}
{{ partial("layout_partials/base_dialog",['fields': httpserver,'id':'httpserverdlg', 'label':lang._('Edit HTTP Server')]) }}
{{ partial("layout_partials/base_dialog",['fields': streamserver,'id':'streamserverdlg', 'label':lang._('Edit Stream Server')]) }}
{{ partial("layout_partials/base_dialog",['fields': httprewrite,'id':'httprewritedlg', 'label':lang._('Edit URL Rewrite')]) }}
{{ partial("layout_partials/base_dialog",['fields': naxsi_custom_policy,'id':'custompolicydlg', 'label':lang._('Edit WAF Policy')]) }}
{{ partial("layout_partials/base_dialog",['fields': naxsi_rule,'id':'naxsiruledlg', 'label':lang._('Edit Naxsi Rule')]) }}
@@ -545,3 +591,5 @@ $( document ).ready(function() {
{{ partial("layout_partials/base_dialog",['fields': limit_request_connection,'id':'limit_request_connectiondlg', 'label':lang._('Edit Request Connection Limit')]) }}
{{ partial("layout_partials/base_dialog",['fields': limit_zone,'id':'limit_zonedlg', 'label':lang._('Edit Limit Zone')]) }}
{{ partial("layout_partials/base_dialog",['fields': cache_path,'id':'cache_pathdlg', 'label':lang._('Edit Cache Path')]) }}
{{ partial("layout_partials/base_dialog",['fields': sni_hostname_map,'id':'sni_hostname_mapdlg', 'label':lang._('Edit SNI Hostname Mapping')]) }}
{{ partial("layout_partials/base_dialog",['fields': ipacl,'id':'ipacl_dlg', 'label':lang._('Edit IP ACL')]) }}
@@ -29,4 +29,4 @@
<script src="{{ cache_safe('/ui/js/nginx/lib/lodash.min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/nginx/lib/backbone-min.js') }}"></script>
<script src="{{ cache_safe('/ui/js/nginx/dist/bundle.js') }}"></script>
<script src="{{ cache_safe('/ui/js/nginx/dist/logviewer.js') }}"></script>
@@ -126,18 +126,25 @@ $new_ips = array_unique(
}, $log_lines)
);
$change_required = false;
foreach (array_diff($new_ips, $alias_ips) as $new_ip) {
$entry = $model->ban->Add();
$entry->ip = $new_ip;
$entry->time = time();
$change_required = true;
}
$val_result = $model->performValidation(false);
if (count($val_result) !== 0) {
print_r($val_result);
exit(1);
if ($change_required) {
$val_result = $model->performValidation(false);
if (count($val_result) !== 0) {
print_r($val_result);
exit(1);
}
$model->serializeToConfig();
Config::getInstance()->save();
}
$model->serializeToConfig();
Config::getInstance()->save();
echo '{"status":"saved"}';
// all ips are used because the others may not be set for some reason
+82 -39
View File
@@ -67,49 +67,92 @@ function find_ca($refid)
if (!isset($config['OPNsense']['Nginx'])) {
die("nginx is not configured");
}
$nginx = $config['OPNsense']['Nginx'];
if (!isset($nginx['http_server'])) {
die("no http servers configured");
}
if (is_array($nginx['http_server']) && !isset($nginx['http_server']['servername'])) {
$http_servers = $nginx['http_server'];
} else {
$http_servers = array($nginx['http_server']);
}
@mkdir('/usr/local/etc/nginx/key', 0750, true);
@mkdir("/var/db/nginx/auth", 0750, true);
foreach ($http_servers as $http_server) {
if (!empty($http_server['listen_https_port']) && !empty($http_server['certificate'])) {
// try to find the reference
$cert = find_cert($http_server['certificate']);
if (!isset($cert)) {
next;
}
$chain = [];
$ca_chain = ca_chain_array($cert);
if (is_array($ca_chain)) {
foreach ($ca_chain as $entry) {
$chain[] = base64_decode($entry['crt']);
$nginx = $config['OPNsense']['Nginx'];
if (isset($nginx['http_server'])) {
if (is_array($nginx['http_server']) && !isset($nginx['http_server']['servername'])) {
$http_servers = $nginx['http_server'];
} else {
$http_servers = array($nginx['http_server']);
}
foreach ($http_servers as $http_server) {
if (!empty($http_server['listen_https_port']) && !empty($http_server['certificate'])) {
// try to find the reference
$cert = find_cert($http_server['certificate']);
if (!isset($cert)) {
next;
}
$chain = [];
$ca_chain = ca_chain_array($cert);
if (is_array($ca_chain)) {
foreach ($ca_chain as $entry) {
$chain[] = base64_decode($entry['crt']);
}
}
$hostname = explode(',', $http_server['servername'])[0];
export_pem_file(
KEY_DIRECTORY . $hostname . '.pem',
$cert['crt'],
implode("\n", $chain)
);
export_pem_file(
KEY_DIRECTORY . $hostname . '.key',
$cert['prv']
);
if (!empty($http_server['ca'])) {
foreach ($http_server['ca'] as $caref) {
$ca = find_ca($caref);
if (isset($ca)) {
export_pem_file(
KEY_DIRECTORY . $hostname . '_ca.pem',
$ca['crt']
);
}
}
}
}
$hostname = explode(',', $http_server['servername'])[0];
export_pem_file(
KEY_DIRECTORY . $hostname . '.pem',
$cert['crt'],
implode("\n", $chain)
);
export_pem_file(
KEY_DIRECTORY . $hostname . '.key',
$cert['prv']
);
if (!empty($http_server['ca'])) {
foreach ($http_server['ca'] as $caref) {
$ca = find_ca($caref);
if (isset($ca)) {
export_pem_file(
KEY_DIRECTORY . $hostname . '_ca.pem',
$ca['crt']
);
}
}
// end http, begin streams
if (isset($nginx['stream_server'])) {
if (is_array($nginx['stream_server']) && !isset($nginx['stream_server']['servername'])) {
$stream_servers = $nginx['stream_server'];
} else {
$stream_servers = array($nginx['stream_server']);
}
foreach ($stream_servers as $stream_server) {
if (!empty($stream_server['listen_port']) && !empty($stream_server['certificate'])) {
// try to find the reference
$cert = find_cert($stream_server['certificate']);
if (!isset($cert)) {
next;
}
$chain = [];
$ca_chain = ca_chain_array($cert);
if (is_array($ca_chain)) {
foreach ($ca_chain as $entry) {
$chain[] = base64_decode($entry['crt']);
}
}
export_pem_file(
KEY_DIRECTORY . $stream_server['@attributes']['uuid'] . '.pem',
$cert['crt'],
implode("\n", $chain)
);
export_pem_file(
KEY_DIRECTORY . $stream_server['@attributes']['uuid'] . '.key',
$cert['prv']
);
if (!empty($stream_server['ca'])) {
foreach ($stream_server['ca'] as $caref) {
$ca = find_ca($caref);
if (isset($ca)) {
export_pem_file(
KEY_DIRECTORY . $hostname . '_ca.pem',
$ca['crt']
);
}
}
}
}
@@ -143,6 +143,10 @@ server {
{
return 418;
}
{% if server.ip_acl is defined %}
{% set ip_acl = server.ip_acl %}
{% include "OPNsense/Nginx/ipacl.conf" %}
{% endif %}
location = /opnsense-report-csp-violation {
include fastcgi_params;
@@ -0,0 +1,15 @@
# IP ACL
{% if ip_acl is defined %}
{% set ipacl_data = helpers.getUUID(ip_acl) %}
{% if ipacl_data is defined %}
{% for acl_entry_uuid in ipacl_data.data.split(',') %}
{% set acl_entry = helpers.getUUID(acl_entry_uuid) %}
{% if acl_entry is defined %}
{{ acl_entry.action }} {{ acl_entry.network }};
{% endif %}
{% endfor %}
{% if ipacl_data.default_action is defined %}
{{ ipacl_data.default_action }} all;
{% endif %}
{% endif %}
{% endif %}
@@ -40,6 +40,10 @@ location {{ location.matchtype }} {{ location.urlpattern }} {
return 302 https://$host$request_uri;
}
{% endif %}
{% if location.ip_acl is defined %}
{% set ip_acl = server.ip_acl %}
{% include "OPNsense/Nginx/ipacl.conf" %}
{% endif %}
{% if location.root is defined %}
root {{ location.root }};
{% endif %}
@@ -17,10 +17,14 @@ events {
http {
{% if helpers.exists('OPNsense.Nginx') %}
{# include http blocks partial #}
{% include "OPNsense/Nginx/http.conf" ignore missing with context %}
{% include "OPNsense/Nginx/http.conf" %}
{% endif %}
}
{% if helpers.exists('OPNsense.Nginx') %}
stream {
{# include streams blocks partial #}
{% include "OPNsense/Nginx/streams.conf" %}
}
# mail {
{# include http blocks partial #}
{% include "OPNsense/Nginx/mail.conf" ignore missing with context %}
@@ -0,0 +1,90 @@
# LOG FORMATS
log_format main '$remote_addr [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time';
log_format anonymized ':: [$time_local] '
'$protocol $status $bytes_sent $bytes_received '
'$session_time';
# UPSTREAM SERVERS
{% for upstream in helpers.toList('OPNsense.Nginx.upstream') %}
upstream upstream{{ upstream['@uuid'].replace('-','') }} {
hash $remote_addr consistent;
{% for upstream_serveruuid in upstream.serverentries.split(',') %}
{% set upstream_server = helpers.getUUID(upstream_serveruuid) %}
server {% if ':' in upstream_server.server %}[{% endif %}{{ upstream_server.server }}{% if ':' in upstream_server.server %}]{% endif
%}{% if upstream_server.port is defined %}:{{ upstream_server.port }}{% endif
%}{% if upstream_server.priority is defined %} weight={{ upstream_server.priority }}{% endif
%}{% if upstream_server.max_conns is defined %} max_conns={{ upstream_server.max_conns }}{% endif
%}{% if upstream_server.max_fails is defined %} max_fails={{ upstream_server.max_fails }}{% endif
%}{% if upstream_server.fail_timeout is defined %} fail_timeout={{ upstream_server.fail_timeout }}{% endif
%}{% if upstream_server.no_use is defined %} {{ upstream_server.no_use }}{% endif %};
{% endfor %}
}
{% endfor %}
# upstream maps
{% for upstream_map in helpers.toList('OPNsense.Nginx.sni_hostname_upstream_map') %}
map $ssl_preread_server_name $hostmap{{ upstream_map['@uuid'].replace('-','') }} {
{% for map_entry_uuid in upstream_map.data.split(',') %}
{% set map_entry = helpers.getUUID(map_entry_uuid) %}
{{ map_entry.hostname }} upstream{{ map_entry.upstream.replace('-','') }};
{% endfor %}
}
{% endfor %}
{% for server in helpers.toList('OPNsense.Nginx.stream_server') %}
# servers
server {
{% set tls_enabled = server.certificate is defined %}
{% if server.listen_port is defined %}
listen {{ server.listen_port }}{% if server.udp is defined and server.udp == '1' %} udp{% endif %}{% if tls_enabled %} ssl{% endif %};
listen [::]:{{ server.listen_port }}{% if server.udp is defined and server.udp == '1' %} udp{% endif %}{% if tls_enabled %} ssl{% endif %};
{% endif %}
access_log /var/log/nginx/stream_{{ server['@uuid'] }}.access.log main;
error_log /var/log/nginx/stream_{{ server['@uuid'] }}.error.log info;
{% if server.route_field == 'sni_upstream_map' %}
ssl_preread on;
{% endif %}
{% if server.ip_acl is defined %}
{% set ip_acl = server.ip_acl %}
{% include "OPNsense/Nginx/ipacl.conf" %}
{% endif %}
{% if server.certificate is defined %}
{% if server.ca is defined %}
ssl_client_certificate /usr/local/etc/nginx/key/{{ server['@uuid'] }}_ca.pem;
ssl_verify_client {{ server.verify_client }};
{% endif %}
ssl_certificate_key /usr/local/etc/nginx/key/{{ server['@uuid'] }}.key;
ssl_certificate /usr/local/etc/nginx/key/{{ server['@uuid'] }}.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_dhparam /usr/local/etc/dh-parameters.4096;
ssl_ciphers 'ECDHE-ECDSA-CAMELLIA256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-CAMELLIA256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-CAMELLIA128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-CAMELLIA128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-CAMELLIA256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-CAMELLIA256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-ECDSA-CAMELLIA128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_session_timeout 1d;
ssl_session_cache shared:sslcache{{ server['@uuid'].replace('-','') }}:50m;
ssl_session_tickets off;
ssl_prefer_server_ciphers on;
{% endif %}
{% if server.route_field == 'upstream' %}
{% if server.upstream is defined %}
{% set upstream = helpers.getUUID(server.upstream) %}
{% if upstream.tls_enable == '1' %}
{% if upstream.tls_client_certificate is defined and upstream.tls_client_certificate != '' %}
proxy_ssl_certificate_key /usr/local/etc/nginx/key/{{ upstream.tls_client_certificate }}.key;
proxy_ssl_certificate /usr/local/etc/nginx/key/{{ upstream.tls_client_certificate }}.pem;
{% endif %}
{% endif %}
proxy_ssl {% if upstream.tls_enable == '1' %}on{% else %}off{% endif %};
proxy_pass upstream{{ server.upstream.replace('-','') }};
{% endif %}
{% elif server.route_field == 'sni_upstream_map' %}
proxy_pass $hostmap{{ server.sni_upstream_map.replace('-','') }};
{% endif %}
}
{% endfor %}

Some files were not shown because too many files have changed in this diff Show More