diff --git a/applications/luci-app-adguardhome/Makefile b/applications/luci-app-adguardhome/Makefile new file mode 100644 index 000000000000..4e50a328a8a4 --- /dev/null +++ b/applications/luci-app-adguardhome/Makefile @@ -0,0 +1,14 @@ +# Copyright 2022- Duncan Hill +# This is free software, licensed under the Apache License, Version 2.0 + +include $(TOPDIR)/rules.mk + +LUCI_TITLE:=LuCI support for AdguardHome +LUCI_DEPENDS:=+adguardhome +luci-lib-jsonc +luasocket +lyaml +LUCI_PKGARCH:=all + +PKG_LICENSE:=Apache-2.0 + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-adguardhome/README.md b/applications/luci-app-adguardhome/README.md new file mode 100644 index 000000000000..1475b52772dd --- /dev/null +++ b/applications/luci-app-adguardhome/README.md @@ -0,0 +1,36 @@ +# LuCI App AdGuardHome + +This LuCI app provides basic integration with the [AdGuard Home](https://github.com/AdguardTeam/AdGuardHome) [package](https://openwrt.org/packages/pkgdata/adguardhome) for OpenWrt. Note that the AdGuard Home package installation and configuration requires interaction with the OpenWrt command line; this app does not remove any of that interaction. + +See also: [AdGuard Home @ AdGuard](https://adguard.com/en/adguard-home/overview.html) + +## Using/installing this app + +First, install the AdGuard Home package - either via the web UI for software package management, or +``` +opkg install adguardhome +``` + +Follow the [installation instructions](https://openwrt.org/docs/guide-user/services/dns/adguard-home) for AdGuard Home, and make a note of the username and password for authenticating to the web UI. + +Next, install this package - either via the web UI for software package management, or +``` +opkg install luci-app-adguardhome +``` + +This package is unable to automatically determine the username and password (the password is encrypted in AdGuard Home's configuration file), so you'll need to go to `Services > AdGuard Home > Configuration` and provide these credentials. The credentials will be stored **unencrypted** in `/etc/config/adguardhome`. + +With the credentials saved, the `Services > AdGuard Home > Status` page should now work, and show you the general status of AdGuard Home. + +If you go to `Services > AdGuard Home > Logs`, you can see the last 50 log lines from both the supporting script used by this package, and the AdGuard Home software. + +This app provides a link to the AdGuard Home web UI, making it easy to see more detailed statistics, and the query log. + +## Dependencies + +Dependencies are declared in the Makefile, but are + +* adguardhome, as this app is useless without it +* luasocket, for talking to the AdGuard Home REST API +* luci-lib-jsonc, for doing JSON encoding +* lyaml, for parsing the AdGuard home YAML configuration \ No newline at end of file diff --git a/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js new file mode 100644 index 000000000000..02f888161f04 --- /dev/null +++ b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js @@ -0,0 +1,41 @@ +'use strict'; +'require form'; +'require view'; + +return view.extend({ + render: function () { + // A basic configuration form; the luci.adguardhome script that + // powers the other UI pages needs a username and password to + // communicate with the AdguardHome REST API. + var s, o; + var m = new form.Map( + 'adguardhome', + _('AdGuard Home Configuration'), + _('This application requires the username and password that were configured when you set up AdGuard Home, ' + + 'as the REST API for AdGuard Home is password protected. The password cannot be read from the YAML ' + + 'configuration file for AdGuard Home, as it is encrypted in that store. The credentials supplied here will ' + + 'be stored unencrypted in /etc/config/adguardhome on your device.') + ); + s = m.section( + form.TypedSection, + 'adguardhome', + _('General settings'), + ); + s.anonymous = true; + o = s.option( + form.Value, + 'web_username', + _('Username for AdGuard Home'), + _('The username you configured when you set up AdGuard Home') + ); + o.placeholder = 'adguard'; + o = s.option( + form.Value, + 'web_password', + _('Password for AdGuard Home'), + _('The password you configured when you set up AdGuard Home') + ); + o.password = true; + return m.render(); + } +}) \ No newline at end of file diff --git a/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js new file mode 100644 index 000000000000..194501a8aca2 --- /dev/null +++ b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js @@ -0,0 +1,44 @@ +'use strict'; +'require dom'; +'require fs'; +'require poll'; +'require ui'; +'require view'; + +return view.extend({ + load: function() { + return Promise.all([ + L.resolveDefault(fs.stat('/sbin/logread'), null), + L.resolveDefault(fs.stat('/usr/sbin/logread'), null) + ]).then(function(stat) { + var logger = stat[0] ? stat[0].path : stat[1] ? stat[1].path : null; + + return fs.exec_direct(logger, [ '-e', 'adguardhome' ]).catch(function(err) { + ui.addNotification(null, E('p', {}, _('Unable to load log data: ' + err.message))); + return ''; + }); + }); + }, + + render: function(logdata) { + var loglines = logdata.trim().split(/\n/).reverse().slice(0, 50); + + return E([], [ + E('h2', {}, [_('System Log (AdGuard Home)')]), + E('div', {}, [_('Showing last 50 lines')]), + E('div', { 'id': 'content_syslog' }, [ + E('textarea', { + 'id': 'syslog', + 'style': 'font-size:12px', + 'readonly': 'readonly', + 'wrap': 'off', + 'rows': loglines.length + 1 + }, [ loglines.join('\n') ]) + ]) + ]); + }, + + handleSave: null, + handleSaveApply: null, + handleReset: null +}); diff --git a/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js new file mode 100644 index 000000000000..8cf1a16ebc6a --- /dev/null +++ b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js @@ -0,0 +1,167 @@ +'use strict'; +'require rpc'; +'require view'; + + +return view.extend({ + load_adguardhome_config: rpc.declare({ + object: 'luci.adguardhome', + method: 'get_config' + }), + load_adguardhome_status: rpc.declare({ + object: 'luci.adguardhome', + method: 'get_status' + }), + load_adguardhome_statistics: rpc.declare({ + object: 'luci.adguardhome', + method: 'get_statistics' + }), + + generic_failure: function(message) { + return E('div', {'class': 'error'}, [_('RPC call failure: '), message]) + }, + urlmaker: function (host, port, tls_flag) { + var proto = tls_flag ? 'https://' : 'http://'; + return proto + host + ':' + port + '/'; + }, + render_status_table: function (status, agh_config) { + if (status.error) { + return this.generic_failure(status.error) + } + // Take a hint from the base LuCI module for the Overview page, + // declare the fields and use a loop to build the tabular status view. + // Written out as key/value pairs, but it's just an iterable of elements. + const weburl = this.urlmaker(agh_config.bind_host, status.http_port, agh_config.tls.enabled); + const listen_addresses = L.isObject(status.dns_addresses) ? status.dns_addresses.join(', ') : _('Not found'); + const bootstrap_dns = L.isObject(agh_config.dns.bootstrap_dns) ? agh_config.dns.bootstrap_dns.join(', ') : _('Not found'); + const upstream_dns = L.isObject(agh_config.dns.upstream_dns) ? agh_config.dns.upstream_dns.join(', ') : _('Not found'); + const fields = [ + _('Running'), status.running ? _('Yes') : _('No'), + _('Protection enabled'), status.protection_enabled ? _('Yes') : _('No'), + _('Statistics period (days)'), agh_config.dns.statistics_interval, + _('Web interface'), E('a', { 'href': weburl, 'target': '_blank' }, status.http_port), + _('DNS listen port'), status.dns_port, + _('DNS listen addresses'), listen_addresses, + _('Bootstrap DNS addresses'), bootstrap_dns, + _('Upstream DNS addresses'), upstream_dns, + _('Version'), status.version, + ]; + + var table = E('table', { 'class': 'table', 'id': 'status' }); + for (var i = 0; i < fields.length; i += 2) { + table.appendChild(E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left', 'width': '33%' }, [fields[i]]), + E('td', { 'class': 'td left' }, [(fields[i + 1] != null) ? fields[i + 1] : _('Not found')]) + ])); + } + return table; + }, + render_statistics_table: function (statistics) { + // High level statistics + if (statistics.error) { + return this.generic_failure(statistics.error) + } + const fields = [ + _('DNS queries'), statistics.num_dns_queries, + _('DNS blocks'), statistics.num_blocked_filtering, + _('DNS replacements (safesearch)'), statistics.num_replaced_safesearch, + _('DNS replacements (malware/phishing)'), statistics.num_replaced_safebrowsing, + _('Average processing time (seconds)'), statistics.avg_processing_time, + ]; + + var table = E('table', { 'class': 'table', 'id': 'statistics' }); + for (var i = 0; i < fields.length; i += 2) { + table.appendChild( + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left', 'width': '33%' }, [fields[i]]), + E('td', { 'class': 'td left' }, [(fields[i + 1] != null) ? fields[i + 1] : _('Not found')]) + ])); + } + return table; + }, + render_top_table: function(table_id, objects) { + var table = E('table', { 'class': 'table', 'id': table_id }); + for (i = 0; i < objects.length; i++) { + table.appendChild( + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left', 'width': '33%' }, Object.keys(objects[i])[0]), + E('td', { 'class': 'td left' }, Object.values(objects[i])[0]) + ]) + ); + } + return table; + }, + render_top_queries_table: function (statistics) { + // Top 5 queried domains table view + if (statistics.error) { + return this.generic_failure(statistics.error) + } + const top_queries = statistics.top_queried_domains.slice(0, 5); + return this.render_top_table('top_queries', top_queries) + }, + render_top_blocked_table: function (statistics) { + // Top 5 blocked domains table view + if (statistics.error) { + return this.generic_failure(statistics.error) + } + const top_blocked = statistics.top_blocked_domains.slice(0, 5); + return this.render_top_table('top_blocked', top_blocked) + }, + // Core LuCI functions from here on. + load: function () { + return Promise.all([ + this.load_adguardhome_status(), + this.load_adguardhome_statistics(), + this.load_adguardhome_config() + ]); + }, + render: function (data) { + // data[0] should be load_adguardhome_status() result + var status = data[0] || {}; + // data[1] should be load_adguardhome_statistics() result + var statistics = data[1] || {}; + // data[2] should be load_adguardhome_config() result + var agh_config = data[2] || {}; + + // status.auth_error is only filled in when the config fetch failed + // to get the credentials. That stops all activity, since the user must + // first configure the username and password. Don't even bother trying + // to make REST API calls without credentials. + if (status.auth_error) { + return E('div', { 'class': 'cbi-map', 'id': 'map' }, [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'left' }, [ + E('h3', _('AdGuard Home Status - Error')), + E('div', { 'class': 'error' }, status.auth_error), + E('div', { 'class': 'info' }, _('Please open the Configuration section, and provide the credentials.')) + ]) + ]), + ]); + } + + // Render all the status tables as one big block. + return E('div', { 'class': 'cbi-map', 'id': 'map' }, [ + E('div', { 'class': 'cbi-section' }, [ + E('div', { 'class': 'left' }, [ + E('h3', _('AdGuard Home Status')), + this.render_status_table(status, agh_config) + ]), + E('div', { 'class': 'left' }, [ + E('h3', _('AdGuard Home Statistics')), + this.render_statistics_table(statistics) + ]), + E('div', { 'class': 'left' }, [ + E('h3', _('Top Queried Domains')), + this.render_top_queries_table(statistics) + ]), + E('div', { 'class': 'left' }, [ + E('h3', _('Top Blocked Domains')), + this.render_top_blocked_table(statistics) + ]) + ]), + ]); + }, + handleSave: null, + handleSaveApply: null, + handleReset: null +}) diff --git a/applications/luci-app-adguardhome/po/templates/adguardhome.pot b/applications/luci-app-adguardhome/po/templates/adguardhome.pot new file mode 100644 index 000000000000..b30f4f8739aa --- /dev/null +++ b/applications/luci-app-adguardhome/po/templates/adguardhome.pot @@ -0,0 +1,169 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:3 +msgid "AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:13 +msgid "AdGuard Home Configuration" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:150 +msgid "AdGuard Home Statistics" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:146 +msgid "AdGuard Home Status" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:134 +msgid "AdGuard Home Status - Error" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:69 +msgid "Average processing time (seconds)" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:45 +msgid "Bootstrap DNS addresses" +msgstr "" + +#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:33 +msgid "Configuration" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:66 +msgid "DNS blocks" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:44 +msgid "DNS listen addresses" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:43 +msgid "DNS listen port" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:65 +msgid "DNS queries" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:68 +msgid "DNS replacements (malware/phishing)" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:67 +msgid "DNS replacements (safesearch)" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:22 +msgid "General settings" +msgstr "" + +#: applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json:3 +msgid "Grant permissions for the AdGuard Home LuCI app" +msgstr "" + +#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:25 +msgid "Logs" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:39 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:40 +msgid "No" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:35 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:36 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:37 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:54 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:77 +msgid "Not found" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:35 +msgid "Password for AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:136 +msgid "Please open the Configuration section, and provide the credentials." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:40 +msgid "Protection enabled" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:21 +msgid "RPC call failure:" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:39 +msgid "Running" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js:27 +msgid "Showing last 50 lines" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:41 +msgid "Statistics period (days)" +msgstr "" + +#: applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json:17 +msgid "Status" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js:26 +msgid "System Log (AdGuard Home)" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:36 +msgid "The password you configured when you set up AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:29 +msgid "The username you configured when you set up AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:14 +msgid "" +"This application requires the username and password that were configured " +"when you set up AdGuard Home, as the REST API for AdGuard Home is password " +"protected. The password cannot be read from the YAML configuration file for " +"AdGuard Home, as it is encrypted in that store. The credentials supplied " +"here will be stored unencrypted in /etc/config/adguardhome on your device." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:158 +msgid "Top Blocked Domains" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:154 +msgid "Top Queried Domains" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js:16 +msgid "Unable to load log data:" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:46 +msgid "Upstream DNS addresses" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:28 +msgid "Username for AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:47 +msgid "Version" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:42 +msgid "Web interface" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:39 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:40 +msgid "Yes" +msgstr "" diff --git a/applications/luci-app-adguardhome/root/usr/libexec/rpcd/luci.adguardhome b/applications/luci-app-adguardhome/root/usr/libexec/rpcd/luci.adguardhome new file mode 100755 index 000000000000..10320534ce8a --- /dev/null +++ b/applications/luci-app-adguardhome/root/usr/libexec/rpcd/luci.adguardhome @@ -0,0 +1,219 @@ +#!/usr/bin/env lua + +local fs = require "nixio.fs" +local http = require "socket.http" +local json = require "luci.jsonc" +local ltn12 = require "ltn12" +local lyaml = require "lyaml" +local mime = require "mime" +local nixio = require "nixio" +local sock_url = require "socket.url" +local UCI = require "luci.model.uci" + +-- Slight overkill, but leaving room to do log_info etcetera. +local function log_to_syslog(level, message) nixio.syslog(level, message) end + +local function log_error(message) + log_to_syslog("err", "[luci.adguardhome]: " .. message) +end + +local function readfile(path) + local s = fs.readfile(path) + return s and (s:gsub("^%s+", ""):gsub("%s+$", "")) +end + +-- Builds a URL very simply. Probably flawed. +local function urlbuilder(tls_enabled, host, port, path) + local proto = nil + if tls_enabled then + proto = 'https' + else + proto = 'http' + end + return proto .. "://" .. host .. ":" .. port .. "/control/" .. path +end + +local function get_credentials() + -- AdGuard Home stores credentials encrypted, so the a secondary + -- copy has to be stored in /etc/config/adguardhome. + local creds = {} + local uci = UCI.cursor() + local username = uci:get("adguardhome", "config", "web_username") + local password = uci:get("adguardhome", "config", "web_password") + uci.unload("adguardhome") + if not username then + local msg = "Username not found in uci adguardhome" + log_error(msg) + print(json.stringify({auth_error = msg})) + os.exit(1) + end + if not password then + local msg = "Password not found in uci adguardhome" + log_error(msg) + print(json.stringify({auth_error = msg})) + os.exit(1) + end + return username, password +end + +local function call_rest_api(api_name) + -- Table to store the result of this function + local r = {} + -- Sink for the resulting JSON from the query to AGH + local resp = {} + + -- Rather than ask for the host and port from the person using this + -- software, just get it directly from adguardhome's config. + local agh_config = readfile("/etc/adguardhome.yaml") + local agh_table = lyaml.load(agh_config) + + -- These have to be passed in, the yaml config uses a salted store + -- (which is good) + local username, password = get_credentials() + + -- Build the URL to chat to for REST API queries + local url = urlbuilder(agh_table.tls.enabled, agh_table.bind_host, + agh_table.bind_port, api_name) + + -- Call the REST API, see what comes back + local _body, code, _headers, _status = http.request { + url = url, + method = "GET", + headers = { + ["Authorization"] = "Basic " .. + (mime.b64(username .. ":" .. sock_url.unescape(password))) + }, + sink = ltn12.sink.table(resp) + } + local result = resp[1] + if code == 200 then + -- Make the shell happy; 0 is good. All else is an error exit code. + code = 0 + else + local msg = "REST call failed with '" .. resp[1] .. "'. Are the credentials correct?" + log_error(msg) + result = json.stringify({error = msg}) + end + r.code = code + r.result = result + return r +end + +local methods = { + -- Converts the AGH YAML configuration into JSON for consumption by + -- the LuCI app. + get_config = { + call = function() + local agh_config = readfile("/etc/adguardhome.yaml") + local agh_table = lyaml.load(agh_config) + local r = {} + r.result = json.stringify(agh_table) + r.code = 200 + return r + end + }, + -- Calls the /control/stat(istic)s REST API, returns the JSON + get_statistics = {call = function() return call_rest_api("stats") end}, + -- Calls the /control/status REST API, returns the JSON + get_status = {call = function() return call_rest_api("status") end} +} + +local function parseInput() + -- Input parsing - the RPC daemon calls the Lua script and + -- sends input to it via stdin, not as an argument on the CLI. + -- Thus, any testing via the lua interpreter needs to be in the form + -- echo '{jsondata}' | lua /usr/libexec/rpcd/script call method_name + local parse = json.new() + local done, err + + while true do + local chunk = io.read(4096) + if not chunk then + break + elseif not done and not err then + done, err = parse:parse(chunk) + end + end + + if not done then + print(json.stringify({ + error = err or "Incomplete input for argument parsing" + })) + os.exit(1) + end + + return parse:get() +end + +local function validateArgs(func, uargs) + -- Validates that arguments picked out by parseInput actually match + -- up to the arguments expected by the function being called. + local method = methods[func] + if not method then + print(json.stringify({error = "Method not found in methods table"})) + os.exit(1) + end + + -- Lua has no length operator for tables, so iterate to get the count + -- of the keys. + local n = 0 + for k, v in pairs(uargs) do n = n + 1 end + + -- If the method defines an args table (so empty tables are not allowed), + -- and there were no args, then give a useful error message about that. + if method.args and n == 0 then + print(json.stringify({ + error = "Received empty arguments for " .. func .. + " but it requires " .. json.stringify(method.args) + })) + os.exit(1) + end + + uargs.ubus_rpc_session = nil + + local margs = method.args or {} + for k, v in pairs(uargs) do + if margs[k] == nil or (v ~= nil and type(v) ~= type(margs[k])) then + print(json.stringify({ + error = "Invalid argument '" .. k .. "' for " .. func .. + " it requires " .. json.stringify(method.args) + })) + os.exit(1) + end + end + + return method +end + +if arg[1] == "list" then + local _, rv = nil, {} + for _, method in pairs(methods) do rv[_] = method.args or {} end + print((json.stringify(rv):gsub(":%[%]", ":{}"))) +elseif arg[1] == "call" then + local args = parseInput() + local method = validateArgs(arg[2], args) + local run = method.call(args) + print(run.result) + os.exit(run.code or 0) +elseif arg[1] == "help" then + local helptext = [[ +Usage: + + To see what methods are exported by this script: + + lua luci.adguardhome list + + To call a method that has no arguments: + + echo '{}' | lua luci.adguardhome call method_name + + To call a method that takes arguments: + + echo '{"valid": "json", "argument": "value"}' | lua luci.adguardhome call method_name + + To call this script via ubus: + + ubus call luci.adguardhome method_name '{"valid": "json", "argument": "value"}' +]] + print(helptext) +end diff --git a/applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json b/applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json new file mode 100644 index 000000000000..23c738ad6287 --- /dev/null +++ b/applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json @@ -0,0 +1,40 @@ +{ + "admin/services/adguardhome": { + "title": "AdGuard Home", + "action": { + "type": "firstchild" + }, + "depends": { + "acl": [ + "luci-app-adguardhome" + ], + "uci": { + "adguardhome": true + } + } + }, + "admin/services/adguardhome/status": { + "title": "Status", + "order": 1, + "action": { + "type": "view", + "path": "adguardhome/status" + } + }, + "admin/services/adguardhome/logs": { + "title": "Logs", + "order": 11, + "action": { + "type": "view", + "path": "adguardhome/logs" + } + }, + "admin/services/adguardhome/config": { + "title": "Configuration", + "order": 21, + "action": { + "type": "view", + "path": "adguardhome/config" + } + } +} \ No newline at end of file diff --git a/applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json b/applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json new file mode 100644 index 000000000000..3ecf8cfc46ef --- /dev/null +++ b/applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json @@ -0,0 +1,30 @@ +{ + "luci-app-adguardhome": { + "description": "Grant permissions for the AdGuard Home LuCI app", + "read": { + "ubus": { + "luci.adguardhome": [ + "get_config", + "get_status", + "get_statistics" + ], + "uci": [ + "get" + ], + "file": { + "/sbin/logread": [ + "exec" + ] + } + }, + "uci": [ + "adguardhome" + ] + }, + "write": { + "uci": [ + "adguardhome" + ] + } + } +} \ No newline at end of file