Files
Keenetic-Split-DNS/scripts/api.sh
Андрей Бобырев bc9355fe40 Initial release: install, uninstall, SmartDNS, web UI
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-24 22:15:37 +03:00

209 lines
5.6 KiB
Bash
Executable File

#!/bin/sh
# CGI API helpers for Keenetic-Split-DNS
KSD_ETC="${KSD_ETC:-/opt/etc/keenetic-split-dns}"
CONFIG="${KSD_ETC}/config.yaml"
TOKEN_FILE="${KSD_ETC}/token"
LOG_FILE="${KSD_ETC}/apply.log"
json_escape() {
printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g' | tr '\n' ' '
}
api_send_json() {
code="${1:-200}"
body="$2"
printf 'Status: %s\r\n' "$code"
printf 'Content-Type: application/json; charset=utf-8\r\n'
printf 'Cache-Control: no-store\r\n'
printf '\r\n'
printf '%s' "$body"
}
api_unauthorized() {
api_send_json "401" '{"ok":false,"error":"unauthorized"}'
exit 0
}
api_check_auth() {
expected=""
[ -f "$TOKEN_FILE" ] && expected="$(cat "$TOKEN_FILE" | tr -d '\n\r ')"
[ -n "$expected" ] || return 0
got=""
case "$HTTP_AUTHORIZATION" in
Bearer*) got="${HTTP_AUTHORIZATION#Bearer }" ;;
esac
[ -z "$got" ] && got="$HTTP_X_KSD_TOKEN"
[ -z "$got" ] && got="$HTTP_X_DNS_SPLIT_TOKEN"
if [ "$got" != "$expected" ]; then
api_unauthorized
fi
}
api_read_body() {
if [ -n "$CONTENT_LENGTH" ] && [ "$CONTENT_LENGTH" -gt 0 ] 2>/dev/null; then
dd bs=1 count="$CONTENT_LENGTH" 2>/dev/null
fi
}
smartdns_running() {
if pidof smartdns >/dev/null 2>&1; then
echo "true"
else
echo "false"
fi
}
smartdns_pid() {
pidof smartdns 2>/dev/null | awk '{print $1}'
}
count_domains() {
n=0
if [ -d "${KSD_ETC}/domain-sets" ]; then
for f in "${KSD_ETC}"/domain-sets/*.txt; do
[ -f "$f" ] || continue
n=$((n + $(grep -cve '^\s*$' -e '^\s*#' "$f" 2>/dev/null || echo 0)))
done
fi
echo "$n"
}
api_status() {
lan="$(grep '^lan_ip:' "$CONFIG" 2>/dev/null | sed 's/.*"\([^"]*\)".*/\1/')"
port="$(grep '^web_port:' "$CONFIG" 2>/dev/null | awk '{print $2}')"
[ -n "$port" ] || port="3200"
domains="$(count_domains)"
running="$(smartdns_running)"
pid="$(smartdns_pid)"
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}\"}"
}
api_get_config_raw() {
if [ ! -f "$CONFIG" ]; then
api_send_json "404" '{"ok":false,"error":"config not found"}'
exit 0
fi
printf 'Status: 200\r\n'
printf 'Content-Type: application/x-yaml; charset=utf-8\r\n'
printf 'Cache-Control: no-store\r\n'
printf '\r\n'
cat "$CONFIG"
exit 0
}
api_save_config() {
body="$(api_read_body)"
[ -n "$body" ] || { api_send_json "400" '{"ok":false,"error":"empty body"}'; exit 0; }
# Accept raw YAML body or {"content":"..."}
case "$body" in
\{*)
content="$(printf '%s' "$body" | sed -n 's/.*"content"[[:space:]]*:[[:space:]]*"\(.*\)".*/\1/p' | sed 's/\\n/\n/g; s/\\"/"/g')"
;;
*)
content="$body"
;;
esac
[ -n "$content" ] || { api_send_json "400" '{"ok":false,"error":"no content"}'; exit 0; }
cp "$CONFIG" "${CONFIG}.bak" 2>/dev/null || true
printf '%s\n' "$content" > "$CONFIG"
api_send_json "200" '{"ok":true,"message":"saved"}'
}
api_reload() {
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname "$0")" && pwd)"
if "$SCRIPT_DIR/apply.sh" >>"$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"}'
else
api_send_json "500" '{"ok":false,"error":"apply failed"}'
fi
}
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")"
[ -n "$domain" ] || domain="vk.com"
rtype="A"
case "$QUERY_STRING" in
*type=AAAA*) rtype="AAAA" ;;
*type=HTTPS*) rtype="HTTPS" ;;
esac
if command -v dig >/dev/null 2>&1; then
dns_port="$(grep '^dns_port:' "$CONFIG" 2>/dev/null | awk '{print $2}')"
[ -n "$dns_port" ] || dns_port="53"
start_ms="$(date +%s)"
out="$(dig @"127.0.0.1" -p "$dns_port" "$domain" "$rtype" +time=3 +tries=1 2>&1)" || out="dig failed"
end_ms="$(date +%s)"
ms=$(( (end_ms - start_ms) * 1000 ))
esc="$(printf '%s' "$out" | json_escape)"
api_send_json "200" "{\"ok\":true,\"domain\":\"${domain}\",\"type\":\"${rtype}\",\"ms\":${ms},\"output\":\"${esc}\"}"
else
api_send_json "503" '{"ok":false,"error":"dig not installed"}'
fi
}
api_domains_list() {
# Build simple JSON array from domain sets
echo -n '{"ok":true,"domains":['
first=1
if [ -d "${KSD_ETC}/domain-sets" ]; then
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)"
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}"
done < "$f"
done
fi
echo ']}'
}
api_route() {
path="${PATH_INFO:-/}"
path="${path#/}"
method="${REQUEST_METHOD:-GET}"
api_check_auth
case "$path" in
status|"") api_status ;;
config)
case "$method" in
GET) api_get_config_raw ;;
POST) api_save_config ;;
*) api_send_json "405" '{"ok":false,"error":"method"}' ;;
esac
;;
domains)
body="$(api_domains_list)"
api_send_json "200" "$body"
;;
reload|apply)
api_reload
;;
test)
api_test_dns
;;
*)
api_send_json "404" '{"ok":false,"error":"not found"}'
;;
esac
}