mirror of
https://github.com/andrey271192/Keenetic-Split-DNS.git
synced 2026-09-20 11:55:36 +00:00
fix: audit corrections
Fix YAML section parsing and upstream ID extraction in compile-config, detect-lan shell syntax, lighttpd CGI modules, API reload path, install idempotency, and dashboard log display. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,5 +2,6 @@
|
|||||||
# Keenetic-Split-DNS REST API (CGI)
|
# Keenetic-Split-DNS REST API (CGI)
|
||||||
|
|
||||||
export KSD_ETC="${KSD_ETC:-/opt/etc/keenetic-split-dns}"
|
export KSD_ETC="${KSD_ETC:-/opt/etc/keenetic-split-dns}"
|
||||||
. /opt/share/keenetic-split-dns/scripts/api.sh
|
export KSD_SHARE="${KSD_SHARE:-/opt/share/keenetic-split-dns}"
|
||||||
|
. "${KSD_SHARE}/scripts/api.sh"
|
||||||
api_route
|
api_route
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
# Keenetic-Split-DNS web UI — lighttpd
|
# Keenetic-Split-DNS web UI — lighttpd
|
||||||
# Installed to /opt/etc/keenetic-split-dns/lighttpd.conf
|
# Installed to /opt/etc/keenetic-split-dns/lighttpd.conf
|
||||||
|
|
||||||
|
server.modules += (
|
||||||
|
"mod_cgi",
|
||||||
|
"mod_rewrite"
|
||||||
|
)
|
||||||
|
|
||||||
server.document-root = "/opt/share/keenetic-split-dns/www"
|
server.document-root = "/opt/share/keenetic-split-dns/www"
|
||||||
server.port = 3200
|
server.port = 3200
|
||||||
server.bind = "LAN_IP_PLACEHOLDER"
|
server.bind = "LAN_IP_PLACEHOLDER"
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
server-name keenetic-split-dns
|
server-name keenetic-split-dns
|
||||||
log-level info
|
log-level info
|
||||||
cache-size 512
|
cache-size 512
|
||||||
cache-persist yes
|
# cache-persist set by compile-config.sh
|
||||||
prefetch-domain yes
|
prefetch-domain yes
|
||||||
serve-expired yes
|
serve-expired yes
|
||||||
dnsmasq-lease-file /var/lib/misc/dnsmasq.leases
|
dnsmasq-lease-file /var/lib/misc/dnsmasq.leases
|
||||||
|
|||||||
20
install.sh
20
install.sh
@@ -58,8 +58,9 @@ mkdir -p "$KSD_ETC" "$KSD_SHARE" "$KSD_VAR_LOG" "$KSD_VAR_RUN" \
|
|||||||
"${KSD_SHARE}/domain-sets"
|
"${KSD_SHARE}/domain-sets"
|
||||||
|
|
||||||
# --- copy files ---
|
# --- copy files ---
|
||||||
cp -f "$SRC_DIR/etc/config.yaml.example" "${KSD_ETC}/config.yaml" 2>/dev/null || true
|
if [ ! -f "${KSD_ETC}/config.yaml" ]; then
|
||||||
[ -f "${KSD_ETC}/config.yaml" ] || cp "$SRC_DIR/etc/config.yaml.example" "${KSD_ETC}/config.yaml"
|
cp "$SRC_DIR/etc/config.yaml.example" "${KSD_ETC}/config.yaml"
|
||||||
|
fi
|
||||||
|
|
||||||
cp -rf "$SRC_DIR/scripts/"* "${KSD_SHARE}/scripts/"
|
cp -rf "$SRC_DIR/scripts/"* "${KSD_SHARE}/scripts/"
|
||||||
cp -rf "$SRC_DIR/www/"* "${KSD_SHARE}/www/"
|
cp -rf "$SRC_DIR/www/"* "${KSD_SHARE}/www/"
|
||||||
@@ -79,10 +80,13 @@ chmod +x "${KSD_SHARE}/scripts/"*.sh "${KSD_SHARE}/cgi-bin/api.cgi"
|
|||||||
DETECT="${KSD_SHARE}/scripts/detect-lan.sh"
|
DETECT="${KSD_SHARE}/scripts/detect-lan.sh"
|
||||||
LAN_IP="$("$DETECT" lan 2>/dev/null || echo "192.168.1.1")"
|
LAN_IP="$("$DETECT" lan 2>/dev/null || echo "192.168.1.1")"
|
||||||
if grep -q 'lan_ip: "192.168.1.1"' "${KSD_ETC}/config.yaml" 2>/dev/null; then
|
if grep -q 'lan_ip: "192.168.1.1"' "${KSD_ETC}/config.yaml" 2>/dev/null; then
|
||||||
sed -i "s/lan_ip: \"192.168.1.1\"/lan_ip: \"${LAN_IP}\"/" "${KSD_ETC}/config.yaml" 2>/dev/null \
|
if sed -i "s/lan_ip: \"192.168.1.1\"/lan_ip: \"${LAN_IP}\"/" "${KSD_ETC}/config.yaml" 2>/dev/null; then
|
||||||
|| sed "s/lan_ip: \"192.168.1.1\"/lan_ip: \"${LAN_IP}\"/" "${KSD_ETC}/config.yaml" > "${KSD_ETC}/config.yaml.tmp" \
|
:
|
||||||
|
else
|
||||||
|
sed "s/lan_ip: \"192.168.1.1\"/lan_ip: \"${LAN_IP}\"/" "${KSD_ETC}/config.yaml" > "${KSD_ETC}/config.yaml.tmp" \
|
||||||
&& mv "${KSD_ETC}/config.yaml.tmp" "${KSD_ETC}/config.yaml"
|
&& mv "${KSD_ETC}/config.yaml.tmp" "${KSD_ETC}/config.yaml"
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# --- API token ---
|
# --- API token ---
|
||||||
if [ ! -f "${KSD_ETC}/token" ]; then
|
if [ ! -f "${KSD_ETC}/token" ]; then
|
||||||
@@ -107,8 +111,12 @@ mkdir -p /opt/etc/ndm/netfilter.d
|
|||||||
cp -f "$SRC_DIR/etc/ndm/netfilter.d/010-keenetic-split-dns.sh" /opt/etc/ndm/netfilter.d/
|
cp -f "$SRC_DIR/etc/ndm/netfilter.d/010-keenetic-split-dns.sh" /opt/etc/ndm/netfilter.d/
|
||||||
chmod +x /opt/etc/ndm/netfilter.d/010-keenetic-split-dns.sh
|
chmod +x /opt/etc/ndm/netfilter.d/010-keenetic-split-dns.sh
|
||||||
|
|
||||||
# --- domain sets to etc ---
|
# --- domain sets to etc (seed defaults only) ---
|
||||||
cp -f "${KSD_SHARE}/domain-sets/"*.txt "${KSD_ETC}/domain-sets/" 2>/dev/null || true
|
for _ds in "${KSD_SHARE}/domain-sets/"*.txt; do
|
||||||
|
[ -f "$_ds" ] || continue
|
||||||
|
_base="$(basename "$_ds")"
|
||||||
|
[ -f "${KSD_ETC}/domain-sets/${_base}" ] || cp -f "$_ds" "${KSD_ETC}/domain-sets/${_base}"
|
||||||
|
done
|
||||||
|
|
||||||
# --- compile & start ---
|
# --- compile & start ---
|
||||||
export KSD_ETC KSD_SHARE
|
export KSD_ETC KSD_SHARE
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# CGI API helpers for Keenetic-Split-DNS
|
# CGI API helpers for Keenetic-Split-DNS
|
||||||
|
|
||||||
KSD_ETC="${KSD_ETC:-/opt/etc/keenetic-split-dns}"
|
KSD_ETC="${KSD_ETC:-/opt/etc/keenetic-split-dns}"
|
||||||
|
KSD_SHARE="${KSD_SHARE:-/opt/share/keenetic-split-dns}"
|
||||||
CONFIG="${KSD_ETC}/config.yaml"
|
CONFIG="${KSD_ETC}/config.yaml"
|
||||||
TOKEN_FILE="${KSD_ETC}/token"
|
TOKEN_FILE="${KSD_ETC}/token"
|
||||||
LOG_FILE="${KSD_ETC}/apply.log"
|
LOG_FILE="${KSD_ETC}/apply.log"
|
||||||
@@ -81,7 +82,23 @@ api_status() {
|
|||||||
last_apply=""
|
last_apply=""
|
||||||
[ -f "$LOG_FILE" ] && last_apply="$(tail -1 "$LOG_FILE" 2>/dev/null | json_escape)"
|
[ -f "$LOG_FILE" ] && last_apply="$(tail -1 "$LOG_FILE" 2>/dev/null | json_escape)"
|
||||||
|
|
||||||
api_send_json "200" "{\"ok\":true,\"smartdns\":{\"running\":${running},\"pid\":\"${pid}\"},\"domains\":${domains},\"lan_ip\":\"${lan}\",\"web_port\":${port},\"url\":\"http://${lan}:${port}\"}"
|
log_json="[]"
|
||||||
|
if [ -f "$LOG_FILE" ]; then
|
||||||
|
_tmp="$(mktemp /tmp/ksd-logs.XXXXXX)"
|
||||||
|
tail -5 "$LOG_FILE" 2>/dev/null > "$_tmp"
|
||||||
|
log_json="["
|
||||||
|
first=1
|
||||||
|
while IFS= read -r line; do
|
||||||
|
esc="$(printf '%s' "$line" | json_escape)"
|
||||||
|
[ "$first" -eq 1 ] || log_json="${log_json},"
|
||||||
|
first=0
|
||||||
|
log_json="${log_json}\"${esc}\""
|
||||||
|
done < "$_tmp"
|
||||||
|
log_json="${log_json}]"
|
||||||
|
rm -f "$_tmp"
|
||||||
|
fi
|
||||||
|
|
||||||
|
api_send_json "200" "{\"ok\":true,\"smartdns\":{\"running\":${running},\"pid\":\"${pid}\"},\"domains\":${domains},\"lan_ip\":\"${lan}\",\"web_port\":${port},\"url\":\"http://${lan}:${port}\",\"last_apply\":\"${last_apply}\",\"logs\":${log_json}}"
|
||||||
}
|
}
|
||||||
|
|
||||||
api_get_config_raw() {
|
api_get_config_raw() {
|
||||||
@@ -118,8 +135,12 @@ api_save_config() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
api_reload() {
|
api_reload() {
|
||||||
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
|
APPLY="${KSD_SHARE}/scripts/apply.sh"
|
||||||
if "$SCRIPT_DIR/apply.sh" >>"$LOG_FILE" 2>&1; then
|
if [ ! -x "$APPLY" ]; then
|
||||||
|
api_send_json "500" '{"ok":false,"error":"apply.sh not found"}'
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
if "$APPLY" >>"$LOG_FILE" 2>&1; then
|
||||||
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
ts="$(date '+%Y-%m-%d %H:%M:%S')"
|
||||||
echo "${ts} reload OK" >>"$LOG_FILE"
|
echo "${ts} reload OK" >>"$LOG_FILE"
|
||||||
api_send_json "200" '{"ok":true,"message":"applied"}'
|
api_send_json "200" '{"ok":true,"message":"applied"}'
|
||||||
@@ -131,7 +152,8 @@ api_reload() {
|
|||||||
api_test_dns() {
|
api_test_dns() {
|
||||||
domain="${QUERY_STRING#*domain=}"
|
domain="${QUERY_STRING#*domain=}"
|
||||||
domain="${domain%%&*}"
|
domain="${domain%%&*}"
|
||||||
domain="$(printf '%s' "$domain" | sed 's/%\([0-9A-F][0-9A-F]\)/\\x\1/g' | xargs -0 printf '%b' 2>/dev/null || echo "$domain")"
|
# URL-decode (percent-encoding)
|
||||||
|
domain="$(printf '%s' "$domain" | sed 's/+/ /g; s/%\([0-9A-Fa-f][0-9A-Fa-f]\)/\\x\1/g' | xargs printf '%b' 2>/dev/null || printf '%s' "$domain")"
|
||||||
[ -n "$domain" ] || domain="vk.com"
|
[ -n "$domain" ] || domain="vk.com"
|
||||||
rtype="A"
|
rtype="A"
|
||||||
case "$QUERY_STRING" in
|
case "$QUERY_STRING" in
|
||||||
@@ -161,14 +183,21 @@ api_domains_list() {
|
|||||||
for f in "${KSD_ETC}"/domain-sets/*.txt; do
|
for f in "${KSD_ETC}"/domain-sets/*.txt; do
|
||||||
[ -f "$f" ] || continue
|
[ -f "$f" ] || continue
|
||||||
group="$(basename "$f" .txt)"
|
group="$(basename "$f" .txt)"
|
||||||
upstream="$(awk -v g="$group" '$0 ~ "^ " g ":$" {f=1} f && $0 ~ "upstream:" {print $2; exit}' "$CONFIG" 2>/dev/null)"
|
upstream="$(awk -v g="$group" '
|
||||||
|
$0 ~ "^ " g ":$" { in_g=1; next }
|
||||||
|
in_g && /^[^ #]/ { exit }
|
||||||
|
in_g && $0 ~ /^ upstream:/ { print $2; exit }
|
||||||
|
' "$CONFIG" 2>/dev/null)"
|
||||||
while IFS= read -r d || [ -n "$d" ]; do
|
while IFS= read -r d || [ -n "$d" ]; do
|
||||||
d="$(echo "$d" | tr -d '\r')"
|
d="$(echo "$d" | tr -d '\r')"
|
||||||
[ -z "$d" ] && continue
|
[ -z "$d" ] && continue
|
||||||
case "$d" in \#*) continue ;; esac
|
case "$d" in \#*) continue ;; esac
|
||||||
[ "$first" -eq 1 ] || echo -n ','
|
[ "$first" -eq 1 ] || echo -n ','
|
||||||
first=0
|
first=0
|
||||||
printf '{"domain":"%s","group":"%s","upstream":"%s"}' "$d" "$group" "${upstream:-yandex-dot}"
|
de="$(printf '%s' "$d" | json_escape)"
|
||||||
|
ge="$(printf '%s' "$group" | json_escape)"
|
||||||
|
ue="$(printf '%s' "${upstream:-yandex-dot}" | json_escape)"
|
||||||
|
printf '{"domain":"%s","group":"%s","upstream":"%s"}' "$de" "$ge" "$ue"
|
||||||
done < "$f"
|
done < "$f"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
186
scripts/capture-readme-screenshots.mjs
Normal file
186
scripts/capture-readme-screenshots.mjs
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Capture README screenshots from www/ with mocked API (no router required).
|
||||||
|
* Usage: node scripts/capture-readme-screenshots.mjs
|
||||||
|
*/
|
||||||
|
import { chromium } from 'playwright';
|
||||||
|
import { createServer } from 'http';
|
||||||
|
import { readFileSync, mkdirSync } from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const repo = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||||||
|
const www = path.join(repo, 'www');
|
||||||
|
const outDir = path.join(repo, 'docs', 'images');
|
||||||
|
|
||||||
|
const domains = `yandex.ru
|
||||||
|
ya.ru
|
||||||
|
yandex.com
|
||||||
|
yandex.net
|
||||||
|
yastatic.net
|
||||||
|
yandex.st
|
||||||
|
mail.ru
|
||||||
|
mail.com
|
||||||
|
imgsmail.ru
|
||||||
|
mycdn.me
|
||||||
|
vk.com
|
||||||
|
vk.me
|
||||||
|
vkuservideo.net
|
||||||
|
vkuseraudio.net
|
||||||
|
userapi.com
|
||||||
|
vk-cdn.net
|
||||||
|
ok.ru
|
||||||
|
odnoklassniki.ru
|
||||||
|
okcdn.ru`
|
||||||
|
.trim()
|
||||||
|
.split('\n')
|
||||||
|
.map((d) => ({ domain: d, group: 'ru-services', upstream: 'yandex-dot' }));
|
||||||
|
|
||||||
|
const status = {
|
||||||
|
ok: true,
|
||||||
|
smartdns: { running: true, pid: '1842' },
|
||||||
|
domains: domains.length,
|
||||||
|
lan_ip: '192.168.1.1',
|
||||||
|
web_port: 3200,
|
||||||
|
url: 'http://192.168.1.1:3200',
|
||||||
|
last_apply: '2026-05-24 12:04:11 reload OK',
|
||||||
|
logs: [
|
||||||
|
'2026-05-24 12:01:02 SmartDNS запущен, listen 0.0.0.0:53',
|
||||||
|
'2026-05-24 12:01:03 Политика ru-services → yandex-dot (19 доменов)',
|
||||||
|
'2026-05-24 12:04:11 lookup vk.com A → 87.240.190.78',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
const digOutput = `; <<>> DiG 9.18.24 <<>> @127.0.0.1 vk.com A
|
||||||
|
;; global options: +cmd
|
||||||
|
;; Got answer:
|
||||||
|
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 48291
|
||||||
|
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0
|
||||||
|
|
||||||
|
;; QUESTION SECTION:
|
||||||
|
;vk.com. IN A
|
||||||
|
|
||||||
|
;; ANSWER SECTION:
|
||||||
|
vk.com. 300 IN A 87.240.190.78
|
||||||
|
|
||||||
|
;; Query time: 24 msec
|
||||||
|
;; SERVER: 127.0.0.1#53(127.0.0.1)
|
||||||
|
;; WHEN: Sun May 24 12:04:11 MSK 2026
|
||||||
|
;; MSG SIZE rcvd: 52`;
|
||||||
|
|
||||||
|
const mime = {
|
||||||
|
'.html': 'text/html; charset=utf-8',
|
||||||
|
'.css': 'text/css; charset=utf-8',
|
||||||
|
'.js': 'application/javascript; charset=utf-8',
|
||||||
|
};
|
||||||
|
|
||||||
|
function createStaticServer() {
|
||||||
|
const yaml = readFileSync(path.join(repo, 'etc', 'config.yaml.example'), 'utf8');
|
||||||
|
return createServer((req, res) => {
|
||||||
|
const url = new URL(req.url, 'http://127.0.0.1');
|
||||||
|
if (url.pathname.startsWith('/api/')) {
|
||||||
|
const route = url.pathname.slice(4);
|
||||||
|
if (route === '/status') {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end(JSON.stringify(status));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route === '/domains') {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end(JSON.stringify({ ok: true, domains }));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route.startsWith('/test')) {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: true,
|
||||||
|
domain: url.searchParams.get('domain') || 'vk.com',
|
||||||
|
type: 'A',
|
||||||
|
ms: 24,
|
||||||
|
output: digOutput,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route === '/config') {
|
||||||
|
res.setHeader('Content-Type', 'application/x-yaml');
|
||||||
|
res.end(yaml);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (route === '/reload') {
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.end('{"ok":true,"message":"applied"}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('{}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rel = url.pathname === '/' ? '/index.html' : url.pathname;
|
||||||
|
const file = path.join(www, rel);
|
||||||
|
try {
|
||||||
|
const data = readFileSync(file);
|
||||||
|
res.setHeader('Content-Type', mime[path.extname(file)] || 'application/octet-stream');
|
||||||
|
res.end(data);
|
||||||
|
} catch {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('not found');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function waitDashboard(page) {
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => document.getElementById('stat-domains')?.textContent !== '—',
|
||||||
|
{ timeout: 10000 }
|
||||||
|
);
|
||||||
|
await page.evaluate(() => {
|
||||||
|
const log = document.getElementById('log-container');
|
||||||
|
if (!log) return;
|
||||||
|
log.innerHTML = `
|
||||||
|
<div class="log-line"><span class="log-time">12:01:02</span> SmartDNS запущен, listen 0.0.0.0:53</div>
|
||||||
|
<div class="log-line"><span class="log-time">12:01:03</span> Политика ru-services → yandex-dot (19 доменов)</div>
|
||||||
|
<div class="log-line"><span class="log-time">12:04:11</span> lookup vk.com A → 87.240.190.78</div>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
mkdirSync(outDir, { recursive: true });
|
||||||
|
const server = createStaticServer();
|
||||||
|
await new Promise((resolve) => server.listen(0, '127.0.0.1', resolve));
|
||||||
|
const port = server.address().port;
|
||||||
|
const base = `http://127.0.0.1:${port}/`;
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: 1360, height: 900 } });
|
||||||
|
|
||||||
|
await page.goto(base);
|
||||||
|
await page.evaluate(() => localStorage.setItem('ksd_token', 'readme-demo'));
|
||||||
|
await page.reload();
|
||||||
|
await waitDashboard(page);
|
||||||
|
await page.screenshot({ path: path.join(outDir, 'overview.png') });
|
||||||
|
|
||||||
|
await page.click('.nav-item[data-tab="upstreams"]');
|
||||||
|
await page.waitForTimeout(400);
|
||||||
|
await page.screenshot({ path: path.join(outDir, 'upstreams-domains.png') });
|
||||||
|
|
||||||
|
await page.click('.nav-item[data-tab="test"]');
|
||||||
|
await page.click('#run-test');
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => (document.getElementById('test-output')?.textContent || '').includes('vk.com'),
|
||||||
|
{ timeout: 5000 }
|
||||||
|
);
|
||||||
|
await page.waitForTimeout(200);
|
||||||
|
await page.screenshot({ path: path.join(outDir, 'test-lookup.png') });
|
||||||
|
|
||||||
|
await browser.close();
|
||||||
|
server.close();
|
||||||
|
console.log('Saved screenshots to', outDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((err) => {
|
||||||
|
console.error(err);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -86,7 +86,14 @@ LOG_Q="$(yaml_val log_queries)"
|
|||||||
|
|
||||||
# --- upstream servers ---
|
# --- upstream servers ---
|
||||||
list_upstream_ids() {
|
list_upstream_ids() {
|
||||||
awk '/^upstreams:$/,/^[^ ]/ { if ($0 ~ /^ [a-zA-Z0-9_-]+:$/) { gsub(/:$/,"",$1); print substr($1,3) } }' "$CONFIG" | head -20
|
awk '
|
||||||
|
/^upstreams:$/ { in_s=1; next }
|
||||||
|
in_s && /^[^ #]/ { exit }
|
||||||
|
in_s && /^ [a-zA-Z0-9_-]+:$/ {
|
||||||
|
gsub(/:$/, "", $1)
|
||||||
|
print $1
|
||||||
|
}
|
||||||
|
' "$CONFIG"
|
||||||
}
|
}
|
||||||
|
|
||||||
for uid in $(list_upstream_ids); do
|
for uid in $(list_upstream_ids); do
|
||||||
@@ -119,7 +126,14 @@ echo "" >> "$OUT_SMART"
|
|||||||
|
|
||||||
# --- domain groups ---
|
# --- domain groups ---
|
||||||
list_group_ids() {
|
list_group_ids() {
|
||||||
awk '/^domain_groups:$/,/^[^ ]/ { if ($0 ~ /^ [a-zA-Z0-9_-]+:$/) { gsub(/:$/,"",$1); print substr($1,3) } }' "$CONFIG"
|
awk '
|
||||||
|
/^domain_groups:$/ { in_s=1; next }
|
||||||
|
in_s && /^[^ #]/ { exit }
|
||||||
|
in_s && /^ [a-zA-Z0-9_-]+:$/ {
|
||||||
|
gsub(/:$/, "", $1)
|
||||||
|
print $1
|
||||||
|
}
|
||||||
|
' "$CONFIG"
|
||||||
}
|
}
|
||||||
|
|
||||||
for gid in $(list_group_ids); do
|
for gid in $(list_group_ids); do
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ detect_lan_ip() {
|
|||||||
detect_isp_dns() {
|
detect_isp_dns() {
|
||||||
dns=""
|
dns=""
|
||||||
if command -v ndm >/dev/null 2>&1; then
|
if command -v ndm >/dev/null 2>&1; then
|
||||||
dns="$(ndm -p show dns-proxy 2>/dev/null | awk '/server/ {print $3; exit}' | tr -d "'\")"
|
dns="$(ndm -p show dns-proxy 2>/dev/null | awk '/server/ {print $3; exit}' | tr -d "'\"")"
|
||||||
fi
|
fi
|
||||||
if [ -z "$dns" ]; then
|
if [ -z "$dns" ]; then
|
||||||
dns="$(grep '^nameserver' /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -1)"
|
dns="$(grep '^nameserver' /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -1)"
|
||||||
|
|||||||
22
www/app.js
22
www/app.js
@@ -66,6 +66,23 @@
|
|||||||
document.getElementById('stat-url').textContent = s.url || '—';
|
document.getElementById('stat-url').textContent = s.url || '—';
|
||||||
document.getElementById('topbar-sub').textContent = s.url ? s.url.replace('http://', '') + ' · lighttpd + CGI' : 'Keenetic Split DNS';
|
document.getElementById('topbar-sub').textContent = s.url ? s.url.replace('http://', '') + ' · lighttpd + CGI' : 'Keenetic Split DNS';
|
||||||
if (s.web_port) document.getElementById('sidebar-port').textContent = s.web_port;
|
if (s.web_port) document.getElementById('sidebar-port').textContent = s.web_port;
|
||||||
|
const logEl = document.getElementById('log-container');
|
||||||
|
if (logEl) {
|
||||||
|
const lines = Array.isArray(s.logs) ? s.logs : [];
|
||||||
|
if (lines.length) {
|
||||||
|
logEl.innerHTML = lines
|
||||||
|
.map((msg) => {
|
||||||
|
const m = String(msg);
|
||||||
|
const time = m.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/);
|
||||||
|
return `<motion class="log-line"><span class="log-time">${esc(time ? time[1] : '—')}</span> ${esc(time ? m.slice(time[1].length).trim() : m)}</div>`;
|
||||||
|
})
|
||||||
|
.join('');
|
||||||
|
} else if (s.last_apply) {
|
||||||
|
logEl.innerHTML = `<motion class="log-line"><span class="log-time">—</span> ${esc(s.last_apply)}</motion>`;
|
||||||
|
} else {
|
||||||
|
logEl.innerHTML = '<div class="log-line"><span class="log-time">—</span> Журнал пуст — нажмите «Применить»</motion>';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function refreshDomains() {
|
async function refreshDomains() {
|
||||||
@@ -221,6 +238,11 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-add-upstream')?.addEventListener('click', () => {
|
||||||
|
toast('Профили upstream задаются в config.yaml → upstreams');
|
||||||
|
document.querySelector('.nav-item[data-tab="settings"]')?.click();
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('btn-export-domains')?.addEventListener('click', () => {
|
document.getElementById('btn-export-domains')?.addEventListener('click', () => {
|
||||||
const text = state.domains.map((x) => x.domain).join('\n');
|
const text = state.domains.map((x) => x.domain).join('\n');
|
||||||
const a = document.createElement('a');
|
const a = document.createElement('a');
|
||||||
|
|||||||
Reference in New Issue
Block a user