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:
Андрей Бобырев
2026-05-24 23:04:44 +03:00
parent a3d5c8e496
commit 66354221a4
9 changed files with 283 additions and 18 deletions

View File

@@ -2,6 +2,7 @@
# CGI API helpers for 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"
TOKEN_FILE="${KSD_ETC}/token"
LOG_FILE="${KSD_ETC}/apply.log"
@@ -81,7 +82,23 @@ api_status() {
last_apply=""
[ -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() {
@@ -118,8 +135,12 @@ api_save_config() {
}
api_reload() {
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
if "$SCRIPT_DIR/apply.sh" >>"$LOG_FILE" 2>&1; then
APPLY="${KSD_SHARE}/scripts/apply.sh"
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')"
echo "${ts} reload OK" >>"$LOG_FILE"
api_send_json "200" '{"ok":true,"message":"applied"}'
@@ -131,7 +152,8 @@ api_reload() {
api_test_dns() {
domain="${QUERY_STRING#*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"
rtype="A"
case "$QUERY_STRING" in
@@ -161,14 +183,21 @@ api_domains_list() {
for f in "${KSD_ETC}"/domain-sets/*.txt; do
[ -f "$f" ] || continue
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
d="$(echo "$d" | tr -d '\r')"
[ -z "$d" ] && continue
case "$d" in \#*) continue ;; esac
[ "$first" -eq 1 ] || echo -n ','
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
fi

View 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);
});

View File

@@ -86,7 +86,14 @@ LOG_Q="$(yaml_val log_queries)"
# --- upstream servers ---
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
@@ -119,7 +126,14 @@ echo "" >> "$OUT_SMART"
# --- domain groups ---
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

View File

@@ -24,7 +24,7 @@ detect_lan_ip() {
detect_isp_dns() {
dns=""
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
if [ -z "$dns" ]; then
dns="$(grep '^nameserver' /etc/resolv.conf 2>/dev/null | awk '{print $2}' | head -1)"