diff --git a/www/nginx/Makefile b/www/nginx/Makefile index 8c403128c..4ef24bcfc 100644 --- a/www/nginx/Makefile +++ b/www/nginx/Makefile @@ -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 diff --git a/www/nginx/pkg-descr b/www/nginx/pkg-descr index fe7205cec..7bb52d9f5 100644 --- a/www/nginx/pkg-descr +++ b/www/nginx/pkg-descr @@ -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 diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php index aefcc98de..1c93711db 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/Api/SettingsController.php @@ -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. + } + } + } } diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php index 257d51da8..817c2b8f3 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/IndexController.php @@ -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; diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml index 3a213f715..8708b85bc 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/httpserver.xml @@ -79,6 +79,13 @@ true Blocks files like .htaccess files or other files not intended for the public. + + httpserver.ip_acl + + dropdown + + If you select an IP ACL, the client can only access this service if it fulfills this requirement. + httpserver.naxsi_extensive_log diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/ipacl.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/ipacl.xml new file mode 100644 index 000000000..c1cd23adf --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/ipacl.xml @@ -0,0 +1,19 @@ +
+ + ipacl.description + + text + Enter a short description like a name for this ACL. It will be shown in the drop downs. + + + ipacl.data + + + hidden + + + ipacl.default_action + + dropdown + +
diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml index 09e767b9d..9334b1377 100644 --- a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/location.xml @@ -152,7 +152,7 @@ location.authbasicuserfile - select_multiple + dropdown Select a credential list to use. @@ -161,6 +161,13 @@ checkbox Send an authentication request to the OPNsense backend for advanced access control. + + location.ip_acl + + dropdown + + If you select an IP ACL, the client can only access this service if it fulfills this requirement. + location.force_https diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/sni_hostname_map.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/sni_hostname_map.xml new file mode 100644 index 000000000..0e27c51b4 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/sni_hostname_map.xml @@ -0,0 +1,14 @@ +
+ + snihostname.description + + text + Enter a short description like a name for this redirect. + + + snihostname.data + + + hidden + +
diff --git a/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/streamserver.xml b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/streamserver.xml new file mode 100644 index 000000000..23a264d94 --- /dev/null +++ b/www/nginx/src/opnsense/mvc/app/controllers/OPNsense/Nginx/forms/streamserver.xml @@ -0,0 +1,61 @@ +
+ + streamserver.listen_port + + text + + + streamserver.udp + + checkbox + + + streamserver.certificate + + dropdown + + + streamserver.ca + + dropdown + + + streamserver.verify_client + + dropdown + true +
  • On: the certificate is requested and validated. Use this option to protect a service with TLS authentication.
  • Off: The certificate is not requested. Choose this option for a normal website.
  • 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.
  • 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.
  • ]]>
    +
    + + streamserver.access_log_format + + dropdown + + + streamserver.route_field + + dropdown + + + + streamserver.upstream + + dropdown + + Select an upstream to proxy to. + + + streamserver.sni_upstream_map + + dropdown + + Select an upstream map to choose the host based on the name given by the client. + + + streamserver.ip_acl + + dropdown + + If you select an IP ACL, the client can only access this service if it fulfills this requirement. + +
    diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.php b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.php index cfed5580e..5453074b4 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.php +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.php @@ -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); + } } diff --git a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml index 1b97ad26a..aebcf735b 100644 --- a/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml +++ b/www/nginx/src/opnsense/mvc/app/models/OPNsense/Nginx/Nginx.xml @@ -300,9 +300,9 @@ name - Selected server not found + Selected user file not found N - Y + N 0 @@ -342,6 +342,18 @@ Y 0 + + + + + Selected ACL not found + N + N + @@ -578,8 +590,194 @@ N Y + + + + + Selected ACL not found + N + N + + + + N + 80 + + + Y + 0 + + + cert + N + + + ca + N + + + Off + + Off + On + Optional + Optional, don't verify + + Y + + + main + +
    Default
    + Anonymized + Disabled +
    + Y +
    + + upstream + + Upstream + SNI Upstream Mapping + + Y + + + + + + Selected upstream not found + N + N + + + This field must be set. + SetIfConstraint + route_field + upstream + + + + + + + + Selected upstream not found + N + N + + + This field must be set. + SetIfConstraint + route_field + sni_upstream_map + + + + + + + + Selected ACL not found + N + N + +
    + + + + Y + + + + Y + + + + + + Y + + + + + + Y + N + + + + + + Y + + + + Y + + + + Deny Access + Allow Access + + N + + + + + + Y + + + deny + + Deny Access + Allow Access + + Y + + + Y @@ -997,6 +1195,7 @@ Y + Y @@ -1006,6 +1205,7 @@ 0 + Y diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt index 648c7fa6d..7b41c062f 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/index.volt @@ -26,117 +26,63 @@ #} + + + + + + +
  • + {{ lang._('IP ACLs')}} +
  • @@ -352,6 +333,29 @@ $( document ).ready(function() { +
    + + + + + + + + + + + + + + + + + +
    {{ lang._('Certificate') }}{{ lang._('UDP') }}{{ lang._('Port') }}{{ lang._('Commands') }}
    + + +
    +
    @@ -528,16 +532,58 @@ $( document ).ready(function() {
    +
    + + + + + + + + + + + + + + + +
    {{ lang._('Description') }}{{ lang._('Commands') }}
    + + +
    +
    +
    + + + + + + + + + + + + + + + +
    {{ lang._('Description') }}{{ lang._('Commands') }}
    + + +
    +
    - {{ 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')]) }} diff --git a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt index e950bfd40..60756a697 100644 --- a/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt +++ b/www/nginx/src/opnsense/mvc/app/views/OPNsense/Nginx/logs.volt @@ -29,4 +29,4 @@ - + diff --git a/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php b/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php index c7b134c75..0dd2d2fe6 100755 --- a/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php +++ b/www/nginx/src/opnsense/scripts/nginx/ngx_autoblock.php @@ -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 diff --git a/www/nginx/src/opnsense/scripts/nginx/setup.php b/www/nginx/src/opnsense/scripts/nginx/setup.php index a14b12c56..65bdca44a 100755 --- a/www/nginx/src/opnsense/scripts/nginx/setup.php +++ b/www/nginx/src/opnsense/scripts/nginx/setup.php @@ -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'] + ); + } } } } diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf index 02a4bc5f9..39f1d911e 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf @@ -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; diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/ipacl.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/ipacl.conf new file mode 100644 index 000000000..fca1e6f8e --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/ipacl.conf @@ -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 %} \ No newline at end of file diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf index c4388e768..13df3959e 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf @@ -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 %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf index 58fd66b66..596bd3cfb 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/nginx.conf @@ -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 %} diff --git a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf new file mode 100644 index 000000000..dfe7cebc1 --- /dev/null +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/streams.conf @@ -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 %} + diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/bundle.js b/www/nginx/src/opnsense/www/js/nginx/dist/bundle.js deleted file mode 100644 index aa20bc987..000000000 --- a/www/nginx/src/opnsense/www/js/nginx/dist/bundle.js +++ /dev/null @@ -1 +0,0 @@ -!function(e){var t={};function n(l){if(t[l])return t[l].exports;var o=t[l]={i:l,l:!1,exports:{}};return e[l].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,l){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:l})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var l=Object.create(null);if(n.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(l,o,function(t){return e[t]}.bind(null,o));return l},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=20)}([function(e,t,n){var l=n(6),o=n(8),i=/[&<>"']/g,r=RegExp(i.source);e.exports=function(e){return(e=o(e))&&r.test(e)?e.replace(i,l):e}},function(e,t,n){var l=n(10).Symbol;e.exports=l},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n\n \n\n'+(null==(__t=_.escape(name))?"":__t)+'\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("username"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("size"))?"":__t)+'\n'+(null==(__t=model.escape("http_referer"))?"":__t)+'\n'+(null==(__t=model.escape("user_agent"))?"":__t)+'\n'+(null==(__t=model.escape("forwarded_for"))?"":__t)+'\n'+(null==(__t=model.escape("request_line"))?"":__t)+"";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("date"))?"":__t)+'\n'+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("severity"))?"":__t)+'\n'+(null==(__t=model.escape("number"))?"":__t)+'\n'+(null==(__t=model.escape("message"))?"":__t)+"";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n \n \n ',"errors"===log_type?__p+="\n \n \n \n \n \n ":__p+="\n \n \n \n \n \n \n \n \n \n ",__p+='\n \n \n ',"errors"===log_type?__p+='\n \n \n \n \n \n ':__p+='\n \n \n \n \n \n \n \n \n \n ',__p+="\n \n \n \n \n
    DateTimeSeverityNumberMessageTimeRemote IPUsernameStatusSizeRefererUser AgentForwarded ForRequest Line
    ";return __p}},function(e,t,n){var l=n(7)({"&":"&","<":"<",">":">",'"':""","'":"'"});e.exports=l},function(e,t){e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},function(e,t,n){var l=n(9);e.exports=function(e){return null==e?"":l(e)}},function(e,t,n){var l=n(1),o=n(13),i=n(14),r=n(15),s=1/0,a=l?l.prototype:void 0,_=a?a.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(i(t))return o(t,e)+"";if(r(t))return _?_.call(t):"";var n=t+"";return"0"==n&&1/t==-s?"-0":n}},function(e,t,n){var l=n(11),o="object"==typeof self&&self&&self.Object===Object&&self,i=l||o||Function("return this")();e.exports=i},function(e,t,n){(function(t){var n="object"==typeof t&&t&&t.Object===Object&&t;e.exports=n}).call(this,n(12))},function(e,t){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){e.exports=function(e,t){for(var n=-1,l=null==e?0:e.length,o=Array(l);++nthis.render_one(e))},render_one:function(e){const t=new i({uuid:e.get("url"),logType:e.get("logType")}),n=new a({collection:t,model:e,logview:this.logview});this.$el.append(n.$el),t.fetch()}}),c=n(3),u=n.n(c),d=n(4),p=n.n(d),m=n(5),h=n.n(m);var f=Backbone.Model.extend({});var b=Backbone.Collection.extend({model:f,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}`},initialize:function(){this.logType="none",this.uuid="none"},filter_collection:function(e){const t=e.keys();return this.filter(function(n){if(!n)return!1;for(let l=0;lthis.render_one(e,t))},render_one:function(e,t){const n=new g({type:this.type,model:t});n.render(),e.append(n.$el)},get_log:function(e,t){this.collection.uuid=t,this.collection.logType=e,this.type=e,this.$el.html(""),this.filter_model.clear(),this.update()},update:function(){this.collection.fetch()},update_filter:function(e){const t=e.target;this.filter_model.set(t.name,$(t).val())}})),v=new _({collection:l,logview:y});$(document.getElementById("logapplication")).append(v.$el).append(y.$el),v.render()}]); diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/configuration.js b/www/nginx/src/opnsense/www/js/nginx/dist/configuration.js new file mode 100644 index 000000000..be46cc622 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/dist/configuration.js @@ -0,0 +1 @@ +!function(e){var t={};function i(n){if(t[n])return t[n].exports;var s=t[n]={i:n,l:!1,exports:{}};return e[n].call(s.exports,s,s.exports,i),s.l=!0,s.exports}i.m=e,i.c=t,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var s in e)i.d(n,s,function(t){return e[t]}.bind(null,s));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i(i.s=21)}({21:function(e,t,i){"use strict";i.r(t);var n=Backbone.View.extend({tagName:"div",attributes:{class:"container-fluid"},child_views:[],createModel:null,upstreamCollection:null,initialize:function(e){this.dataField=$(e.dataField),this.entryclass=e.entryclass,this.createModel=e.createModel,this.upstreamCollection=e.upstreamCollection,this.listenTo(this.collection,"add remove reset",this.render),this.listenTo(this.collection,"change",this.update),this.dataField.after(this.$el)},events:{"click .add":"addEntry"},render:function(){this.child_views.forEach(e=>e.remove()),this.$el.html(""),this.child_views=[],this.update(),this.collection.each(e=>{const t=new this.entryclass({model:e,collection:this.collection,upstreamCollection:this.upstreamCollection});this.child_views.push(t),this.$el.append(t.$el),t.render()}),this.$el.append($('\n
    \n \n
    '))},update:function(){this.dataField.val(JSON.stringify(this.collection.toJSON()))},addEntry:function(e){e.preventDefault(),this.collection.add(this.createModel())}});var s=Backbone.Collection.extend({url:"/api/nginx/settings/searchupstream",parse:function(e){return e.rows}});const l=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(e){this.upstreamCollection=e.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='',this.third.append(this.delBtn),this.model.has("upstream")&&0!==this.upstreamCollection.where({uuid:this.model.get("upstream")}).length||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(){const e=$(this.value);e.html(""),this.upstreamCollection.each(t=>e.append(``)),e.val(this.model.get("upstream")),e.selectpicker("refresh")}}),a=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(e){this.upstreamCollection=e.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='',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(){const e=$(this.value);e.html(""),this.upstreamCollection.each(t=>e.append(``)),e.val(this.model.get("action")),e.selectpicker("refresh")}});var o=Backbone.Collection.extend({initialize:function(){let e=this;$("#snihostname\\.data").change(function(){e.regenerateFromView()})},regenerateFromView:function(){let e=JSON.parse($("#snihostname\\.data").val());_.isArray(e)||(e=[]),this.reset(e)}}),r=Backbone.Model.extend({}),c=Backbone.Model.extend({}),d=Backbone.Collection.extend({initialize:function(){let e=this;$("#ipacl\\.data").change(function(){e.regenerateFromView()})},regenerateFromView:function(){let e=JSON.parse($("#ipacl\\.data").val());_.isArray(e)||(e=[]),this.reset(e)}});const u=new s,h=new Backbone.Collection([{name:"Deny",value:"deny"},{name:"Allow",value:"allow"}]);$(document).ready(function(){mapDataToFormUI({frm_nginx:"/api/nginx/settings/get"}).done(function(){formatTokenizersUI(),$('select[data-allownew="false"]').selectpicker("refresh"),updateServiceControlUI("nginx")}),""!==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(e,t){$(".reloadAct_progress").removeClass("fa-spin")})}),$('[id*="save_"]').each(function(){$(this).click(function(e){let t=$(this).closest("form").attr("id"),i=$(this).closest("form").attr("data-title");saveFormToEndpoint(url="/api/nginx/settings/set",formid=t,callback_ok=function(){$("#"+t+"_progress").addClass("fa fa-spinner fa-pulse"),ajaxCall(url="/api/nginx/service/reconfigure",sendData={},callback=function(e,n){$("#"+t+"_progress").removeClass("fa fa-spinner fa-pulse"),void 0===e||"success"===n&&"ok"===e.status?updateServiceControlUI("nginx"):BootstrapDialog.show({type:BootstrapDialog.TYPE_WARNING,title:i,message:JSON.stringify(e),draggable:!0})})})})}),["upstream","upstreamserver","location","credential","userlist","httpserver","streamserver","httprewrite","custompolicy","security_header","ipacl","limit_zone","cache_path","limit_request_connection","snifwd","naxsirule"].forEach(function(e){$("#grid-"+e).UIBootgrid({search:"/api/nginx/settings/search"+e,get:"/api/nginx/settings/get"+e+"/",set:"/api/nginx/settings/set"+e+"/",add:"/api/nginx/settings/add"+e+"/",del:"/api/nginx/settings/del"+e+"/",options:{selection:!1,multiSelect:!1}})}),bind_naxsi_rule_dl_button(),function(){let e=new n({dataField:document.getElementById("snihostname.data"),upstreamCollection:u,entryclass:l,collection:new o,createModel:function(){return new r({hostname:"localhost"})}});window.snifield=e,e.render(),$("#grid-upstream").on("loaded.rs.jquery.bootgrid",function(){u.fetch()}),u.fetch()}();let e=new n({dataField:document.getElementById("ipacl.data"),upstreamCollection:h,entryclass:a,collection:new d,createModel:function(){return new c({network:"::",action:"deny"})}});window.ipaclfield=e,e.render()})}}); \ No newline at end of file diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.js b/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.js new file mode 100644 index 000000000..7ac306cda --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/dist/logviewer.js @@ -0,0 +1 @@ +!function(e){var t={};function n(l){if(t[l])return t[l].exports;var o=t[l]={i:l,l:!1,exports:{}};return e[l].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,l){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:l})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var l=Object.create(null);if(n.r(l),Object.defineProperty(l,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(l,o,function(t){return e[t]}.bind(null,o));return l},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=20)}([function(e,t,n){var l=n(6),o=n(8),i=/[&<>"']/g,r=RegExp(i.source);e.exports=function(e){return(e=o(e))&&r.test(e)?e.replace(i,l):e}},function(e,t,n){var l=n(10).Symbol;e.exports=l},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n\n \n\n'+(null==(__t=_.escape(name))?"":__t)+'\n\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("remote_ip"))?"":__t)+'\n'+(null==(__t=model.escape("username"))?"":__t)+'\n'+(null==(__t=model.escape("status"))?"":__t)+'\n'+(null==(__t=model.escape("size"))?"":__t)+'\n'+(null==(__t=model.escape("http_referer"))?"":__t)+'\n'+(null==(__t=model.escape("user_agent"))?"":__t)+'\n'+(null==(__t=model.escape("forwarded_for"))?"":__t)+'\n'+(null==(__t=model.escape("request_line"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="";with(obj)__p+=''+(null==(__t=model.escape("date"))?"":__t)+'\n'+(null==(__t=model.escape("time"))?"":__t)+'\n'+(null==(__t=model.escape("severity"))?"":__t)+'\n'+(null==(__t=model.escape("number"))?"":__t)+'\n'+(null==(__t=model.escape("message"))?"":__t)+"\n";return __p}},function(module,exports,__webpack_require__){var _={escape:__webpack_require__(0)};module.exports=function(obj){obj||(obj={});var __t,__p="",__j=Array.prototype.join;function print(){__p+=__j.call(arguments,"")}with(obj)__p+='\n \n \n ',"errors"===log_type?__p+="\n \n \n \n \n \n ":__p+="\n \n \n \n \n \n \n \n \n \n ",__p+='\n \n \n ',"errors"===log_type?__p+='\n \n \n \n \n \n ':__p+='\n \n \n \n \n \n \n \n \n \n ',__p+="\n \n \n \n \n
    DateTimeSeverityNumberMessageTimeRemote IPUsernameStatusSizeRefererUser AgentForwarded ForRequest Line
    \n";return __p}},function(e,t,n){var l=n(7)({"&":"&","<":"<",">":">",'"':""","'":"'"});e.exports=l},function(e,t){e.exports=function(e){return function(t){return null==e?void 0:e[t]}}},function(e,t,n){var l=n(9);e.exports=function(e){return null==e?"":l(e)}},function(e,t,n){var l=n(1),o=n(13),i=n(14),r=n(15),s=1/0,a=l?l.prototype:void 0,_=a?a.toString:void 0;e.exports=function e(t){if("string"==typeof t)return t;if(i(t))return o(t,e)+"";if(r(t))return _?_.call(t):"";var n=t+"";return"0"==n&&1/t==-s?"-0":n}},function(e,t,n){var l=n(11),o="object"==typeof self&&self&&self.Object===Object&&self,i=l||o||Function("return this")();e.exports=i},function(e,t,n){(function(t){var n="object"==typeof t&&t&&t.Object===Object&&t;e.exports=n}).call(this,n(12))},function(e,t){var n;n=function(){return this}();try{n=n||Function("return this")()||(0,eval)("this")}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t){e.exports=function(e,t){for(var n=-1,l=null==e?0:e.length,o=Array(l);++nthis.render_one(e))},render_one:function(e){const t=new i({uuid:e.get("url"),logType:e.get("logType")}),n=new a({collection:t,model:e,logview:this.logview});this.$el.append(n.$el),t.fetch()}}),c=n(3),u=n.n(c),d=n(4),p=n.n(d),m=n(5),h=n.n(m);var f=Backbone.Model.extend({});var b=Backbone.Collection.extend({model:f,url:function(){return`/api/nginx/logs/${this.logType}/${this.uuid}`},initialize:function(){this.logType="none",this.uuid="none"},filter_collection:function(e){const t=e.keys();return this.filter(function(n){if(!n)return!1;for(let l=0;lthis.render_one(e,t))},render_one:function(e,t){const n=new g({type:this.type,model:t});n.render(),e.append(n.$el)},get_log:function(e,t){this.collection.uuid=t,this.collection.logType=e,this.type=e,this.$el.html(""),this.filter_model.clear(),this.update()},update:function(){this.collection.fetch()},update_filter:function(e){const t=e.target;this.filter_model.set(t.name,$(t).val())}})),v=new _({collection:l,logview:y});$(document.getElementById("logapplication")).append(v.$el).append(y.$el),v.render()}]); \ No newline at end of file diff --git a/www/nginx/src/opnsense/www/js/nginx/src/controller/KeyValueMapField.js b/www/nginx/src/opnsense/www/js/nginx/src/controller/KeyValueMapField.js new file mode 100644 index 000000000..66f88e83d --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/controller/KeyValueMapField.js @@ -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($(` +
    + +
    `)); + }, + update: function () { + this.dataField.val(JSON.stringify(this.collection.toJSON())); + }, + addEntry: function (e) { + e.preventDefault(); + this.collection.add(this.createModel()); + } +}); \ No newline at end of file diff --git a/www/nginx/src/opnsense/www/js/nginx/src/controller/KeyValueMapFieldEntry.js b/www/nginx/src/opnsense/www/js/nginx/src/controller/KeyValueMapFieldEntry.js new file mode 100644 index 000000000..6fcd6fea4 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/controller/KeyValueMapFieldEntry.js @@ -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 = ''; + 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(``) + ); + // 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 = ''; + 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(``) + ); + // restore + v.val(this.model.get('action')); + v.selectpicker('refresh'); + } +}); diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/IPACLCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/IPACLCollection.js new file mode 100644 index 000000000..c814ee920 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/IPACLCollection.js @@ -0,0 +1,15 @@ +export default Backbone.Collection.extend({ + initialize: function() { + let that = this; + $('#ipacl\\.data').change(function () { + that.regenerateFromView(); + }); + }, + regenerateFromView: function () { + let data = JSON.parse($('#ipacl\\.data').val()); + if (!_.isArray(data)) { + data = []; + } + this.reset(data); + } +}); \ No newline at end of file diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/IPACLModel.js b/www/nginx/src/opnsense/www/js/nginx/src/models/IPACLModel.js new file mode 100644 index 000000000..457325136 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/IPACLModel.js @@ -0,0 +1,3 @@ +export default Backbone.Model.extend({ + // standard model +}); \ No newline at end of file diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/SNIHostnameUpstreamCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/SNIHostnameUpstreamCollection.js new file mode 100644 index 000000000..3258b0722 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/SNIHostnameUpstreamCollection.js @@ -0,0 +1,15 @@ +export default Backbone.Collection.extend({ + initialize: function() { + let that = this; + $('#snihostname\\.data').change(function () { + that.regenerateFromView(); + }); + }, + regenerateFromView: function () { + let data = JSON.parse($('#snihostname\\.data').val()); + if (!_.isArray(data)) { + data = []; + } + this.reset(data); + } +}); \ No newline at end of file diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/SNIHostnameUpstreamModel.js b/www/nginx/src/opnsense/www/js/nginx/src/models/SNIHostnameUpstreamModel.js new file mode 100644 index 000000000..457325136 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/SNIHostnameUpstreamModel.js @@ -0,0 +1,3 @@ +export default Backbone.Model.extend({ + // standard model +}); \ No newline at end of file diff --git a/www/nginx/src/opnsense/www/js/nginx/src/models/UpstreamCollection.js b/www/nginx/src/opnsense/www/js/nginx/src/models/UpstreamCollection.js new file mode 100644 index 000000000..e6ae51120 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/models/UpstreamCollection.js @@ -0,0 +1,7 @@ +const UpstreamCollection = Backbone.Collection.extend({ + url: '/api/nginx/settings/searchupstream', + parse: function(response) { + return response.rows; + } +}); +export default UpstreamCollection; \ No newline at end of file diff --git a/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js b/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js new file mode 100644 index 000000000..a3c9d11f5 --- /dev/null +++ b/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js @@ -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 (event) { + 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 + }); + } 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(url="/api/nginx/service/reconfigure", sendData={}, callback=function(data,status) { + $(".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(); +}); diff --git a/www/nginx/src/opnsense/www/js/nginx/webpack.conf.js b/www/nginx/src/opnsense/www/js/nginx/webpack.conf.js index 09244d3bf..c28a654fd 100644 --- a/www/nginx/src/opnsense/www/js/nginx/webpack.conf.js +++ b/www/nginx/src/opnsense/www/js/nginx/webpack.conf.js @@ -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].js' }, mode: 'production', module: {