diff --git a/www/nginx/pkg-descr b/www/nginx/pkg-descr index 729721ed5..bcb357234 100644 --- a/www/nginx/pkg-descr +++ b/www/nginx/pkg-descr @@ -32,6 +32,7 @@ Plugin Changelog * User interface improvements of NAXSI configuration (contributed by 8191) * Fixed missing certificate validation of upstreams (contributed by 8191) +* Add custom error pages on a per HTTP server basis (contributed by 8191) 1.19 diff --git a/www/nginx/src/etc/nginx/views/opnsense_error_403.html b/www/nginx/src/etc/nginx/views/opnsense_error_403.html new file mode 100644 index 000000000..05b6871ec --- /dev/null +++ b/www/nginx/src/etc/nginx/views/opnsense_error_403.html @@ -0,0 +1,60 @@ + + + + + Access Denied + + + + + + +

Access Denied

+

You are not permitted to access the requested resource.

+

Please contact the webmaster if you think this is an error.

+
+ + + diff --git a/www/nginx/src/etc/nginx/views/opnsense_error_404.html b/www/nginx/src/etc/nginx/views/opnsense_error_404.html index 48e45ca98..714896406 100644 --- a/www/nginx/src/etc/nginx/views/opnsense_error_404.html +++ b/www/nginx/src/etc/nginx/views/opnsense_error_404.html @@ -5,6 +5,7 @@ Not Found + + Enter the HTTP status codes for which the error page shall be shown. + + + errorpage.pagecontent + + textbox + The HTML page content to display. + + + errorpage.redirect + + text + Instead of displaying a static error page redirect to a URL. + true + + + errorpage.response + + dropdown + + Optionally enter a HTTP status code to change the response to. This does not work in combination with a redirection target. + true + + 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 62b3f44d6..f59e1e5f7 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 @@ -237,4 +237,11 @@ If you choose multiple limits, the strictest will be used. + + httpserver.errorpages + + + select_multiple + Select custom error pages to display instead of the default builtin error pages. If at least one error page is selected here, all default error pages will be disabled. + 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 05adce938..97b247d9b 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 @@ -35,6 +35,14 @@ checkbox Enable learning mode means nothing is blocked but logged. + + location.secrules_errorpage + + + dropdown + Select a custom error page to display instead of the default builtin error pages for security rule violations. Only the page content itself is used. Status code rewriting and redirection is not supported. + true + location.xss_block_score @@ -312,4 +320,11 @@ true If you enable this option, nginx will not terminate the connection to the backend server if the client connection is terminated. + + location.errorpages + + + select_multiple + Select custom error pages to display instead of the default builtin error pages. Selection will override error pages configured on HTTP server. + 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 d079f90b7..c5ac1ca22 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 @@ -204,6 +204,21 @@ 0 Y + + + + + Selected error page(s) not found + N + N + N @@ -464,6 +479,18 @@ Y 0 + + + + + Selected error page(s) not found + N + Y + @@ -852,6 +879,18 @@ Y 0 + + + + + Selected error page(s) not found + N + Y + @@ -1442,6 +1481,71 @@ + + + Y + + + Y + + 400 Bad Request + 401 Unauthorized + 403 Forbidden + 404 Not Found + 405 Method Not Allowed + 407 Proxy Authentication Required + 408 Request Timeout + 410 Gone + 415 Unsupported Media Type + 429 Too Many Requests + 431 Request Header Fields Too Large + 500 Internal Server Error + 501 Not Implemented + 502 Bad Gateway + 503 Service Unavailable + 504 Gateway Timeout + + Y + + + PCFET0NUWVBFIGh0bWw+CjxodG1sPgo8aGVhZD4KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04Ij4KICAgIDxtZXRhIG5hbWU9InZpZXdwb3J0IiBjb250ZW50PSJ3aWR0aD1kZXZpY2Utd2lkdGgsaW5pdGlhbC1zY2FsZT0xIiAvPgogICAgPHRpdGxlPkVycm9yPC90aXRsZT4KPC9oZWFkPgo8Ym9keT4KICAgIDxoMT5FcnJvcjwvaDE+CiAgICA8cD5Tb3JyeSwgYnV0IHNvbWV0aGluZyB3ZW50IHdyb25nLjwvcD4KPC9ib2R5Pgo8L2h0bWw+ + + + SingleSelectConstraint + Page content is required if redirect is not used and needs to be empty otherwise. + + redirect + + Y + + + + + N + + + pagecontent.check001 + + + + + N + + 200 OK + 300 Multiple Choice + 301 Moved Permanently + 302 Found + 401 Unauthorized + 403 Forbidden + 404 Not Found + 451 Unavailable For Legal Reasons + 500 Internal Server Error + 503 Service Unavailable + + N + + + 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 ec5a47d71..7ad59f716 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 @@ -124,6 +124,9 @@
  • {{ lang._('Cache Path')}}
  • +
  • + {{ lang._('Error Pages')}} +
  • {{ lang._('TLS Fingerprint (Advanced)')}}
  • @@ -606,6 +609,29 @@ +
    + + + + + + + + + + + + + + + + + +
    {{ lang._('Name') }}{{ lang._('Status Codes') }}{{ lang._('Response') }}{{ lang._('Commands') }}
    + + +
    +
    @@ -670,5 +696,6 @@ {{ 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')]) }} +{{ partial("layout_partials/base_dialog",['fields': errorpage,'id':'errorpage_dlg', 'label':lang._('Edit Error Page')]) }} {{ partial("layout_partials/base_dialog",['fields': tls_fingerprint,'id':'tls_fingerprint_dlg', 'label':lang._('Edit TLS Fingerprint')]) }} {{ partial("layout_partials/base_dialog",['fields': syslog_target,'id':'syslog_target_dlg', 'label':lang._('Edit SYSLOG Target')]) }} diff --git a/www/nginx/src/opnsense/scripts/nginx/setup.php b/www/nginx/src/opnsense/scripts/nginx/setup.php index 4860fbcd0..202db2650 100755 --- a/www/nginx/src/opnsense/scripts/nginx/setup.php +++ b/www/nginx/src/opnsense/scripts/nginx/setup.php @@ -239,11 +239,66 @@ foreach ($nginx->userlist->iterateItems() as $user_list) { unset($file); } } + // create directories for cache foreach ($nginx->cache_path->iterateItems() as $cache_path) { @mkdir((string)$cache_path->path, 0755, true); } +// create custom error pages +const ERRORPAGE_DIR = '/usr/local/etc/nginx/views'; +@mkdir(ERRORPAGE_DIR, 0755, true); +$used_errorpages = array(); +// search used error pages in http servers and locations +foreach (array($nginx->http_server, $nginx->location) as $entity) { + foreach ($entity->iterateItems() as $element) { + $pages = explode(',', $element->errorpages); + foreach ($pages as $page) { + $page = str_replace('-', '', $page); + if (!in_array($page, $used_errorpages)) { + $used_errorpages[] = $page; + } + } + } +} +// search used WAF error pages +foreach ($nginx->location->iterateItems() as $location) { + if ($location->secrules_errorpage != '') { + $page = str_replace('-', '', $location->secrules_errorpage); + if (!in_array($page, $used_errorpages)) { + $used_errorpages[] = $page; + } + } +} +// create/update error pages +foreach ($nginx->errorpage->iterateItems() as $errorpage) { + $uuid = str_replace('-', '', $errorpage->getAttributes()['uuid']); + if (in_array($uuid, $used_errorpages)) { + $filename = "error_$uuid.html"; + $content = base64_decode((string)$errorpage->pagecontent); + // Does error page have a content? + if (strlen($content) > 0) { + $fs_hash = @hash_file("sha1", ERRORPAGE_DIR . "/$filename"); + if ($fs_hash !== hash("sha1", $content)) { + @file_put_contents(ERRORPAGE_DIR . "/$filename", $content); + } + chmod(ERRORPAGE_DIR . "/$filename", 0644); + } + else { + unset($used_errorpages[array_search($uuid, $used_errorpages)]); + } + } +} +// delete unused (old) error pages +$dir = new \DirectoryIterator(ERRORPAGE_DIR); +foreach ($dir as $file) { + if ($file->isFile() && strpos($file->getFilename(), 'error_') === 0) { + if (!in_array(substr($file->getFilename(), 6, 32), $used_errorpages)) { + @unlink($file->getPathname()); + } + } +} + // export TLS fingerprint database for MitM detection $tls_fingerprint_database = array(); foreach ($nginx->tls_fingerprint->iterateItems() as $tls_fingerprint) { 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 bcd597f13..49aca5f88 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/http.conf @@ -171,8 +171,38 @@ server { satisfy {{ server.satisfy }}; {% endif %} #include tls.conf; +{% set used_errorpages = [] %} +{% if server.errorpages is defined and server.errorpages != '' %} +{% for errorpage_uuid in server.errorpages.split(',') %} +{% do used_errorpages.append(errorpage_uuid) %} +{% set errorpage = helpers.getUUID(errorpage_uuid) %} + error_page {{ errorpage.statuscodes.replace(',', ' ') }} {% if errorpage.response is defined and errorpage.response != '' %}={{ errorpage.response }} {% endif %}{% if errorpage.redirect is defined and errorpage.redirect != '' %}{{ errorpage.redirect }}{% else %}/error_{{ errorpage_uuid.replace('-', '') }}.html{% endif %}; +{% if errorpage.redirect is not defined or errorpage.redirect == '' %} + location = /error_{{ errorpage_uuid.replace('-', '') }}.html { + internal; + root /usr/local/etc/nginx/views; + } +{% endif %} +{% endfor %} +{% else %} + error_page 403 /opnsense_error_403.html; error_page 404 /opnsense_error_404.html; + error_page 405 /waf_denied.html; error_page 500 501 502 503 504 /opnsense_server_error.html; + + location = /opnsense_error_403.html { + internal; + root /usr/local/etc/nginx/views; + } + location = /opnsense_error_404.html { + internal; + root /usr/local/etc/nginx/views; + } + location = /opnsense_server_error.html { + internal; + root /usr/local/etc/nginx/views; + } +{% endif %} {% if server.security_header is defined and server.security_header != '' %} {% set security_rule = helpers.getUUID(server.security_header) %} {% if security_rule is defined %} @@ -193,14 +223,6 @@ server { return 403 "You got banned permanently from this server."; } error_page 418 = @permanentban; - location = /opnsense_server_error.html { - internal; - root /usr/local/etc/nginx/views; - } - location = /opnsense_error_404.html { - internal; - root /usr/local/etc/nginx/views; - } location = /waf_denied.html { root /usr/local/etc/nginx/views; access_log /var/log/nginx/waf_denied.access.log main; @@ -292,11 +314,38 @@ server { {% endif %} {% if server.locations is defined %} +{% set location_errorpages = [] %} {% 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 %} +{% if location.urlpattern is defined %} +{% include "OPNsense/Nginx/location.conf" ignore missing with context %} +{# Find used error pages in secrules_errorpage #} +{% if location.secrules_errorpage is defined and location.secrules_errorpage != '' %} +{% if location.secrules_errorpage not in location_errorpages %} +{% do location_errorpages.append(location.secrules_errorpage) %} +{% endif %} {% endif %} +{# Find custom error pages used in locations #} +{% if location.errorpages is defined and location.errorpages != '' %} +{% for errorpage_uuid in location.errorpages.split(',') %} +{% if errorpage_uuid not in location_errorpages %} +{% do location_errorpages.append(errorpage_uuid) %} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} +{% endfor %} +{# Error pages used in locations #} +{% for errorpage_uuid in location_errorpages %} +{% if errorpage_uuid not in used_errorpages %} +{% set errorpage = helpers.getUUID(errorpage_uuid) %} +{% if errorpage.redirect is not defined or errorpage.redirect == '' %} +location = /error_{{ errorpage_uuid.replace('-', '') }}.html { + internal; + root /usr/local/etc/nginx/views; +} +{% endif %} +{% endif %} {% endfor %} {% endif %} 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 5df147b2b..254055642 100644 --- a/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf +++ b/www/nginx/src/opnsense/service/templates/OPNsense/Nginx/location.conf @@ -36,7 +36,17 @@ location {{ location.matchtype }} {{ location.urlpattern }} { LibInjectionSql; CheckRule "$LIBINJECTION_SQL >= {{ location.sqli_block_score }}" BLOCK; {% endif %} +{% if location.secrules_errorpage is defined and location.secrules_errorpage != '' %} + DeniedUrl "/error_{{ location.secrules_errorpage.replace('-', '') }}.html"; +{% else %} DeniedUrl "/waf_denied.html"; +{% endif %} +{% if location.errorpages is defined and location.errorpages != '' %} +{% for errorpage_uuid in location.errorpages.split(',') %} +{% set errorpage = helpers.getUUID(errorpage_uuid) %} + error_page {{ errorpage.statuscodes.replace(',', ' ') }} {% if errorpage.response is defined and errorpage.response != '' %}={{ errorpage.response }} {% endif %}{% if errorpage.redirect is defined and errorpage.redirect != '' %}{{ errorpage.redirect }}{% else %}/error_{{ errorpage_uuid.replace('-', '') }}.html{% endif %}; +{% endfor %} +{% endif %} {% if location.force_https is defined and location.force_https == '1' %} if ($scheme != "https") { return 302 https://$host$request_uri; @@ -210,3 +220,4 @@ location {{ location.matchtype }} {{ location.urlpattern }} { {% endif %}{# honeypot #} include {{ location['@uuid'] }}_post/*.conf; } + diff --git a/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js b/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js index 26a213660..c6a86734e 100644 --- a/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js +++ b/www/nginx/src/opnsense/www/js/nginx/dist/configuration.min.js @@ -1 +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=27)}({27: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.data("data",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 a=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")}}),l=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=$("#snihostname\\.data").data("data");_.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=$("#ipacl\\.data").data("data");_.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("/api/nginx/service/reconfigure",{},function(){$(".reloadAct_progress").removeClass("fa-spin")})}),$('[id*="save_"]').each(function(){$(this).click(function(){let e=$(this).closest("form").attr("id"),t=$(this).closest("form").attr("data-title");saveFormToEndpoint("/api/nginx/settings/set",e,function(){$("#"+e+"_progress").addClass("fa fa-spinner fa-pulse"),ajaxCall("/api/nginx/service/reconfigure",{},function(i,n){$("#"+e+"_progress").removeClass("fa fa-spinner fa-pulse"),void 0===i||"success"===n&&"ok"===i.status?updateServiceControlUI("nginx"):BootstrapDialog.show({type:BootstrapDialog.TYPE_WARNING,title:t,message:JSON.stringify(i),draggable:!0})})})})}),["upstream","upstreamserver","location","credential","userlist","httpserver","streamserver","httprewrite","custompolicy","security_header","ipacl","limit_zone","cache_path","limit_request_connection","snifwd","tls_fingerprint","syslog_target","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:a,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:l,collection:new d,createModel:function(){return new c({network:"::",action:"deny"})}});window.ipaclfield=e,e.render()})}}); \ No newline at end of file +!function(e){var t={};function n(s){if(t[s])return t[s].exports;var i=t[s]={i:s,l:!1,exports:{}};return e[s].call(i.exports,i,i.exports,n),i.l=!0,i.exports}n.m=e,n.c=t,n.d=function(e,t,s){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:s})},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 s=Object.create(null);if(n.r(s),Object.defineProperty(s,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var i in e)n.d(s,i,function(t){return e[t]}.bind(null,i));return s},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=27)}({27:function(e,t,n){"use strict";n.r(t);var s=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.data("data",this.collection.toJSON())},addEntry:function(e){e.preventDefault(),this.collection.add(this.createModel())}});var i=Backbone.Collection.extend({url:"/api/nginx/settings/searchupstream",parse:function(e){return e.rows}});const a=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")}}),l=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=$("#snihostname\\.data").data("data");_.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=$("#ipacl\\.data").data("data");_.isArray(e)||(e=[]),this.reset(e)}});const u=new i,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("/api/nginx/service/reconfigure",{},function(){$(".reloadAct_progress").removeClass("fa-spin")})}),$('[id*="save_"]').each(function(){$(this).click(function(){let e=$(this).closest("form").attr("id"),t=$(this).closest("form").attr("data-title");saveFormToEndpoint("/api/nginx/settings/set",e,function(){$("#"+e+"_progress").addClass("fa fa-spinner fa-pulse"),ajaxCall("/api/nginx/service/reconfigure",{},function(n,s){$("#"+e+"_progress").removeClass("fa fa-spinner fa-pulse"),void 0===n||"success"===s&&"ok"===n.status?updateServiceControlUI("nginx"):BootstrapDialog.show({type:BootstrapDialog.TYPE_WARNING,title:t,message:JSON.stringify(n),draggable:!0})})})})}),["upstream","upstreamserver","location","credential","userlist","httpserver","streamserver","httprewrite","custompolicy","security_header","ipacl","limit_zone","cache_path","limit_request_connection","snifwd","errorpage","tls_fingerprint","syslog_target","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,formatters:{commands:function(e,t){return''},response:function(e,t){return"none"==t.response?"unchanged":t.response},statuscodes:function(e,t){const n=[],s=t.statuscodes.split(",");for(let e of s)n.push(e.substr(0,3));return n.join(", ")}}}})}),bind_naxsi_rule_dl_button(),function(){let e=new s({dataField:document.getElementById("snihostname.data"),upstreamCollection:u,entryclass:a,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 s({dataField:document.getElementById("ipacl.data"),upstreamCollection:h,entryclass:l,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/src/nginx_config.js b/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js index 4cd780377..0e69aaf17 100644 --- a/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js +++ b/www/nginx/src/opnsense/www/js/nginx/src/nginx_config.js @@ -68,6 +68,7 @@ function init_grids() { 'cache_path', 'limit_request_connection', 'snifwd', + 'errorpage', 'tls_fingerprint', 'syslog_target', 'naxsirule'].forEach(function (element) { @@ -78,7 +79,29 @@ function init_grids() { 'set': '/api/nginx/settings/set' + element + '/', 'add': '/api/nginx/settings/add' + element + '/', 'del': '/api/nginx/settings/del' + element + '/', - 'options': {selection: false, multiSelect: false} + 'options': { + selection: false, + multiSelect: false, + formatters: { + "commands": function (column, row) { + return " " + + "" + + ""; + }, + "response": function (column, row) { + return ((row.response == "none") ? "unchanged" : row.response); + }, + // Extract 3 digit HTTP status code from string with human readable text (302 Found -> 302) + "statuscodes": function (column, row) { + const result = []; + const elems = row.statuscodes.split(","); + for (let elem of elems) { + result.push(elem.substr(0, 3)); + } + return result.join(", "); + } + } + } } ); });