#!/bin/sh
#
# ajax_tcblock - TrapCloud 精准封堵 AJAX 后端
#
. /usr/ramdisk/admin/cgi-bin/common/ajax_common
. /etc/PG.conf

APPNAME=tcblock
CONFDIR="${PGETC}/App/${APPNAME}"
APPCONF="${CONFDIR}/tcblock.conf"
SYNCFILE="${CONFDIR}/sync.state"
LOGFILE="${CONFDIR}/sync.log"
APPCTRL="${PGPATH}/app/${APPNAME}/appctrl"
SYNC_BIN="/usr/ramdisk/app/${APPNAME}/bin/tcblock_sync"
FLOWEYE="/usr/ramdisk/bin/floweye"
BASE_URL="https://www.trapcloud.cn"

mkdir -p "${CONFDIR}"

# ---------- 白名单消毒 ----------
san_token()   { echo "$1" | tr -cd 'A-Za-z0-9_-' | cut -c1-64; }   # apikey / grpname
san_num()     { echo "$1" | tr -cd '0-9' | cut -c1-5; }
in_set()      { # in_set value "a b c" default
	for x in $2; do [ "$1" = "${x}" ] && { echo "$1"; return; }; done
	echo "$3"
}

# 🔴 读配置一律按键读取 + 白名单,绝不 source
# `. "${APPCONF}"` 等于把 conf 内容交给 root shell 执行 —— WebUI 写入侧的白名单
# 挡不住历史脏配置、手工编辑和升级异常(szsync 1200 之前的注入面同源)。
conf_get() { [ -f "${APPCONF}" ] && grep "^$1=" "${APPCONF}" 2>/dev/null | tail -1 | cut -d'=' -f2-; }
load_conf()
{
	enable=$(in_set "$(conf_get enable)" "0 1" "0")
	apikey=$(san_token "$(conf_get apikey)")
	ktype=$(in_set "$(conf_get ktype)" "all attacker scanner research_scanner asset_scanner" "all")
	service=$(in_set "$(conf_get service)" "all ssh rdp vnc db web smb telnet mail ics ai proxy" "all")
	level=$(in_set "$(conf_get level)" "all critical high medium" "all")
	precision=$(in_set "$(conf_get precision)" "standard high" "standard")
	autoupdate=$(in_set "$(conf_get autoupdate)" "0 1" "0")
	days=$(san_num "$(conf_get days)");         [ -z "${days}" ] && days=3
	[ "${days}" -lt 1 ] 2>/dev/null && days=1
	[ "${days}" -gt 7 ] 2>/dev/null && days=7
	interval=$(san_num "$(conf_get interval)"); [ -z "${interval}" ] && interval=1440
	[ "${interval}" -lt 5 ] 2>/dev/null && interval=5
	grpname=$(san_token "$(conf_get grpname)")
	[ -z "${grpname}" ] && grpname="TrapCloud_block"
}

ujson() { print_json_head "utf-8"; printf '%s' "$1"; exit 0; }

# ============================================================
get_config()
{
	load_conf
	st="disable"; [ -f "${CONFDIR}/app_status" ] && st=$(cat "${CONFDIR}/app_status" | tr -cd 'a-z')
	print_json_head "utf-8"
	key_set=0; [ -n "${apikey}" ] && key_set=1
	printf '{"code":0,"msg":"OK","data":{"enable":%s,"apikey_set":%s,"ktype":"%s","service":"%s","level":"%s","precision":"%s","days":%s,"grpname":"%s","interval":%s,"autoupdate":%s,"app_status":"%s"}}' \
		"${enable:-0}" "${key_set}" "${ktype}" "${service}" "${level}" "${precision}" "${days:-3}" "${grpname}" "${interval:-1440}" "${autoupdate:-0}" "${st}"
	exit 0
}

# ============================================================
set_config()
{
	# Panabit httpd 把 GET/POST 参数注入为 CGI_<name> 环境变量
	c_apikey=$(san_token "${CGI_apikey}")
	# 空值表示“保持已有 Key”，避免前端为了展示配置而读取或回传密钥。
	if [ -z "${c_apikey}" ] && [ -f "${APPCONF}" ]; then
		c_apikey=$(grep '^apikey=' "${APPCONF}" 2>/dev/null | cut -d'=' -f2 | tr -cd 'A-Za-z0-9_-')
	fi
	c_grp=$(san_token "${CGI_grpname}")
	c_days=$(san_num "${CGI_days}");        [ "${c_days}" = "" ] && c_days=3
	[ ${c_days} -lt 1 ] && c_days=1; [ ${c_days} -gt 7 ] && c_days=7   # 2026-07-25:上限 30→7 天
	c_int=$(san_num "${CGI_interval}");     [ "${c_int}" = "" ] && c_int=1440
	[ ${c_int} -lt 5 ] && c_int=5
	c_type=$(in_set "${CGI_ktype}"     "all attacker scanner research_scanner asset_scanner" "all")
	c_svc=$(in_set  "${CGI_service}"   "all ssh rdp vnc db web smb telnet mail ics ai proxy"  "all")
	c_lvl=$(in_set  "${CGI_level}"     "all critical high medium" "all")
	c_pre=$(in_set  "${CGI_precision}" "standard high" "standard")
	c_en=$(in_set   "${CGI_enable}"    "0 1" "0")
	# autoupdate 没传时保留原值(旧页面缓存/其它调用路径不该把已开启的开关悄悄关掉)
	if [ "${CGI_autoupdate}" = "" ]; then
		c_au=$(grep '^autoupdate=' "${APPCONF}" 2>/dev/null | cut -d'=' -f2 | tr -cd '01')
		[ "${c_au}" = "" ] && c_au=0
	else
		c_au=$(in_set "${CGI_autoupdate}" "0 1" "0")
	fi
	[ "${c_grp}" = "" ] && c_grp="TrapCloud_block"

	cat > "${APPCONF}.tmp" << EOF
enable=${c_en}
apikey=${c_apikey}
ktype=${c_type}
service=${c_svc}
level=${c_lvl}
precision=${c_pre}
days=${c_days}
grpname=${c_grp}
interval=${c_int}
autoupdate=${c_au}
EOF
	mv -f "${APPCONF}.tmp" "${APPCONF}"

	# 刷新 cron(appctrl reload 读新配置重置定时)
	[ -f "${APPCTRL}" ] && sh "${APPCTRL}" reload >/dev/null 2>&1
	ujson '{"code":0,"msg":"OK","data":"saved"}'
}

# ============================================================
sync_now()
{
	[ ! -f "${SYNC_BIN}" ] && SYNC_BIN="${PGPATH}/app/${APPNAME}/bin/tcblock_sync"
	sh "${SYNC_BIN}" sync >/dev/null 2>&1
	rc=$?
	# 返回最新一条日志
	last=$(tail -1 "${LOGFILE}" 2>/dev/null | tr '"' "'")
	print_json_head "utf-8"
	if [ ${rc} -eq 0 ]; then
		printf '{"code":0,"msg":"OK","data":{"last":"%s"}}' "${last}"
	else
		printf '{"code":1,"msg":"sync_failed","data":{"last":"%s"}}' "${last}"
	fi
	exit 0
}

# ============================================================
get_status()
{
	# 🔴 sync.state 同样按键读取,不 source。它虽由本 APP 自己写,
	# 但只要有人能落地写这个文件,source 就等于给了一条 root 执行路径。
	# 顺带:所有值都做类型收敛,避免脏内容直接进 JSON 输出。
	state_get() { [ -f "${SYNCFILE}" ] && grep "^$1=" "${SYNCFILE}" 2>/dev/null | tail -1 | cut -d'=' -f2-; }
	grpname=$(state_get grpname | tr -cd 'A-Za-z0-9_-' | cut -c1-64)
	tid=$(state_get tid | tr -cd '0-9' | cut -c1-9)
	count=$(state_get count | tr -cd '0-9' | cut -c1-12);           [ -z "${count}" ] && count=0
	last_sync=$(state_get last_sync | tr -cd '0-9' | cut -c1-12);   [ -z "${last_sync}" ] && last_sync=0
	last_check=$(state_get last_check | tr -cd '0-9' | cut -c1-12); [ -z "${last_check}" ] && last_check=0
	status=$(state_get status | tr -cd 'a-z_');                     [ -z "${status}" ] && status="none"
	en=$(in_set "$(conf_get enable)" "0 1" "0")
	# 实时群组条数
	live=""
	if [ "${tid}" != "" ]; then
		live=$(${FLOWEYE} table get id=${tid} stat=1 2>/dev/null | grep -E '^ipv4cnt=' | head -1 | cut -d'=' -f2 | tr -cd '0-9')
	fi
	[ "${live}" = "" ] && live=${count}
	print_json_head "utf-8"
	printf '{"code":0,"msg":"OK","data":{"enable":%s,"grpname":"%s","tid":"%s","count":%s,"live":%s,"last_sync":%s,"last_check":%s,"status":"%s"}}' \
		"${en:-0}" "${grpname}" "${tid}" "${count:-0}" "${live:-0}" "${last_sync:-0}" "${last_check:-0}" "${status}"
	exit 0
}

# ============================================================
get_log()
{
	print_json_head "utf-8"
	printf '{"code":0,"msg":"OK","data":['
	if [ -f "${LOGFILE}" ]; then
		first=1
		tail -30 "${LOGFILE}" | while IFS='|' read t s m; do
			[ "${first}" = "0" ] && printf ','
			printf '{"time":"%s","status":"%s","msg":"%s"}' "${t}" "${s}" "$(echo "${m}" | tr '\\"' "/'")"
			first=0
		done
	fi
	printf ']}'
	exit 0
}

# ============================================================
test_connect()
{
	code=$(${CURL} -k -s -m 8 -o /dev/null -w "%{http_code}" "${BASE_URL}/api/stats" 2>/dev/null)
	print_json_head "utf-8"
	if [ "${code}" = "200" ]; then
		printf '{"code":0,"msg":"OK","data":{"http":"%s"}}' "${code}"
	else
		printf '{"code":1,"msg":"connect_fail:http=%s"}' "${code}"
	fi
	exit 0
}

# ============================================================
# 版本更新(2026-07-26)
# ============================================================
UPDSTATE="${CONFDIR}/update.state"
RAMBIN_UPD="/usr/ramdisk/app/tcblock/bin/tcblock_update"
PERSIST_UPD="/usr/panabit/app/tcblock/bin/tcblock_update"

get_update()
{
	# force=1 时先联网检查一次(点「检查更新」按钮),否则直接读每日 cron 写下的状态
	[ "${CGI_force}" = "1" ] && [ -x "${RAMBIN_UPD}" ] && sh "${RAMBIN_UPD}" check >/dev/null 2>&1

	lv=""; lt=""; hu=0; nt=""; ck=""; st="none"
	if [ -f "${UPDSTATE}" ]; then
		st=$(grep '^status=' "${UPDSTATE}" | cut -d'=' -f2)
		lv=$(grep '^local=' "${UPDSTATE}" | cut -d'=' -f2)
		lt=$(grep '^latest=' "${UPDSTATE}" | cut -d'=' -f2)
		hu=$(grep '^has_update=' "${UPDSTATE}" | cut -d'=' -f2 | tr -cd '01')
		nt=$(grep '^notes=' "${UPDSTATE}" | cut -d'=' -f2- | tr '"' "'")
		ck=$(grep '^checked_at=' "${UPDSTATE}" | cut -d'=' -f2 | tr -cd '0-9')
	fi
	[ "${lv}" = "" ] && lv=$(grep -a '^app_version=' /usr/ramdisk/app/tcblock/app.inf 2>/dev/null | cut -d'"' -f2)
	[ "${hu}" = "" ] && hu=0
	# 是否有更新任务正在跑
	running=0
	ps axw 2>/dev/null | grep "tcblock_update apply" | grep -v grep >/dev/null 2>&1 && running=1
	ulog=$(tail -1 "${CONFDIR}/update.log" 2>/dev/null | tr '"' "'")
	print_json_head "utf-8"
	printf '{"code":0,"msg":"OK","data":{"status":"%s","local":"%s","latest":"%s","has_update":%s,"notes":"%s","checked_at":"%s","running":%s,"last_log":"%s"}}' \
		"${st}" "${lv}" "${lt}" "${hu:-0}" "${nt}" "${ck}" "${running}" "${ulog}"
	exit 0
}

do_update()
{
	if [ ! -x "${PERSIST_UPD}" ]; then
		ujson '{"code":1,"msg":"UPDATER_MISSING"}'
	fi
	# 必须从持久化目录 detach 启动:appctrl stop 会 rm -rf ramdisk 运行区(含本 CGI),
	# 从 ramdisk 跑会把自己删掉导致更新中断。
	# 🔴 不能用 nohup:部分固件(实测 192.168.100.254)根本没有 nohup/setsid,
	#    点「更新」会返回 STARTED 但进程压根没起来 —— 静默失败。
	#    改用 subshell 忽略 HUP + exec,不依赖任何外部命令。
	( trap '' HUP; exec sh "${PERSIST_UPD}" apply ) >/dev/null 2>&1 &
	ujson '{"code":0,"msg":"STARTED"}'
}

case "${CGI_action}" in
"get_config")   get_config   ;;
"set_config")   set_config   ;;
"sync_now")     sync_now     ;;
"get_status")   get_status   ;;
"get_log")      get_log      ;;
"test_connect") test_connect ;;
"get_update")   get_update   ;;
"do_update")    do_update    ;;
*) ujson '{"code":1,"msg":"UNKNOWN_ACTION"}' ;;
esac
