-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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 of the application. 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. Incorrect credentials were supplied, and the UI was verified as showing a RPC error message. ACLs were checked by removing luci-compat.json (which wildcard allows saving/reading), and ensuring that the UI could read and save the config. 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. Signed-off-by: Duncan Hill <openwrt-dev@cricalix.net>
- Loading branch information
Duncan Hill
committed
Jun 27, 2022
1 parent
fd72e1c
commit ace79f8
Showing
9 changed files
with
760 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
# Copyright 2022- Duncan Hill <openwrt-dev@cricalix.net> | ||
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
41 changes: 41 additions & 0 deletions
41
applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/config.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
}) |
44 changes: 44 additions & 0 deletions
44
applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/logs.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}); |
167 changes: 167 additions & 0 deletions
167
applications/luci-app-adguardhome/htdocs/luci-static/resources/view/adguardhome/status.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
}) |
Oops, something went wrong.