#!/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 }