From: Ayushman Tripathi Date: Tue, 25 Apr 2023 11:25:27 +0000 (+0530) Subject: luci-app-uhttpd: migrate to js X-Git-Url: http://git.lede-project.org./?a=commitdiff_plain;h=refs%2Fpull%2F6359%2Fhead;p=project%2Fluci.git luci-app-uhttpd: migrate to js Signed-off-by: Ayushman Tripathi luci-app-uhttpd: migrate to js spaces to tab Signed-off-by: Ayushman Tripathi luci-app-uhttpd: migrate to js Signed-off-by: Ayushman Tripathi luci-app-uhttpd: migrate to JavaScript-based implementation tab spaces Signed-off-by: Ayushman Tripathi luci-app-uhttpd: migrate to js luci-app-uhttpd: migrate to JavaScript-based implementation formatting Signed-off-by: Ayushman Tripathi luci-app-uhttpd: migrate to js luci-app-uhttpd: migrate to JavaScript-based implementation fix formatting bug luci-app-uhttpd: migrate to js luci-app-uhttpd: migrate to JavaScript-based implementation fix upload bug luci-app-uhttpd: migrate to js luci-app-uhttpd: migrate to js update dependency luci-app-uhttpd: migrate to js --- diff --git a/applications/luci-app-uhttpd/Makefile b/applications/luci-app-uhttpd/Makefile index a365353cc6..d28a48f87d 100644 --- a/applications/luci-app-uhttpd/Makefile +++ b/applications/luci-app-uhttpd/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=uHTTPd Webserver Configuration -LUCI_DEPENDS:=+luci-compat +uhttpd +LUCI_DEPENDS:= +uhttpd LUCI_PKGARCH:=all PKG_LICENSE:=Apache-2.0 diff --git a/applications/luci-app-uhttpd/htdocs/luci-static/resources/view/uhttpd/uhttpd.js b/applications/luci-app-uhttpd/htdocs/luci-static/resources/view/uhttpd/uhttpd.js new file mode 100644 index 0000000000..db659f659b --- /dev/null +++ b/applications/luci-app-uhttpd/htdocs/luci-static/resources/view/uhttpd/uhttpd.js @@ -0,0 +1,258 @@ +'use strict'; +'require view'; +'require form'; +'require fs'; +'require uci'; + +return view.extend({ + load: function () { + return Promise.all([uci.load('uhttpd')]); + }, + render: function () { + var lhttp = null; + var lhttps = null; + var cert_file = null; + var key_file = null; + var ucs = null; + var uhttpdMap = new form.Map('uhttpd', _('uHTTPd'), _('A lightweight single-threaded HTTP(S) server')); + + ucs = uhttpdMap.section(form.TypedSection, 'uhttpd'); + ucs.addremove = true; + ucs.anonymous = false; + + ucs.tab('general', _('General Settings')); + ucs.tab('server', _('Full Web Server Settings'), _('For settings primarily geared to serving more than the web UI')); + ucs.tab('advanced', _('Advanced Settings'), _('Settings which are either rarely needed or which affect serving the WebUI')); + + lhttp = ucs.taboption('general', form.DynamicList, 'listen_http', _('HTTP listeners (address:port)'), _('Bind to specific interface:port (by specifying interface address')); + lhttp.datatype = 'list(ipaddrport(1))'; + + lhttp.validate = function (section_id, value) { + var have_https_listener = false; + var have_http_listener = false; + if (lhttp && lhttp.formvalue(section_id) && lhttp.formvalue(section_id).length > 0) { + lhttp.formvalue(section_id).forEach(function (v) { + if (v && v !== '') { + have_http_listener = true; + return false; + } + }); + } + if (lhttps && lhttps.formvalue(section_id) && lhttps.formvalue(section_id).length > 0) { + lhttps.formvalue(section_id).forEach(function (v) { + if (v && v !== '') { + have_https_listener = true; + return false; + } + }); + } + if (!(have_http_listener || have_https_listener)) { + return [null, 'must listen on at least one address:port']; + } + return true; + }; + + lhttps = ucs.taboption('general', form.DynamicList, 'listen_https', _('HTTPS listener (address:port)'), _('Bind to specific interface:port (by specifying interface address')); + lhttps.datatype = 'list(ipaddrport(1))'; + + var cert = uci.get('uhttpd', 'main', 'cert'); + var key = uci.get('uhttpd', 'main', 'key'); + + lhttps.validate = function (section_id, value) { + let have_https_listener = false; + let have_http_listener = false; + + if (lhttps && lhttps.formvalue(section_id) && lhttps.formvalue(section_id).length > 0) { + lhttps.formvalue(section_id).forEach(function (v) { + if (v && v !== '') { + have_https_listener = true; + return false; + } + }); + if (have_https_listener && (!cert_file || !cert_file.formvalue(section_id) || cert_file.formvalue(section_id) === '')) { + return [null, 'must have certificate when using https']; + } + if (have_https_listener && (!key_file || !key_file.formvalue(section_id) || key_file.formvalue(section_id) === '')) { + return [null, 'must have key when using https']; + } + } + + if (lhttp && lhttp.formvalue(section_id) && lhttp.formvalue(section_id).length > 0) { + lhttp.formvalue(section_id).forEach(function (v) { + if (v && v !== '') { + have_http_listener = true; + return false; + } + }); + } + + if (!(have_http_listener || have_https_listener)) { + return [null, 'must listen on at least one address:port']; + } + + return true; + }; + + lhttps.depends({ cert, key }); + + var httptoHttps = ucs.taboption('general', form.Flag, 'redirect_https', _('Redirect all HTTP to HTTPS')); + httptoHttps.default = httptoHttps.enabled; + httptoHttps.rmempty = false; + + var rfc1918Filter = ucs.taboption('general', form.Flag, 'rfc1918_filter', _('Ignore private IPs on public interface'), _('Prevent access from private (RFC1918) IPs on an interface if it has an public IP address')); + rfc1918Filter.default = rfc1918Filter.enabled; + rfc1918Filter.rmempty = false; + + cert_file = ucs.taboption('general', form.FileUpload, 'cert', _('HTTPS Certificate (DER or PEM format)')); + cert_file.root_directory = '/'; + cert_file.enable_delete = false; + + key_file = ucs.taboption('general', form.FileUpload, 'key', _('HTTPS Private Key (DER or PEM format)')); + key_file.root_directory = '/'; + key_file.enable_delete = false; + + var removeOld = ucs.taboption('general', form.Button, 'remove_old', _('Remove old certificate and key'), _('uHTTPd will generate a new self-signed certificate using the configuration shown below.')); + removeOld.inputstyle = 'remove'; + + removeOld.onclick = function (section_id) { + fs.remove(`${uci.get('uhttpd', 'main', 'cert')}`) + .then(() => fs.remove(`${uci.get('uhttpd', 'main', 'key')}`)) + .then(() => { + return fs.exec('/etc/init.d/uhttpd', ['restart']); + }) + .finally(() => { + window.location.reload(); + }); + }; + + var removeConf = ucs.taboption('general', form.Button, 'remove_conf', _('Remove configuration for certificate and key'), _('This permanently deletes the cert, key, and configuration to use same.')); + removeConf.inputstyle = 'remove'; + removeConf.onclick = function (section_id) { + fs.remove(`${uci.get('uhttpd', 'main', 'cert')}`) + .then(() => fs.remove(`${uci.get('uhttpd', 'main', 'key')}`)) + .then(() => { + uci.unset('uhttpd', 'main', 'cert'); + uci.unset('uhttpd', 'main', 'key'); + uci.unset('uhttpd', 'main', 'listen_https'); + return uci.save(); + }) + .then(() => { + return fs.exec('/etc/init.d/uhttpd', ['restart']); + }) + .finally(() => { + window.location.reload(); + }); + }; + + var indexPage = ucs.taboption('server', form.DynamicList, 'index_page', _('Index page(s)'), _('E.g specify with index.html and index.php when using PHP')); + indexPage.optional = true; + indexPage.placeholder = 'index.html'; + + var interpreter = ucs.taboption('server', form.DynamicList, 'interpreter', _('CGI filetype handler'), _("Interpreter to associate with file endings ('suffix=handler', e.g. '.php=/usr/bin/php-cgi')")); + interpreter.optional = true; + + var noSymlinks = ucs.taboption('server', form.Flag, 'no_symlinks', _('Do not follow symlinks outside document root')); + noSymlinks.optional = true; + + var noDirlists = ucs.taboption('server', form.Flag, 'no_dirlists', _('Do not generate directory listings.')); + noDirlists.default = noDirlists.disabled; + + var alias = ucs.taboption('server', form.DynamicList, 'alias', _('Aliases'), _('(/old/path=/new/path) or (just /old/path which becomes /cgi-prefix/old/path)')); + alias.optional = true; + + var realm = ucs.taboption('server', form.Value, 'realm', _('Realm for Basic Auth')); + realm.optional = true; + realm.placeholder = window.location.hostname || 'OpenWrt'; + + var httpconfig = ucs.taboption('server', form.Value, 'config', _('Config file (e.g. for credentials for Basic Auth)'), _('Will not use HTTP authentication if not present')); + httpconfig.optional = true; + + var errorPage = ucs.taboption('server', form.Value, 'error_page', _('404 Error'), _("Virtual URL or CGI script to display on status '404 Not Found'. Must begin with '/'")); + errorPage.optional = true; + + var docRoot = ucs.taboption('advanced', form.Value, 'home', _('Document root'), _('Base directory for files to be served')); + docRoot.default = '/www'; + docRoot.datatype = 'directory'; + + var cgiPrefix = ucs.taboption('advanced', form.Value, 'cgi_prefix', _('Path prefix for CGI scripts'), _('CGI is disabled if not present.')); + cgiPrefix.optional = true; + + var luaPrefix = ucs.taboption('advanced', form.Value, 'lua_prefix', _('Virtual path prefix for Lua scripts')); + luaPrefix.placeholder = '/lua'; + luaPrefix.optional = true; + + var luaHandler = ucs.taboption('advanced', form.Value, 'lua_handler', _('Full real path to handler for Lua scripts'), _('Embedded Lua interpreter is disabled if not present.')); + luaHandler.optional = true; + + var ubusPrefix = ucs.taboption('advanced', form.Value, 'ubus_prefix', _('Virtual path prefix for ubus via JSON-RPC integration'), _('ubus integration is disabled if not present')); + ubusPrefix.optional = true; + + var ubusSocket = ucs.taboption('advanced', form.Value, 'ubus_socket', _('Override path for ubus socket')); + ubusSocket.optional = true; + + var ubusCors = ucs.taboption('advanced', form.Flag, 'ubus_cors', _('Enable JSON-RPC Cross-Origin Resource Support')); + ubusCors.default = ubusCors.disabled; + ubusCors.optional = true; + + var noUbusauth = ucs.taboption('advanced', form.Flag, 'no_ubusauth', _('Disable JSON-RPC authorization via ubus session API')); + noUbusauth.optional = true; + noUbusauth.default = noUbusauth.disabled; + + var scriptTimeout = ucs.taboption('advanced', form.Value, 'script_timeout', _('Maximum wait time for Lua, CGI, or ubus execution')); + scriptTimeout.placeholder = 60; + scriptTimeout.datatype = 'uinteger'; + scriptTimeout.optional = true; + + var networkTimeout = ucs.taboption('advanced', form.Value, 'network_timeout', _('Maximum wait time for network activity')); + networkTimeout.placeholder = 30; + networkTimeout.datatype = 'uinteger'; + networkTimeout.optional = true; + + var httpKeepalive = ucs.taboption('advanced', form.Value, 'http_keepalive', _('Connection reuse')); + httpKeepalive.placeholder = 20; + httpKeepalive.datatype = 'uinteger'; + httpKeepalive.optional = true; + + var tcpKeepalive = ucs.taboption('advanced', form.Value, 'tcp_keepalive', _('TCP Keepalive')); + tcpKeepalive.optional = true; + tcpKeepalive.datatype = 'uinteger'; + tcpKeepalive.default = 1; + + var maxConnections = ucs.taboption('advanced', form.Value, 'max_connections', _('Maximum number of connections')); + maxConnections.optional = true; + maxConnections.datatype = 'uinteger'; + + var maxRequests = ucs.taboption('advanced', form.Value, 'max_requests', _('Maximum number of script requests')); + maxRequests.optional = true; + maxRequests.datatype = 'uinteger'; + + var certParam = uhttpdMap.section(form.TypedSection, 'cert', _('uHTTPd Self-signed Certificate Parameters')); + + certParam.template = 'cbi/tsection'; + certParam.anonymous = true; + + var days = certParam.option(form.Value, 'days', _('Valid for # of Days')); + days.default = 730; + days.datatype = 'uinteger'; + + var bits = certParam.option(form.Value, 'bits', _('Length of key in bits')); + bits.default = 2048; + bits.datatype = 'min(1024)'; + + var commonname = certParam.option(form.Value, 'commonname', _('Server Hostname'), _('a.k.a CommonName')); + commonname.default = window.location.hostname || 'OpenWrt'; + + var organization = certParam.option(form.Value, 'organization', _('Organization'), _('If empty, a random/unique value is used in cert generation')); + + var location = certParam.option(form.Value, 'location', _('Location')); + location.default = 'Unknown'; + + var state = certParam.option(form.Value, 'state', _('State')); + state.default = 'Unknown'; + + var country = certParam.option(form.Value, 'country', _('Country')); + country.default = 'ZZ'; + + return uhttpdMap.render(); + }, +}); diff --git a/applications/luci-app-uhttpd/luasrc/model/cbi/uhttpd/uhttpd.lua b/applications/luci-app-uhttpd/luasrc/model/cbi/uhttpd/uhttpd.lua deleted file mode 100644 index 940f09873b..0000000000 --- a/applications/luci-app-uhttpd/luasrc/model/cbi/uhttpd/uhttpd.lua +++ /dev/null @@ -1,232 +0,0 @@ --- Copyright 2015 Daniel Dickinson --- Licensed to the public under the Apache License 2.0. - -local fs = require("nixio.fs") - -local m = Map("uhttpd", translate("uHTTPd"), - translate("A lightweight single-threaded HTTP(S) server")) - -local ucs = m:section(TypedSection, "uhttpd", "") -ucs.addremove = true -ucs.anonymous = false - -local lhttp = nil -local lhttps = nil -local cert_file = nil -local key_file = nil - -ucs:tab("general", translate("General Settings")) -ucs:tab("server", translate("Full Web Server Settings"), translate("For settings primarily geared to serving more than the web UI")) -ucs:tab("advanced", translate("Advanced Settings"), translate("Settings which are either rarely needed or which affect serving the WebUI")) - -lhttp = ucs:taboption("general", DynamicList, "listen_http", translate("HTTP listeners (address:port)"), translate("Bind to specific interface:port (by specifying interface address")) -lhttp.datatype = "list(ipaddrport(1))" - -function lhttp.validate(self, value, section) - local have_https_listener = false - local have_http_listener = false - if lhttp and lhttp:formvalue(section) and (#(lhttp:formvalue(section)) > 0) then - for k, v in pairs(lhttp:formvalue(section)) do - if v and (v ~= "") then - have_http_listener = true - break - end - end - end - if lhttps and lhttps:formvalue(section) and (#(lhttps:formvalue(section)) > 0) then - for k, v in pairs(lhttps:formvalue(section)) do - if v and (v ~= "") then - have_https_listener = true - break - end - end - end - if not (have_http_listener or have_https_listener) then - return nil, "must listen on at least one address:port" - end - return DynamicList.validate(self, value, section) -end - -lhttps = ucs:taboption("general", DynamicList, "listen_https", translate("HTTPS listener (address:port)"), translate("Bind to specific interface:port (by specifying interface address")) -lhttps.datatype = "list(ipaddrport(1))" -lhttps:depends("cert") -lhttps:depends("key") - -function lhttps.validate(self, value, section) - local have_https_listener = false - local have_http_listener = false - if lhttps and lhttps:formvalue(section) and (#(lhttps:formvalue(section)) > 0) then - for k, v in pairs(lhttps:formvalue(section)) do - if v and (v ~= "") then - have_https_listener = true - break - end - end - if have_https_listener and ((not cert_file) or (not cert_file:formvalue(section)) or (cert_file:formvalue(section) == "")) then - return nil, "must have certificate when using https" - end - if have_https_listener and ((not key_file) or (not key_file:formvalue(section)) or (key_file:formvalue(section) == "")) then - return nil, "must have key when using https" - end - end - if lhttp and (lhttp:formvalue(section)) and (#lhttp:formvalue(section) > 0) then - for k, v in pairs(lhttp:formvalue(section)) do - if v and (v ~= "") then - have_http_listener = true - break - end - end - end - if not (have_http_listener or have_https_listener) then - return nil, "must listen on at least one address:port" - end - return DynamicList.validate(self, value, section) -end - -o = ucs:taboption("general", Flag, "redirect_https", translate("Redirect all HTTP to HTTPS")) -o.default = o.enabled -o.rmempty = false - -o = ucs:taboption("general", Flag, "rfc1918_filter", translate("Ignore private IPs on public interface"), translate("Prevent access from private (RFC1918) IPs on an interface if it has an public IP address")) -o.default = o.enabled -o.rmempty = false - -cert_file = ucs:taboption("general", FileUpload, "cert", translate("HTTPS Certificate (DER or PEM format)")) - -key_file = ucs:taboption("general", FileUpload, "key", translate("HTTPS Private Key (DER or PEM format)")) - -o = ucs:taboption("general", Button, "remove_old", translate("Remove old certificate and key"), - translate("uHTTPd will generate a new self-signed certificate using the configuration shown below.")) -o.inputstyle = "remove" - -function o.write(self, section) - if cert_file:cfgvalue(section) and fs.access(cert_file:cfgvalue(section)) then fs.unlink(cert_file:cfgvalue(section)) end - if key_file:cfgvalue(section) and fs.access(key_file:cfgvalue(section)) then fs.unlink(key_file:cfgvalue(section)) end - luci.sys.call("/etc/init.d/uhttpd restart") - luci.http.redirect(luci.dispatcher.build_url("admin", "services", "uhttpd")) -end - -o = ucs:taboption("general", Button, "remove_conf", translate("Remove configuration for certificate and key"), - translate("This permanently deletes the cert, key, and configuration to use same.")) -o.inputstyle = "remove" - -function o.write(self, section) - if cert_file:cfgvalue(section) and fs.access(cert_file:cfgvalue(section)) then fs.unlink(cert_file:cfgvalue(section)) end - if key_file:cfgvalue(section) and fs.access(key_file:cfgvalue(section)) then fs.unlink(key_file:cfgvalue(section)) end - self.map:del(section, "cert") - self.map:del(section, "key") - self.map:del(section, "listen_https") - luci.http.redirect(luci.dispatcher.build_url("admin", "services", "uhttpd")) -end - -o = ucs:taboption("server", DynamicList, "index_page", translate("Index page(s)"), translate("E.g specify with index.html and index.php when using PHP")) -o.optional = true -o.placeholder = "index.html" - -o = ucs:taboption("server", DynamicList, "interpreter", translate("CGI filetype handler"), translate("Interpreter to associate with file endings ('suffix=handler', e.g. '.php=/usr/bin/php-cgi')")) -o.optional = true - -o = ucs:taboption("server", Flag, "no_symlinks", translate("Do not follow symlinks outside document root")) -o.optional = true - -o = ucs:taboption("server", Flag, "no_dirlists", translate("Do not generate directory listings.")) -o.default = o.disabled - -o = ucs:taboption("server", DynamicList, "alias", translate("Aliases"), translate("(/old/path=/new/path) or (just /old/path which becomes /cgi-prefix/old/path)")) -o.optional = true - -o = ucs:taboption("server", Value, "realm", translate("Realm for Basic Auth")) -o.optional = true -o.placeholder = luci.sys.hostname() or "OpenWrt" - -local httpconfig = ucs:taboption("server", Value, "config", translate("Config file (e.g. for credentials for Basic Auth)"), translate("Will not use HTTP authentication if not present")) -httpconfig.optional = true - -o = ucs:taboption("server", Value, "error_page", translate("404 Error"), translate("Virtual URL or CGI script to display on status '404 Not Found'. Must begin with '/'")) -o.optional = true - -o = ucs:taboption("advanced", Value, "home", translate("Document root"), - translate("Base directory for files to be served")) -o.default = "/www" -o.datatype = "directory" - -o = ucs:taboption("advanced", Value, "cgi_prefix", translate("Path prefix for CGI scripts"), translate("CGI is disabled if not present.")) -o.optional = true - -o = ucs:taboption("advanced", Value, "lua_prefix", translate("Virtual path prefix for Lua scripts")) -o.placeholder = "/lua" -o.optional = true - -o = ucs:taboption("advanced", Value, "lua_handler", translate("Full real path to handler for Lua scripts"), translate("Embedded Lua interpreter is disabled if not present.")) -o.optional = true - -o = ucs:taboption("advanced", Value, "ubus_prefix", translate("Virtual path prefix for ubus via JSON-RPC integration"), translate("ubus integration is disabled if not present")) -o.optional = true - -o = ucs:taboption("advanced", Value, "ubus_socket", translate("Override path for ubus socket")) -o.optional = true - -o = ucs:taboption("advanced", Flag, "ubus_cors", translate("Enable JSON-RPC Cross-Origin Resource Support")) -o.default = o.disabled -o.optional = true - -o = ucs:taboption("advanced", Flag, "no_ubusauth", translate("Disable JSON-RPC authorization via ubus session API")) -o.optional= true -o.default = o.disabled - -o = ucs:taboption("advanced", Value, "script_timeout", translate("Maximum wait time for Lua, CGI, or ubus execution")) -o.placeholder = 60 -o.datatype = "uinteger" -o.optional = true - -o = ucs:taboption("advanced", Value, "network_timeout", translate("Maximum wait time for network activity")) -o.placeholder = 30 -o.datatype = "uinteger" -o.optional = true - -o = ucs:taboption("advanced", Value, "http_keepalive", translate("Connection reuse")) -o.placeholder = 20 -o.datatype = "uinteger" -o.optional = true - -o = ucs:taboption("advanced", Value, "tcp_keepalive", translate("TCP Keepalive")) -o.optional = true -o.datatype = "uinteger" -o.default = 1 - -o = ucs:taboption("advanced", Value, "max_connections", translate("Maximum number of connections")) -o.optional = true -o.datatype = "uinteger" - -o = ucs:taboption("advanced", Value, "max_requests", translate("Maximum number of script requests")) -o.optional = true -o.datatype = "uinteger" - -local s = m:section(TypedSection, "cert", translate("uHTTPd Self-signed Certificate Parameters")) - -s.template = "cbi/tsection" -s.anonymous = true - -o = s:option(Value, "days", translate("Valid for # of Days")) -o.default = 730 -o.datatype = "uinteger" - -o = s:option(Value, "bits", translate("Length of key in bits")) -o.default = 2048 -o.datatype = "min(1024)" - -o = s:option(Value, "commonname", translate("Server Hostname"), translate("a.k.a CommonName")) -o.default = luci.sys.hostname() - -o = s:option(Value, "organization", translate("Organization"), translate("If empty, a random/unique value is used in cert generation")) - -o = s:option(Value, "location", translate("Location")) -o.default = "Unknown" - -o = s:option(Value, "state", translate("State")) -o.default = "Unknown" - -o = s:option(Value, "country", translate("Country")) -o.default = "ZZ" - -return m diff --git a/applications/luci-app-uhttpd/root/usr/share/luci/menu.d/luci-app-uhttpd.json b/applications/luci-app-uhttpd/root/usr/share/luci/menu.d/luci-app-uhttpd.json index db3be65088..03a97cb2e7 100644 --- a/applications/luci-app-uhttpd/root/usr/share/luci/menu.d/luci-app-uhttpd.json +++ b/applications/luci-app-uhttpd/root/usr/share/luci/menu.d/luci-app-uhttpd.json @@ -1,14 +1,13 @@ { - "admin/services/uhttpd/*": { + "admin/services/uhttpd": { "title": "uHTTPd", "action": { - "type": "cbi", - "path": "uhttpd/uhttpd", - "post": { "cbi.submit": true } + "type": "view", + "path": "uhttpd" }, "depends": { "acl": [ "luci-app-uhttpd" ], "uci": { "uhttpd": true } } } -} +} \ No newline at end of file diff --git a/applications/luci-app-uhttpd/root/usr/share/rpcd/acl.d/luci-app-uhttpd.json b/applications/luci-app-uhttpd/root/usr/share/rpcd/acl.d/luci-app-uhttpd.json index d3b93523cc..ab946db77a 100644 --- a/applications/luci-app-uhttpd/root/usr/share/rpcd/acl.d/luci-app-uhttpd.json +++ b/applications/luci-app-uhttpd/root/usr/share/rpcd/acl.d/luci-app-uhttpd.json @@ -2,10 +2,17 @@ "luci-app-uhttpd": { "description": "Grant UCI access for luci-app-uhttpd", "read": { - "uci": [ "uhttpd" ] + "uci": [ "uhttpd" ], + "file": { + "/*": ["read"] + } }, "write": { - "uci": [ "uhttpd" ] + "uci": [ "uhttpd" ], + "file": { + "/*": ["write"], + "/etc/init.d/uhttpd restart": ["exec"] + } } } }