From a9ff41e6a2cbc1f32ac49bf9722e70bee40b4ebe Mon Sep 17 00:00:00 2001 From: Duncan Hill Date: Sun, 26 Jun 2022 07:41:13 +0100 Subject: [PATCH] Summary: There is no integration into the web UI for AdGuard Home. This PR adds * A Lua script for rpcd to interface with the AGH REST API * ACL controls allowing JS code to call rpcd, uci, logread * Menu entries to put AdGuard Home under Services * Three UIs - status, logs, config The Lua script supports three API calls: * get_status - maps to /control/status in the AGH REST API * get_statistics - maps to /control/stats in the AGH REST API * get_config - converts /etc/adguardhome.yaml to JSON Authentication details must be provided by the user, as the password in the YAML file is encrypted. This results in the AGH password being stored cleartext in /etc/config/adguardhome. The Lua script will log if it encounters an error, in an effort to make it easier to debug. These logs are visible in the Logs UI. Testing: I could not find any unit testing for things like the UI or the Lua code, so the only testing done was by hand. The credentials were removed from the config, and both the ubus call and the web UI render the failure to acquire the credentials. The REST API was forced to return an error message, and the UI was verified as showing the RPC error message. Package was built and tested with ``` make package/luci-app-adguardhome/compile -j20 scp bin/packages/i386_pentium4/cricalixluci/luci-app-adguardhome_unknown_all.ipk root@192.168.0.1: ssh root@192.168.0.1 opkg remove luci-app-adguardhome && opkg install luci-app-adguardhome_unknown_all.ipk && rm luci-app-adguardhome_unknown_all.ipk ``` and then browsing the UI to make sure the deployment was clean and behaved as expected (unknown_all because this was done pre-commit). Signed-off-by: Duncan Hill --- applications/luci-app-adguardhome/Makefile | 14 ++ applications/luci-app-adguardhome/README.md | 34 +++ .../resources/view/adguardhome/config.js | 40 ++++ .../resources/view/adguardhome/logs.js | 63 ++++++ .../resources/view/adguardhome/status.js | 165 +++++++++++++++ .../po/templates/adguardhome.pot | 172 +++++++++++++++ .../root/usr/libexec/rpcd/luci.adguardhome | 195 ++++++++++++++++++ .../luci/menu.d/luci-app-adguardhome.json | 40 ++++ .../rpcd/acl.d/luci-app-adguardhome.json | 20 ++ 9 files changed, 743 insertions(+) create mode 100644 applications/luci-app-adguardhome/Makefile create mode 100644 applications/luci-app-adguardhome/README.md create mode 100644 applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js create mode 100644 applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js create mode 100644 applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js create mode 100644 applications/luci-app-adguardhome/po/templates/adguardhome.pot create mode 100755 applications/luci-app-adguardhome/root/usr/libexec/rpcd/luci.adguardhome create mode 100644 applications/luci-app-adguardhome/root/usr/share/luci/menu.d/luci-app-adguardhome.json create mode 100644 applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json diff --git a/applications/luci-app-adguardhome/Makefile b/applications/luci-app-adguardhome/Makefile new file mode 100644 index 000000000000..2386d62cd661 --- /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 +lua-cjson +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..bb98d99076d5 --- /dev/null +++ b/applications/luci-app-adguardhome/README.md @@ -0,0 +1,34 @@ +# LuCI App AdGuardHome + +This LuCI app provides basic integration with the [AdGuard Home]() package for OpenWrt. Note that the AdGuard Home package requires interaction with the OpenWrt command line; this app does not remove any of that interaction. + +## 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. + +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 + +* lua-cjson, for doing JSON encoding/decoding +* luasocket, for talking to the AdGuard Home REST API +* lyaml, for parsing the AdGuard home YAML configuration +* adguardhome, as this app is useless without it \ 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..121b75276fdb --- /dev/null +++ b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js @@ -0,0 +1,40 @@ +'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.') + ); + 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..a2124caedac6 --- /dev/null +++ b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js @@ -0,0 +1,63 @@ +'use strict'; +'require dom'; +'require fs'; +'require poll'; +'require view'; + +var css = ' \ +#log_textarea { \ + padding: 10px; \ + text-align: left; \ +} \ +#log_textarea pre { \ + padding: .5rem; \ + word-break: break-all; \ + margin: 0; \ +} \ +.description { \ + background-color: #33ccff; \ +}'; + +function pollLog(e) { + return Promise.all([ + fs.exec_direct('/sbin/logread', ['-e', 'adguardhome']).then(function (res) { + return res.trim().split(/\n/).reverse().slice(0, 50).join('\n') + }) + ]).then(function (data) { + var t = E('pre', { 'wrap': 'pre' }, [ + E('br'), + data[0] || _('No log data.') + ]); + dom.content(e, t); + }); +}; + +return view.extend({ + render: function () { + var log_textarea = E('div', { 'id': 'log_textarea' }, + E('img', { + 'src': L.resource(['icons/loading.gif']), + 'alt': _('Loading'), + 'style': 'vertical-align:middle' + }, _('Collecting data...')) + ); + + poll.add(pollLog.bind(this, log_textarea)); + return E([ + E('style', [css]), + E('div', { 'class': 'cbi-map' }, [ + E('h2', { 'name': 'content' }, '%s - %s'.format(_('AdGuard Home'), _('Syslog'))), + E('div', { 'class': 'cbi-section' }, [ + log_textarea, + E('div', { 'style': 'text-align:right' }, + E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval)) + ) + ]) + ]) + ]); + }, + + 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..d7df55af71b2 --- /dev/null +++ b/applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js @@ -0,0 +1,165 @@ +'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 upstream_dns = L.isObject(agh_config.dns.bootstrap_dns) ? agh_config.dns.bootstrap_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, + _('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..b0391ac4fc0f --- /dev/null +++ b/applications/luci-app-adguardhome/po/templates/adguardhome.pot @@ -0,0 +1,172 @@ +msgid "" +msgstr "Content-Type: text/plain; charset=UTF-8" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js:49 +#: 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:148 +msgid "AdGuard Home Statistics" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:144 +msgid "AdGuard Home Status" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:132 +msgid "AdGuard Home Status - Error" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:67 +msgid "Average processing time (seconds)" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js:42 +msgid "Collecting data..." +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:64 +msgid "DNS blocks" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:43 +msgid "DNS listen addresses" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:42 +msgid "DNS listen port" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:63 +msgid "DNS queries" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:66 +msgid "DNS replacements (malware/phishing)" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:65 +msgid "DNS replacements (safesearch)" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:21 +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/htdocs/luci-static/resources/view/adguardhome/logs.js:40 +msgid "Loading" +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:38 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:39 +msgid "No" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js:29 +msgid "No log data." +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:52 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:75 +msgid "Not found" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:34 +msgid "Password for AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:134 +msgid "Please open the Configuration section, and provide the credentials." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:39 +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/logs.js:53 +msgid "Refresh every %s seconds." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:38 +msgid "Running" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:40 +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:49 +msgid "Syslog" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:35 +msgid "The password you configured when you set up AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:28 +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." +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:156 +msgid "Top Blocked Domains" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:152 +msgid "Top Queried Domains" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:44 +msgid "Upstream DNS addresses" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js:27 +msgid "Username for AdGuard Home" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:45 +msgid "Version" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:41 +msgid "Web interface" +msgstr "" + +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:38 +#: applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js:39 +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..6c31ec6bf686 --- /dev/null +++ b/applications/luci-app-adguardhome/root/usr/libexec/rpcd/luci.adguardhome @@ -0,0 +1,195 @@ +#!/usr/bin/env lua + +local fs = require "nixio.fs" +local http = require "socket.http" +local json = require "cjson" +local ltn12 = require "ltn12" +local luci_json = require "luci.jsonc" +local lyaml = require "lyaml" +local mime = require "mime" +local sock_url = require "socket.url" +local nixio = require "nixio" +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 + +-- Borrowed from the 'net for debugging +local function dump(o) + if type(o) == 'table' then + local s = '{ ' + for k, v in pairs(o) do + if type(k) ~= 'number' then k = '"' .. k .. '"' end + s = s .. '[' .. k .. '] = ' .. dump(v) .. ',' + end + return s .. '} ' + else + return tostring(o) + end +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() + 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 + msg = "Username not found in uci adguardhome" + log_error(msg) + print(luci_json.stringify({auth_error = msg})) + os.exit(1) + end + if not password then + msg = "Password not found in uci adguardhome" + log_error(msg) + print(luci_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) + } + result = resp[1] + if code == 200 then + -- Make the shell happy; 0 is good. All else is an error exit code. + code = 0 + else + log_error("REST call failed with " .. resp[1]) + result = json.encode({error = resp[1]}) + end + + r.code = code + r.headers = headers + r.status = status + 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.encode(agh_table) + r.code = 200 + r.status = "OK" + r.headers = {} + 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} +} + +-- Borrowed from luci.ddns, modified. +local function parseInput(arg) + local parse = luci_json.new() + local done, err + + done, err = parse:parse(arg) + + if not done then + print(luci_json.stringify({error = err or "Incomplete input"})) + os.exit(1) + end + + return parse:get() +end + +-- Borrowed from luci.ddns +local function validateArgs(func, uargs) + local method = methods[func] + if not method then + print(luci_json.stringify({error = "Method not found in methods"})) + os.exit(1) + end + + if type(uargs) ~= "table" then + print(luci_json.stringify({error = "Invalid arguments"})) + 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(luci_json.stringify({error = "Invalid arguments"})) + os.exit(1) + end + end + + return method +end + +-- Borrowed from luci.ddns +if arg[1] == "list" then + local _, rv = nil, {} + for _, method in pairs(methods) do rv[_] = method.args or {} end + print((luci_json.stringify(rv):gsub(":%[%]", ":{}"))) +elseif arg[1] == "call" then + local args = {} + if arg[3] then + args = parseInput(arg[3]) + else + args = {} + end + local method = validateArgs(arg[2], args) + local run = method.call(args) + print(run.result) + os.exit(run.code or 0) +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..44fca21d7fec --- /dev/null +++ b/applications/luci-app-adguardhome/root/usr/share/rpcd/acl.d/luci-app-adguardhome.json @@ -0,0 +1,20 @@ +{ + "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" ] + } + } + } + } +} \ No newline at end of file