www/nginx: version 1.5

This commit is contained in:
Franco Fichtner 2018-12-11 12:51:21 +01:00
parent 2845096784
commit 107a7f40e3
49 changed files with 1968 additions and 221 deletions

View file

@ -1,5 +1,5 @@
PLUGIN_NAME= nginx
PLUGIN_VERSION= 1.3
PLUGIN_VERSION= 1.5
PLUGIN_COMMENT= Nginx HTTP server and reverse proxy
PLUGIN_DEPENDS= nginx
PLUGIN_MAINTAINER= franz.fabian.94@gmail.com

View file

@ -8,6 +8,35 @@ reuse, SSL offload and HTTP media streaming.
Plugin Changelog
================
1.5
* Add proxy options for ignore client abort and disabling buffering
* Add logviewer support for streams
* Fix charset is not defined bug (contributed by ccesario [1])
* Add an existence check for locations
* Add PROXY protocol for HTTP and Streams frontend
* Add PROXY backend support for Streams
* Add support for better whitelist rules in the WAF
[1] https://github.com/opnsense/plugins/pull/1035
1.4 (Development only)
* 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
* add log rotation (contributed by Julius Cesar Camargo [2])
* add support for satisfy, body size limitation
* change: allow to disable internal bot protection (contributed by @fzoske) [3]
* 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
* replace headers instead of just adding our own (duplication issue #971), suppress X-Powered-By from Upstream [4]
[1] https://github.com/opnsense/plugins/pull/930
[2] https://github.com/opnsense/plugins/pull/982
[3] https://github.com/opnsense/plugins/pull/934
[4] https://github.com/opnsense/plugins/issues/971
1.3
* bugfix: correctly set upstream header

View file

@ -57,6 +57,28 @@ class LogsController extends ApiControllerBase
return $this->call_configd('error', $uuid);
}
}
public function stream_accessesAction($uuid = null)
{
$this->nginx = new Nginx();
if (!isset($uuid)) {
// emulate REST API -> /stream_accesses delivers a list of servers with access logs
return $this->list_streams();
} else {
// emulate REST call for a specific log /stream_accesses/uuid
return $this->call_configd_stream('streamaccess', $uuid);
}
}
public function stream_errorsAction($uuid = null)
{
$this->nginx = new Nginx();
if (!isset($uuid)) {
// emulate REST API -> /stream_errors delivers a list of servers with error logs
return $this->list_streams();
} else {
// emulate REST call for a specific log /stream_errors/uuid
return $this->call_configd_stream('streamerror', $uuid);
}
}
private function call_configd($type, $uuid)
{
@ -68,19 +90,42 @@ class LogsController extends ApiControllerBase
$data = $backend->configdRun('nginx log ' . $type . ' ' . $uuid);
return json_decode($data, true);
}
private function call_configd_stream($type, $uuid)
{
if (!$this->stream_exists($uuid)) {
$this->response->setStatusCode(404, "Not Found");
}
$backend = new Backend();
$data = $backend->configdRun('nginx log ' . $type . ' ' . $uuid);
return json_decode($data, true);
}
private function list_vhosts()
{
$data = [];
foreach ($this->nginx->http_server->__items as $item) {
foreach ($this->nginx->http_server->iterateItems() as $item) {
$data[] = array('id' => $item->getAttributes()['uuid'], 'server_name' => (string)$item->servername);
}
return $data;
}
private function list_streams()
{
$data = [];
foreach ($this->nginx->stream_server->iterateItems() as $item) {
$data[] = array('id' => $item->getAttributes()['uuid'], 'port' => (string)$item->listen_port);
}
return $data;
}
private function vhost_exists($uuid)
{
$data = $this->nginx->getNodeByReference('http_server.'. $uuid);
return isset($data);
}
private function stream_exists($uuid)
{
$data = $this->nginx->getNodeByReference('stream_server.'. $uuid);
return isset($data);
}
}

View file

@ -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.
}
}
}
}

View file

@ -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,9 +58,11 @@ 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;
count($nginx->custom_policy->iterateItems()) == 0 && count($nginx->naxsi_rule->iterateItems()) == 0;
$this->view->pick('OPNsense/Nginx/index');
}

View file

@ -9,6 +9,30 @@
<label>HTTPS Listen Port</label>
<type>text</type>
</field>
<field>
<id>httpserver.proxy_protocol</id>
<label>PROXY Protocol</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>If you enable the proxy protocol, a downstream proxy can send the client IP and port before the real traffic is set.</help>
</field>
<field>
<id>httpserver.trusted_proxies</id>
<label>Trusted Proxies</label>
<allownew>true</allownew>
<style>tokenize</style>
<type>select_multiple</type>
<advanced>true</advanced>
<help>Enter a list of IP addresses or CIDR networks which are allowed to override the source IP address using the specified header.</help>
</field>
<field>
<id>httpserver.real_ip_source</id>
<label>Real IP Source</label>
<style>selectpicker</style>
<advanced>true</advanced>
<help>X-Real-IP and X-Forwarded-For are HTTP headers, while PROXY protocol is a protocol which needs to be enabled.</help>
<type>dropdown</type>
</field>
<field>
<id>httpserver.servername</id>
<label>Server Name</label>
@ -34,6 +58,20 @@
<label>File System Root</label>
<type>text</type>
</field>
<field>
<id>httpserver.max_body_size</id>
<label>Maximum Body Size</label>
<type>text</type>
<advanced>true</advanced>
<help>If the request is larger, it will be rejectet with error 413 (Request Entity Too Large). For example, you can enter 200m.</help>
</field>
<field>
<id>httpserver.body_buffer_size</id>
<label>Body Buffer Size</label>
<type>text</type>
<advanced>true</advanced>
<help>If the request exceeds this size, it will be written to disk. Enter a number and a unit like 1m.</help>
</field>
<field>
<id>httpserver.certificate</id>
<label>TLS Certificate</label>
@ -79,6 +117,28 @@
<advanced>true</advanced>
<help>Blocks files like .htaccess files or other files not intended for the public.</help>
</field>
<field>
<id>httpserver.disable_bot_protection</id>
<label>Disable Bot Protection</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>Blocks the request when a possibly bad bot is detected and adds the originating IP to the managed firewall alias for permanent blocking.</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.satisfy</id>
<label>Satisfy</label>
<type>dropdown</type>
<advanced>true</advanced>
<style>selectpicker</style>
<help>All: All access restrictions must be fulfilled; Any: Any of the access restrictions must be fulfilled.</help>
</field>
<field>
<id>httpserver.naxsi_extensive_log</id>
<label>Extensive Naxsi Log</label>

View file

@ -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>

View file

@ -131,10 +131,26 @@
<type>text</type>
<help>Enter the file system root from which the files are served.</help>
</field>
<field>
<id>location.max_body_size</id>
<label>Maximum Body Size</label>
<type>text</type>
<advanced>true</advanced>
<help>If the request is larger, it will be rejectet with error 413 (Request Entity Too Large). For example, you can enter 200m.</help>
</field>
<field>
<id>location.body_buffer_size</id>
<label>Body Buffer Size</label>
<type>text</type>
<advanced>true</advanced>
<help>If the request exceeds this size, it will be written to disk. Enter a number and a unit like 1m.</help>
</field>
<field>
<id>location.index</id>
<label>Index File</label>
<type>select_multiple</type>
<allownew>true</allownew>
<style>tokenize</style>
<help>Enter a list of file extensions, which are served instead of a directory. It is common to use index.html or index.php here.</help>
</field>
<field>
@ -161,6 +177,21 @@
<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.satisfy</id>
<label>Satisfy</label>
<type>dropdown</type>
<style>selectpicker</style>
<advanced>true</advanced>
<help>All: All access restrictions must be fulfilled; Any: Any of the access restrictions must be fulfilled.</help>
</field>
<field>
<id>location.force_https</id>
<label>Force HTTPS</label>
@ -192,11 +223,37 @@
<advanced>true</advanced>
<help>If you enable the honeypot, all requests to this location will go to a special temporary log which will be used to block the IP. This is dangerous because you may accidentally block legitimate users or search engines. The result is available as a special alias in the firewall section. For example you can trigger on locations of Wordpress for phpMyAdmin if you are not using it.</help>
</field>
<field>
<type>header</type>
<label>Advanced Proxy Options</label>
<advanced>true</advanced>
</field>
<field>
<id>location.websocket</id>
<label>WebSocket</label>
<label>WebSocket Support</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>If you enable the WebSocket Support option, nginx will pass the upgrade header to the backed server.</help>
</field>
<field>
<id>location.proxy_buffering</id>
<label>Response Buffering</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>If you enable the Response Buffering option, nginx will buffer response from the backed server.</help>
</field>
<field>
<id>location.proxy_request_buffering</id>
<label>Request Buffering</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>If you enable the WebSocket option, nginx will pass the upgrade header to the backed server.</help>
</field>
<field>
<id>location.proxy_ignore_client_abort</id>
<label>Ignore Client Abort</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>If you enable this option, nginx will not terminate the connection to the backend server if the client connection is terminated.</help>
</field>
</form>

View file

@ -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>

View file

@ -0,0 +1,76 @@
<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.proxy_protocol</id>
<label>PROXY Protocol</label>
<type>checkbox</type>
<help>If you enable the proxy protocol, a downstream proxy can send the client IP and port before the real traffic is set.</help>
</field>
<field>
<id>httpserver.trusted_proxies</id>
<label>Trusted Proxies</label>
<allownew>true</allownew>
<style>tokenize</style>
<type>select_multiple</type>
<advanced>true</advanced>
<help>Enter a list of IP addresses or CIDR networks which are allowed to override the source IP address using the specified header.</help>
</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>

View file

@ -10,6 +10,13 @@
<style>selectpicker</style>
<type>select_multiple</type>
</field>
<field>
<id>upstream.proxy_protocol</id>
<label>PROXY Protocol</label>
<type>checkbox</type>
<advanced>true</advanced>
<help>If you enable the proxy protocol, an upstream proxy or server will get the client IP and the server port before the real traffic is sent.</help>
</field>
<field>
<id>upstream.tls_enable</id>
<label>Enable TLS (HTTPS)</label>

View file

@ -0,0 +1,39 @@
<?php
/*
Copyright (C) 2018 Fabian Franz
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\Nginx;
class StreamAccessLogLine
{
public $remote_ip;
public $time;
public $status;
public $bytes_sent;
public $bytes_received;
public $session_time;
}

View file

@ -0,0 +1,63 @@
<?php
/*
Copyright (C) 2018 Fabian Franz
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\Nginx;
class StreamAccessLogParser
{
private $file_name;
private $lines;
private $result;
private const LogLineRegex = '/(\S+) \[([\d\sa-z\:\-\/\+]+)\] (\S+?) (\d+) (\d+) (\d+) (\d+(?:\.\d+)?)/i';
function __construct($file_name)
{
$this->file_name = $file_name;
$this->lines = file($this->file_name);
$this->result = array_map([$this, 'parse_line'], $this->lines);
}
private function parse_line($line)
{
$container = new StreamAccessLogLine();
if (preg_match(self::LogLineRegex, $line, $data)) {
$container->remote_ip = $data[1];
$container->time = $data[2];
$container->status = $data[3];
$container->bytes_sent = $data[4];
$container->bytes_received = $data[5];
$container->session_time = $data[6];
}
return $container;
}
public function get_result()
{
return $this->result;
}
}

View file

@ -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);
}
}

View file

@ -1,6 +1,6 @@
<model>
<mount>//OPNsense/Nginx</mount>
<version>1.1.2</version>
<version>1.5.0</version>
<description>nginx web server, reverse proxy and waf</description>
<items>
<general>
@ -74,6 +74,10 @@
<Required>Y</Required>
<multiple>Y</multiple>
</serverentries>
<proxy_protocol type="BooleanField">
<default>0</default>
<Required>Y</Required>
</proxy_protocol>
<store type="BooleanField">
<Required>Y</Required>
<default>0</default>
@ -300,9 +304,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>
@ -330,6 +334,16 @@
<Required>N</Required>
<multiple>Y</multiple>
</limit_request_connections>
<max_body_size type="TextField">
<Required>N</Required>
<mask>/^\d+[kmg]$/i</mask>
<ValidationMessage>Enter a number followed by k, m or g.</ValidationMessage>
</max_body_size>
<body_buffer_size type="TextField">
<Required>N</Required>
<mask>/^\d+[kmg]$/i</mask>
<ValidationMessage>Enter a number followed by k, m or g.</ValidationMessage>
</body_buffer_size>
<honeypot type="BooleanField">
<Required>Y</Required>
<default>0</default>
@ -338,10 +352,41 @@
<Required>Y</Required>
<default>0</default>
</websocket>
<proxy_ignore_client_abort type="BooleanField">
<Required>Y</Required>
<default>0</default>
</proxy_ignore_client_abort>
<proxy_request_buffering type="BooleanField">
<Required>Y</Required>
<default>1</default>
</proxy_request_buffering>
<proxy_buffering type="BooleanField">
<Required>Y</Required>
<default>1</default>
</proxy_buffering>
<http2_push_preload type="BooleanField">
<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>
<satisfy type="OptionField">
<OptionValues>
<any>Any</any>
<all>All</all>
</OptionValues>
<Required>N</Required>
</satisfy>
</location>
<custom_policy type="ArrayField">
@ -398,8 +443,16 @@
<Required>Y</Required>
</ruletype>
<message type="TextField">
<Required>Y</Required>
<Required>N</Required>
<pattern>/^[^"]+$/</pattern>
<Constraints>
<check001>
<ValidationMessage>This field must be set.</ValidationMessage>
<type>SetIfConstraint</type>
<field>match_type</field>
<check>id</check>
</check001>
</Constraints>
</message>
<identifier type="IntegerField">
<Required>Y</Required>
@ -414,8 +467,16 @@
<pattern>/^[^"]+$/</pattern>
</dollar_url>
<match_value type="TextField">
<Required>Y</Required>
<Required>N</Required>
<pattern>/^[^"]+$/</pattern>
<Constraints>
<check001>
<ValidationMessage>This field must be set.</ValidationMessage>
<type>SetIfConstraint</type>
<field>match_type</field>
<check>id</check>
</check001>
</Constraints>
</match_value>
<match_type type="OptionField">
<Required>Y</Required>
@ -429,8 +490,16 @@
<Required>Y</Required>
</negate>
<score type="IntegerField">
<Required>Y</Required>
<Required>N</Required>
<default>8</default>
<Constraints>
<check001>
<ValidationMessage>This field must be set.</ValidationMessage>
<type>SetIfConstraint</type>
<field>match_type</field>
<check>id</check>
</check001>
</Constraints>
</score>
<regex type="BooleanField">
<Required>Y</Required>
@ -473,6 +542,23 @@
<Required>N</Required>
<default>443</default>
</listen_https_port>
<proxy_protocol type="BooleanField">
<default>0</default>
<Required>Y</Required>
</proxy_protocol>
<trusted_proxies type="CSVListField">
<Required>N</Required>
<mask>/^((?:\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?(,?(?:(?:(\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?))*$/i</mask>
<multiple>Y</multiple>
</trusted_proxies>
<real_ip_source type="OptionField">
<OptionValues>
<X-Real-IP>X-Real-IP (default)</X-Real-IP>
<X-Forwarded-For>X-Forwarded-For</X-Forwarded-For>
<proxy_protocol>PROXY Protocol</proxy_protocol>
</OptionValues>
<Required>N</Required>
</real_ip_source>
<locations type="ModelRelationField">
<Model>
<template>
@ -546,6 +632,10 @@
<default>0</default>
<Required>Y</Required>
</block_nonpublic_data>
<disable_bot_protection type="BooleanField">
<default>0</default>
<Required>Y</Required>
</disable_bot_protection>
<naxsi_extensive_log type="BooleanField">
<default>0</default>
<Required>Y</Required>
@ -578,8 +668,226 @@
<Required>N</Required>
<multiple>Y</multiple>
</limit_request_connections>
<max_body_size type="TextField">
<Required>N</Required>
<mask>/^\d+[kmg]$/i</mask>
<ValidationMessage>Enter a number followed by k, m or g.</ValidationMessage>
</max_body_size>
<body_buffer_size type="TextField">
<Required>N</Required>
<mask>/^\d+[kmg]$/i</mask>
<ValidationMessage>Enter a number followed by k, m or g.</ValidationMessage>
</body_buffer_size>
<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>
<satisfy type="OptionField">
<OptionValues>
<any>Any</any>
<all>All</all>
</OptionValues>
<Required>N</Required>
</satisfy>
</http_server>
<stream_server type="ArrayField">
<listen_port type="PortField">
<Required>N</Required>
<default>80</default>
<Constraints>
<check001>
<ValidationMessage>You can only use one server at this port.</ValidationMessage>
<type>UniqueConstraint</type>
</check001>
</Constraints>
</listen_port>
<udp type="BooleanField">
<Required>Y</Required>
<default>0</default>
</udp>
<trusted_proxies type="CSVListField">
<Required>N</Required>
<mask>/^((?:\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?(,?(?:(?:(\d+\.){3,3}\d+|[a-f0-9\:]+)(?:\/\d+)?))*$/i</mask>
<multiple>Y</multiple>
</trusted_proxies>
<proxy_protocol type="BooleanField">
<default>0</default>
<Required>Y</Required>
</proxy_protocol>
<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 +1305,7 @@
<Required>Y</Required>
</description>
</limit_request_connection>
<ban type="ArrayField">
<ip type="NetworkField">
<Required>Y</Required>
@ -1006,6 +1315,7 @@
<MinimumValue>0</MinimumValue>
</time>
</ban>
<cache_path type="ArrayField">
<path type="TextField">
<Required>Y</Required>

View file

@ -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.min.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')]) }}

View file

@ -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.min.js') }}"></script>

View file

@ -89,7 +89,7 @@ $log_lines = $log_parser->get_result();
$model = new Alias();
$blacklist_element = null;
foreach ($model->aliases->alias->__items as $alias) {
foreach ($model->aliases->alias->iterateItems() as $alias) {
if ((string)$alias->name == $autoblock_alias_name) {
if ((string)$alias->type != 'external') {
nginx_print_error('alias is misconfigured - exiting');
@ -111,7 +111,7 @@ if ($blacklist_element == null) {
$model = new Nginx();
$alias_ips = [];
foreach ($model->ban->__items as $entry) {
foreach ($model->ban->iterateItems() as $entry) {
$alias_ips[] = (string)$entry->ip;
}
@ -126,22 +126,29 @@ $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
foreach ($model->ban->__items as $entry) {
foreach ($model->ban->iterateItems() as $entry) {
add_to_blocklist($autoblock_alias_name, (string)$entry->ip);
}

View file

@ -30,6 +30,7 @@ require_once 'config.inc';
use OPNsense\Nginx\Nginx;
use OPNsense\Nginx\ErrorLogParser;
use OPNsense\Nginx\AccessLogParser;
use OPNsense\Nginx\StreamAccessLogParser;
$log_prefix = '/var/log/nginx/';
$log_suffix = '.log';
@ -45,35 +46,72 @@ $mode = $_SERVER['argv'][1];
$server = $_SERVER['argv'][2];
$nginx = new Nginx();
if ($data = $nginx->getNodeByReference('http_server.'. $server)) {
$server_names = (string)$data->servername;
if (empty($server_names)) {
die('{"error": "The server entry has no server name"}');
}
$lines = [];
foreach (explode(',', $server_names) as $server_name) {
$log_file_name = $log_prefix . basename($server_name) . '.' . $mode . $log_suffix;
// this entry has no log file, ignore it
if (!file_exists($log_file_name)) {
continue;
}
$logparser = null;
switch ($mode) {
case 'error':
case 'access':
if ($data = $nginx->getNodeByReference('http_server.'. $server)) {
$server_names = (string)$data->servername;
if (empty($server_names)) {
die('{"error": "The server entry has no server name"}');
}
$lines = [];
foreach (explode(',', $server_names) as $server_name) {
$log_file_name = $log_prefix . basename($server_name) . '.' . $mode . $log_suffix;
// this entry has no log file, ignore it
if (!file_exists($log_file_name)) {
continue;
}
$logparser = null;
if ($mode == 'error') {
$logparser = new ErrorLogParser($log_file_name);
} elseif ($mode == 'access') {
$logparser = new AccessLogParser($log_file_name);
if ($mode == 'error') {
$logparser = new ErrorLogParser($log_file_name);
} elseif ($mode == 'access') {
$logparser = new AccessLogParser($log_file_name);
}
// we cannot parse the file - something went wrong
if ($logparser == null) {
continue;
}
$lines = array_merge($lines, $logparser->get_result());
}
if (empty($lines)) {
$lines['error'] = 'no lines found';
}
echo json_encode($lines);
} else {
die('{"error": "UUID not found"}');
}
// we cannot parse the file - something went wrong
if ($logparser == null) {
continue;
break;
case 'streamerror':
case 'streamaccess':
if ($data = $nginx->getNodeByReference('stream_server.'. $server)) {
$lines = [];
$mode = str_replace('stream', '', $mode);
$log_file_name = $log_prefix . 'stream_' . $server . '.' . $mode . $log_suffix;
// this entry has no log file, ignore it
if (!file_exists($log_file_name)) {
die('{"error": "file not found"}');
}
$logparser = null;
if ($mode == 'error') {
$logparser = new ErrorLogParser($log_file_name);
} elseif ($mode == 'access') {
$logparser = new StreamAccessLogParser($log_file_name);
}
// we cannot parse the file - something went wrong
if ($logparser == null) {
continue;
}
$lines = array_merge($lines, $logparser->get_result());
if (empty($lines)) {
$lines['error'] = 'no lines found';
}
echo json_encode($lines);
} else {
die('{"error": "UUID not found"}');
}
$lines = array_merge($lines, $logparser->get_result());
}
if (empty($lines)) {
$lines['error'] = 'no lines found';
}
echo json_encode($lines);
} else {
die('{"error": "UUID not found"}');
break;
default:
die('{"error": "action (' . $mode . ') not found"}');
}

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']
);
}
}
}
}
@ -170,7 +213,7 @@ if (isset($nginx['upstream'])) {
// export users
$nginx = new Nginx();
foreach ($nginx->userlist->__items as $user_list) {
foreach ($nginx->userlist->iterateItems() as $user_list) {
$attributes = $user_list->getAttributes();
$uuid = $attributes['uuid'];
$file = null;
@ -191,6 +234,6 @@ foreach ($nginx->userlist->__items as $user_list) {
}
}
// create directories for cache
foreach ($nginx->cache_path->__items as $cache_path) {
foreach ($nginx->cache_path->iterateItems() as $cache_path) {
@mkdir((string)$cache_path->path, 0755, true);
}

View file

@ -5,3 +5,4 @@ mime.types:/usr/local/etc/nginx/mime.types
php_fpm:/etc/rc.conf.d/php_fpm
php-www.conf:/usr/local/etc/php-fpm.d/www.conf
php-webgui.conf:/usr/local/etc/php-fpm.d/webgui.conf
newsyslog.conf:/etc/newsyslog.conf.d/nginx

View file

@ -57,9 +57,11 @@ if cache_path.use_temp_path is defined and cache_path.use_temp_path == '1'
{% for server in helpers.toList('OPNsense.Nginx.http_server') %}
{% set single_servername = server.servername.split(",")[0] %}
server {
{% set our_headers = [] %}
{% do our_headers.append('X-Powered-By') %}
{% if server.listen_http_port is defined %}
listen {{ server.listen_http_port }};
listen [::]:{{ server.listen_http_port }};
listen {{ server.listen_http_port }}{% if server.proxy_protocol is defined and server.proxy_protocol == '1' %} proxy_protocol{% endif %};
listen [::]:{{ server.listen_http_port }}{% if server.proxy_protocol is defined and server.proxy_protocol == '1' %} proxy_protocol{% endif %};
{% do listen_list.append(server.listen_http_port) %}
{% endif %}
{% if server.listen_https_port is defined and server.certificate is defined %}
@ -80,14 +82,34 @@ server {
ssl_session_tickets off;
ssl_prefer_server_ciphers on;
add_header Strict-Transport-Security max-age=15768000;
{% do our_headers.append('Strict-Transport-Security') %}
sendfile {% if server.sendfile is defined and server.sendfile == '1' %}On{% else %}Off{% endif %};
{% endif %}
server_name {{ server.servername.replace(',', ' ') }};
{% if server.real_ip_source is defined and server.real_ip_source != '' %}
real_ip_header {{ server.real_ip_source }};
{% if server.trusted_proxies is defined and server.trusted_proxies != '' %}
{% for trusted_proxy in server.trusted_proxies.split(',') %}
set_real_ip_from {{ trusted_proxy }};
{% endfor %}
{% endif %}
{% endif %}
{% if server.charset is defined %}
charset {{ server.charset }};
{% endif %}
access_log /var/log/nginx/{{ server.servername }}.access.log {{ server.access_log_format }};
error_log /var/log/nginx/{{ server.servername }}.error.log;
{% if server.root is defined and server.root != '' %}
root "{{server.root}}";
{% endif %}
{% if server.max_body_size is defined %}
client_max_body_size {{ server.max_body_size }};
{% endif %}
{% if server.body_buffer_size is defined %}
client_body_buffer_size {{ server.body_buffer_size }};
{% endif %}
{% if server.satisfy is defined %}
satisfy {{ server.satisfy }};
{% endif %}
#include tls.conf;
error_page 404 /opnsense_error_404.html;
@ -130,6 +152,7 @@ server {
root /var/etc/acme-client/challenges;
}
{% endif %}
{% if server.disable_bot_protection is not defined or server.disable_bot_protection != '1' %}
# block based on User Agents - stuff I have found over the years in my server log
if ($http_user_agent ~* Python-urllib|Nmap|python-requests|libwww-perl|MJ12bot|Jorgee|fasthttp|libwww|Telesphoreo|A6-Indexer|ltx71|okhttp|ZmEu|sqlmap|LMAO/2.0|ltx71|zgrab|Ronin/2.0|Hakai/2.0) {
return 418;
@ -143,6 +166,11 @@ server {
{
return 418;
}
{% endif %}
{% 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;
@ -200,7 +228,9 @@ server {
{% if server.locations is defined %}
{% for location_uuid in server.locations.split(',') %}
{% set location = helpers.getUUID(location_uuid) %}
{% if location.urlpattern is defined %}
{% include "OPNsense/Nginx/location.conf" ignore missing with context %}
{% endif %}
{% endfor %}
{% endif %}

View file

@ -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 %}

View file

@ -40,9 +40,22 @@ 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 %}
{% if location.max_body_size is defined %}
client_max_body_size {{ location.max_body_size }};
{% endif %}
{% if location.body_buffer_size is defined %}
client_body_buffer_size {{ location.body_buffer_size }};
{% endif %}
{% if location.satisfy is defined %}
satisfy {{ location.satisfy }};
{% endif %}
{% if location.index is defined %}
index {{ location.index.replace(",", " ") }};
{% endif %}
@ -122,6 +135,9 @@ location {{ location.matchtype }} {{ location.urlpattern }} {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_ignore_client_abort {% if location.proxy_ignore_client_abort == '1' %}on{% else %}off{% endif %};
proxy_request_buffering {% if location.proxy_request_buffering == '1' %}on{% else %}off{% endif %};
proxy_buffering {% if location.proxy_buffering == '1' %}on{% else %}off{% endif %};
{% if location.path_prefix is defined and location.path_prefix != '' %}
proxy_pass http{% if upstream.tls_enable == '1' %}s{% endif %}://upstream{{ location.upstream.replace('-','') }}{{ location.path_prefix }};
{% else %}
@ -153,6 +169,9 @@ location {{ location.matchtype }} {{ location.urlpattern }} {
proxy_store {% if upstream.store == '1' %}on{% else %}off{% endif %};
{% endif %}
{% endif %}
{% for our_header in our_headers %}
proxy_hide_header {{ our_header }};
{% endfor %}
{% endif %}
{% endif %}

View file

@ -39,10 +39,14 @@
{{ mz_matches|join('|') }}
{%- endmacro %}
{% macro naxsi_rule(uuid, rule, ruletype) -%}
{% if rule.message is defined and rule.match_value is defined %}
{{ ruletype }}{% if rule.negate is defined and rule.negate == '1' %} negative{% endif
%} {{ rule.match_type }}:{{ rule.identifier }} "{% if rule.regex == '1' %}rx{% else %}str{% endif
%}:{{ rule.match_value }}" "msg:{{ rule.message }}" "mz:{{ naxsi_mzhelper(rule)
}}" "s:$policy{{ uuid.replace('-', '') }}:{{ rule.score }}";
{% else %}
{{ ruletype }} {{ rule.match_type }}:{{ rule.identifier }};
{% endif %}
{%- endmacro %}
{% if naxsi_ruletype == 'basic' %}

View file

@ -0,0 +1,5 @@
# logfilename [owner:group] mode count size when flags [/pid_file] [sig_num]
{% if helpers.exists('OPNsense.Nginx') %}
/var/log/nginx/*access.log www:www 640 14 * @T00 GZB /var/run/nginx.pid 30
/var/log/nginx/*error.log www:www 640 14 * @T00 GZB /var/run/nginx.pid 30
{% endif %}

View file

@ -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 %}

View file

@ -1,18 +1,24 @@
{% if security_rule.referrer is defined %}
{% do our_headers.append('Referrer-Policy') %}
add_header Referrer-Policy "{{ security_rule.referrer }}" always;
{% endif %}
{% if security_rule.xssprotection is defined %}
{% do our_headers.append('X-XSS-Protection') %}
add_header X-XSS-Protection "{{ security_rule.xssprotection }}" always;
{% endif %}
{% if security_rule.content_type_options is defined and security_rule.content_type_options == '1' %}
{% do our_headers.append('X-Content-Type-Options') %}
add_header X-Content-Type-Options "nosniff" always;
{% endif %}
{% if security_rule.strict_transport_security_time is defined %}
{% do our_headers.append('Strict-Transport-Security') %}
add_header Strict-Transport-Security "{{ security_rule.strict_transport_security_time }}{%
if security_rule.strict_transport_security_include_subdomains is defined and
security_rule.strict_transport_security_include_subdomains == '1' %}; includeSubDomains{% endif %}" always;
{% endif %}
{% if security_rule.hpkp_keys is defined and security_rule.hpkp_time is defined %}
{% do our_headers.append('Public-Key-Pins') %}
{% do our_headers.append('Public-Key-Pins-Report-Only') %}
add_header Public-Key-Pins{% if security_rule.hpkp_report_only is defined and security_rule.hpkp_report_only == '1'
%}-Report-Only{% endif %} "{% for key in security_rule.hpkp_keys.split(',')
%}pin-sha256={{ key }}; {% endfor %}max-age={{ security_rule.hpkp_time }}{%
@ -59,6 +65,8 @@
{% endif %}
{% endif %}
{% endfor %}
{% do our_headers.append('Content-Security-Policy') %}
{% do our_headers.append('Content-Security-Policy-Report-Only') %}
add_header Content-Security-Policy{% if security_rule.csp_report_only %}-Report-Only{% endif %} "{%
for key, value in hash_csp.items() %}{{ key }} {{ value|join(' ') }}; {% endfor %}{#
#} report-uri /opnsense-report-csp-violation" always;

View file

@ -0,0 +1,95 @@
# 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 %}{% if server.proxy_protocol is defined and server.proxy_protocol == '1' %} proxy_protocol{% endif %};
listen [::]:{{ server.listen_port }}{% if server.udp is defined and server.udp == '1' %} udp{% endif %}{% if tls_enabled %} ssl{% endif %}{% if server.proxy_protocol is defined and server.proxy_protocol == '1' %} proxy_protocol{% 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 %}
proxy_protocol {% if upstream.proxy_protocol == '1' %}on{% else %}off{% endif %};
{% if server.trusted_proxies is defined and server.trusted_proxies != '' %}
{% for trusted_proxy in server.trusted_proxies.split(',') %}
set_real_ip_from {{ trusted_proxy }};
{% endfor %}
{% endif%}
}
{% endfor %}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1,10 +1,18 @@
export const defaultEndpoints = new Backbone.Collection([
{
"name": 'Access Logs',
"name": 'HTTP Access Logs',
"logType" : 'accesses'
},
{
"name": 'Error Logs',
"name": 'HTTP Error Logs',
"logType" : 'errors'
},
{
"name": 'Stream Access Logs',
"logType" : 'stream_accesses'
},
{
"name": 'Stream Error Logs',
"logType" : 'stream_errors'
}
]);

View file

@ -0,0 +1,50 @@
export default Backbone.View.extend({
tagName: 'div',
attributes: {'class': 'container-fluid'},
child_views: [],
createModel: null,
upstreamCollection: null,
initialize: function (params) {
this.dataField = $(params.dataField);
this.entryclass = params.entryclass;
this.createModel = params.createModel;
this.upstreamCollection = params.upstreamCollection;
this.listenTo(this.collection, "add remove reset", this.render);
this.listenTo(this.collection, "change", this.update);
// inject our table holder
this.dataField.after(this.$el);
},
events: {
"click .add": "addEntry"
},
render: function () {
// clear table
this.child_views.forEach((model) => model.remove());
this.$el.html('');
this.child_views = [];
this.update();
this.collection.each((model) => {
const childView = new this.entryclass({
model: model,
collection: this.collection,
upstreamCollection: this.upstreamCollection
});
this.child_views.push(childView);
this.$el.append(childView.$el);
childView.render();
});
this.$el.append($(`
<div class="row">
<button class="btn btn-primary pull-right add">
<span class="fa fa-plus"></span>
</button>
</div>`));
},
update: function () {
this.dataField.data('data', this.collection.toJSON());
},
addEntry: function (e) {
e.preventDefault();
this.collection.add(this.createModel());
}
});

View file

@ -0,0 +1,152 @@
export const KeyValueMapFieldEntryUpstreamMap = Backbone.View.extend({
tagName: 'div',
attributes: {'class': 'row'},
events: {
'keyup .key': function () {
this.model.set('hostname', this.key.value);
},
'change .value': function () {
this.model.set('upstream', this.value.value);
},
"click .delete" : "deleteEntry"
},
key: null,
value: null,
delBtn: null,
first: null,
second: null,
third: null,
upstreamCollection: null,
initialize: function (params) {
this.upstreamCollection = params.upstreamCollection;
this.listenTo(this.upstreamCollection, "update reset add remove", this.regenerate_list);
this.first = document.createElement('div');
this.first.classList.add('col-sm-5');
this.key = document.createElement('input');
this.first.append(this.key);
this.key.type = 'text';
this.key.classList.add('key');
this.key.value = this.model.get('hostname');
this.second = document.createElement('div');
this.second.classList.add('col-sm-5');
this.value = document.createElement('select');
this.second.append(this.value);
this.value.classList.add('value');
this.value.classList.add('form-control');
this.value.value = this.model.get('upstream');
this.third = document.createElement('div');
this.third.classList.add('col-sm-2');
this.third.style.textAlign = 'right';
this.delBtn = document.createElement("button");
this.delBtn.classList.add('delete');
this.delBtn.classList.add('btn');
this.delBtn.innerHTML = '<span class="fa fa-trash"></span>';
this.third.append(this.delBtn);
if (!this.model.has('upstream') ||
this.upstreamCollection.where ({'uuid' : this.model.get('upstream')}).length === 0) {
if (this.upstreamCollection.length > 0) {
this.model.set('upstream', this.upstreamCollection.at(0).get('uuid'));
}
}
this.$el.append(this.first).append(this.second).append(this.third);
},
render: function() {
$(this.key).val(this.model.get('hostname'));
this.regenerate_list();
$(this.value).val(this.model.get('upstream'));
},
deleteEntry: function (e) {
e.preventDefault();
this.collection.remove(this.model);
},
regenerate_list: function () {
// backup value
const v = $(this.value);
// clear the dropdown
v.html('');
this.upstreamCollection.each(
(mdl) => v.append(`<option value="${mdl.escape('uuid')}">${mdl.escape('description')}</option>`)
);
// restore
v.val(this.model.get('upstream'));
v.selectpicker('refresh');
}
});
export const KeyValueMapFieldEntryACL = Backbone.View.extend({
tagName: 'div',
attributes: {'class': 'row'},
events: {
'keyup .key': function () {
this.model.set('network', this.key.value);
},
'change .value': function () {
this.model.set('action', this.value.value);
},
"click .delete" : "deleteEntry"
},
key: null,
value: null,
delBtn: null,
first: null,
second: null,
third: null,
upstreamCollection: null,
initialize: function (params) {
this.upstreamCollection = params.upstreamCollection;
this.listenTo(this.upstreamCollection, "update reset add remove", this.regenerate_list);
this.first = document.createElement('div');
this.first.classList.add('col-sm-5');
this.key = document.createElement('input');
this.first.append(this.key);
this.key.type = 'text';
this.key.classList.add('key');
this.key.value = this.model.get('network');
this.second = document.createElement('div');
this.second.classList.add('col-sm-5');
this.value = document.createElement('select');
this.second.append(this.value);
this.value.classList.add('value');
this.value.classList.add('form-control');
this.value.value = this.model.get('action');
this.third = document.createElement('div');
this.third.classList.add('col-sm-2');
this.third.style.textAlign = 'right';
this.delBtn = document.createElement("button");
this.delBtn.classList.add('delete');
this.delBtn.classList.add('btn');
this.delBtn.innerHTML = '<span class="fa fa-trash"></span>';
this.third.append(this.delBtn);
this.$el.append(this.first).append(this.second).append(this.third);
},
render: function() {
$(this.key).val(this.model.get('network'));
this.regenerate_list();
$(this.value).val(this.model.get('action'));
},
deleteEntry: function (e) {
e.preventDefault();
this.collection.remove(this.model);
},
regenerate_list: function () {
// backup value
const v = $(this.value);
// clear the dropdown
v.html('');
this.upstreamCollection.each(
(mdl) => v.append(`<option value="${mdl.escape('value')}">${mdl.escape('name')}</option>`)
);
// restore
v.val(this.model.get('action'));
v.selectpicker('refresh');
}
});

View file

@ -1,7 +1,9 @@
import accessLogLine from '../templates/AccessLogLine.html';
import streamAccessLogLine from '../templates/StreamAccessLogLine.html';
import errorLogLine from '../templates/ErrorLogLine.html';
import logViewer from '../templates/logviewer.html';
import LogLinesCollection from "../models/LogLinesCollection";
import noDataAvailable from '../templates/noDataAvailable.html';
const LogViewLine = Backbone.View.extend({
@ -17,6 +19,8 @@ const LogViewLine = Backbone.View.extend({
get_template: function() {
if (this.type === 'accesses') {
return accessLogLine;
} else if (this.type === 'stream_accesses') {
return streamAccessLogLine;
} else {
return errorLogLine;
}
@ -43,15 +47,21 @@ const LogView = Backbone.View.extend({
render: function() {
let tbody = this.$el.find('tbody');
if (tbody.length < 1) {
this.$el.html(logViewer({log_type: this.type, model: this.filter_model}));
tbody = this.$el.find('tbody');
if (this.collection.length !== 0) {
this.$el.html(logViewer({log_type: this.type, model: this.filter_model}));
tbody = this.$el.find('tbody');
} else {
this.$el.html(noDataAvailable);
}
}
else {
tbody.html('');
}
this.collection.filter_collection(this.filter_model).forEach(
(model) => this.render_one(tbody, model)
);
if (this.collection.length !== 0) {
this.collection.filter_collection(this.filter_model).forEach(
(model) => this.render_one(tbody, model)
);
}
},
render_one: function(parent_element, model) {
const logline = new LogViewLine({type: this.type, model: model});

View file

@ -0,0 +1,15 @@
export default Backbone.Collection.extend({
initialize: function() {
let that = this;
$('#ipacl\\.data').change(function () {
that.regenerateFromView();
});
},
regenerateFromView: function () {
let data = $('#ipacl\\.data').data('data');
if (!_.isArray(data)) {
data = [];
}
this.reset(data);
}
});

View file

@ -0,0 +1,3 @@
export default Backbone.Model.extend({
// standard model
});

View file

@ -9,6 +9,12 @@ const LogLinesCollection = Backbone.Collection.extend({
this.logType = 'none';
this.uuid = 'none';
},
parse: function(response) {
if ('error' in response) {
return [];
}
return response;
},
filter_collection: function(filter_model) {
const filter_model_keys = filter_model.keys();
return this.filter(function (model) {

View file

@ -0,0 +1,15 @@
export default Backbone.Collection.extend({
initialize: function() {
let that = this;
$('#snihostname\\.data').change(function () {
that.regenerateFromView();
});
},
regenerateFromView: function () {
let data = $('#snihostname\\.data').data('data');
if (!_.isArray(data)) {
data = [];
}
this.reset(data);
}
});

View file

@ -0,0 +1,3 @@
export default Backbone.Model.extend({
// standard model
});

View file

@ -0,0 +1,7 @@
const UpstreamCollection = Backbone.Collection.extend({
url: '/api/nginx/settings/searchupstream',
parse: function(response) {
return response.rows;
}
});
export default UpstreamCollection;

View file

@ -0,0 +1,151 @@
import KeyValueMapField from './controller/KeyValueMapField';
import UpstreamCollection from './models/UpstreamCollection';
import {
KeyValueMapFieldEntryACL,
KeyValueMapFieldEntryUpstreamMap} from "./controller/KeyValueMapFieldEntry";
import SNIHostnameUpstreamCollection from "./models/SNIHostnameUpstreamCollection";
import SNIHostnameUpstreamModel from "./models/SNIHostnameUpstreamModel";
import IPACLModel from "./models/IPACLModel";
import IPACLCollection from "./models/IPACLCollection";
const uc = new UpstreamCollection();
const actioncollection = new Backbone.Collection([
{
'name': 'Deny',
'value': 'deny'
},
{
'name': 'Allow',
'value': 'allow'
}
]);
function bind_save_buttons() {
// 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("/api/nginx/settings/set", frm_id, function () {
// on correct save, perform reconfigure. set progress animation when reloading
$("#" + frm_id + "_progress").addClass("fa fa-spinner fa-pulse");
ajaxCall("/api/nginx/service/reconfigure", {}, 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
});
} else {
updateServiceControlUI('nginx');
}
});
});
});
});
}
function init_grids() {
['upstream',
'upstreamserver',
'location',
'credential',
'userlist',
'httpserver',
'streamserver',
'httprewrite',
'custompolicy',
'security_header',
'ipacl',
'limit_zone',
'cache_path',
'limit_request_connection',
'snifwd',
'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}
}
);
});
}
function initSNIFieldComponent() {
let snifield = new KeyValueMapField({
dataField: document.getElementById('snihostname.data'),
upstreamCollection: uc,
entryclass: KeyValueMapFieldEntryUpstreamMap,
collection: new SNIHostnameUpstreamCollection(),
createModel: function () {
return new SNIHostnameUpstreamModel({
hostname: 'localhost',
});
}
});
window.snifield = snifield;
snifield.render();
$("#grid-upstream").on("loaded.rs.jquery.bootgrid", function () {
/* we always have to reload too after bootgrid reloads */
uc.fetch();
});
uc.fetch();
}
$( 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("/api/nginx/service/reconfigure", {}, function() {
$(".reloadAct_progress").removeClass("fa-spin");
});
});
bind_save_buttons();
init_grids();
bind_naxsi_rule_dl_button();
initSNIFieldComponent();
let ipaclfield = new KeyValueMapField({
dataField: document.getElementById('ipacl.data'),
upstreamCollection: actioncollection,
entryclass: KeyValueMapFieldEntryACL,
collection: new IPACLCollection(),
createModel: function () {
return new IPACLModel({
network: '::',
action: 'deny'
});
}
});
window.ipaclfield = ipaclfield;
ipaclfield.render();
});

View file

@ -0,0 +1,6 @@
<td class="time"><%= model.escape('time') %></td>
<td class="remote_ip"><%= model.escape('remote_ip') %></td>
<td class="status"><%= model.escape('status') %></td>
<td class="bytes_sent"><%= model.escape('bytes_sent') %></td>
<td class="bytes_received"><%= model.escape('bytes_received') %></td>
<td class="session_time"><%= model.escape('session_time') %></td>

View file

@ -45,7 +45,7 @@
data-model-cid="<%= row.cid %>"
data-model-uuid="<%= row.escape('id') %>"
id="subtab_item_<%= row.escape('id') %>"
href="#subtab_<%= row.escape('id') %>"><%= row.escape('server_name') %></a>
href="#subtab_<%= row.escape('id') %>"><%= row.has("server_name") ? row.escape("server_name") : row.escape("port") %></a>
</li>
<% }) %>
</ul>

View file

@ -29,5 +29,5 @@
*/
%>
<a data-toggle="tab" href="#subtab_item_<%= model.escape('id') %>">
<b><%= model.escape("server_name") %></b>
<b><%= model.has("server_name") ? model.escape("server_name") : model.escape("port") %></b>
</a>

View file

@ -1,13 +1,13 @@
<table class="table table-striped">
<thead>
<tr>
<% if (log_type === 'errors') { %>
<% if (log_type === 'errors' || log_type === 'stream_errors') { %>
<th>Date</th>
<th>Time</th>
<th>Severity</th>
<th>Number</th>
<th>Message</th>
<% } else { %>
<% } else if (log_type === 'accesses') { %>
<th>Time</th>
<th>Remote IP</th>
<th>Username</th>
@ -17,16 +17,23 @@
<th>User Agent</th>
<th>Forwarded For</th>
<th>Request Line</th>
<% } else {%>
<th>Time</th>
<th>Remote IP</th>
<th>Status</th>
<th>Bytes Sent</th>
<th>Bytes Received</th>
<th>Session Time</th>
<% } %>
</tr>
<tr class="filter">
<% if (log_type === 'errors') { %>
<% if (log_type === 'errors' || log_type === 'stream_errors') { %>
<td><input type="text" value="<%= model.escape('date') %>" name="date" /></td>
<td><input type="text" value="<%= model.escape('time') %>" name="time" /></td>
<td><input type="text" value="<%= model.escape('severity') %>" name="severity" /></td>
<td><input type="text" value="<%= model.escape('number') %>" name="number" /></td>
<td><input type="text" value="<%= model.escape('message') %>" name="message" /></td>
<% } else { %>
<% } else if (log_type === 'accesses') { %>
<td><input type="text" value="<%= model.escape('time') %>" name="time" /></td>
<td><input type="text" value="<%= model.escape('remote_ip') %>" name="remote_ip" /></td>
<td><input type="text" value="<%= model.escape('username') %>" name="username" /></td>
@ -36,6 +43,13 @@
<td><input type="text" value="<%= model.escape('user_agent') %>" name="user_agent" /></td>
<td><input type="text" value="<%= model.escape('forwarded_for') %>" name="forwarded_for" /></td>
<td><input type="text" value="<%= model.escape('request_line') %>" name="request_line" /></td>
<% } else { %>
<td><input type="text" value="<%= model.escape('time') %>" name="time" /></td>
<td><input type="text" value="<%= model.escape('remote_ip') %>" name="remote_ip" /></td>
<td><input type="text" value="<%= model.escape('status') %>" name="status" /></td>
<td><input type="text" value="<%= model.escape('bytes_sent') %>" name="bytes_sent" /></td>
<td><input type="text" value="<%= model.escape('bytes_received') %>" name="bytes_received" /></td>
<td><input type="text" value="<%= model.escape('session_time') %>" name="session_time" /></td>
<% } %>
</tr>
</thead>

View file

@ -0,0 +1,3 @@
<div style="text-align: center;">
No data avialable
</div>

View file

@ -1,10 +1,13 @@
const path = require('path'), webpack = require('webpack');
module.exports = {
entry: './src/logviewer.js',
entry: {
'logviewer': './src/logviewer.js',
'configuration': './src/nginx_config.js'
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
filename: '[name].min.js'
},
mode: 'production',
module: {