#!/bin/sh
#
# tcblock_net - 下载节点多地址容错(2026-09-24)
#   被 tcblock_sync / tcblock_update / ajax_tcblock(测试连接)共用,由调用方 source。
#   调用方须先设置:CURL、CONFDIR;可选 NET_TMP(探测临时文件目录,默认 /tmp)。
#
# 背景:原来只连 dl.trapcloud.cn:64005 一个地址。它是 IDC 三线 BGP(电信/联通/移动各一个 IP),
#   客户现场只要"本机 DNS 解析到的那条线路"走不通(跨网阻断、出口中间设备代答握手后吞数据),
#   同步和版本检查就永远 curl_err:ret=28 且无告警(诸暨教育网 08-21 → 09-24 断供一个多月;
#   该现场 TCP Ping 64005 是通的,但请求从未到达 IDC —— 只测握手的连通性检查会误判)。
#
# 做法:按 [上次成功的节点] → 本机DNS → 电信 → 联通 → 移动 → 主站 www(仅名单下载有此路由)依次尝试。
#   - 每个节点先做一次小探测(拉小文件,TLS 完整握手+收数据),通过才发正式请求;
#     坏节点几秒就跳过,不会每个都等满正式超时。
#   - 正式请求只要拿到 HTTP 响应(哪怕 401/403)就当作服务端的答复,不再换节点 ——
#     换节点改变不了鉴权/配额结果,还会重复计配额。
#   - 正式请求最多发 TCB_MAX_FETCH 次(探测通过但正式请求传输中断才会换节点)。
#

TCB_DL_HOST="dl.trapcloud.cn"
TCB_DL_PORT="64005"
TCB_DL_BASE="https://${TCB_DL_HOST}:${TCB_DL_PORT}"
TCB_WWW_BASE="https://www.trapcloud.cn"
TCB_EPSTATE="${CONFDIR}/endpoint.state"   # 上次成功的节点名,下次优先
TCB_PROBE_TIMEOUT=8
TCB_MAX_FETCH=2

# 节点名:dns=本机解析  ct/cu/cm=IDC 电信/联通/移动线路 IP(--resolve 钉定,SNI/Host 仍是域名)  www=主站
ep_ip()
{
	case "$1" in
		ct) echo "103.85.171.10" ;;
		cu) echo "114.247.187.38" ;;
		cm) echo "112.54.132.50" ;;
	esac
}

# 界面/日志只显示中性代号,不暴露运营商线路与兜底通道(用户 2026-09-24 要求)。
# 对内对照:线路1=电信 线路2=联通 线路3=移动 备用=主站 www
ep_label()
{
	case "$1" in
		dns) echo "默认" ;;
		ct)  echo "线路1" ;;
		cu)  echo "线路2" ;;
		cm)  echo "线路3" ;;
		www) echo "备用" ;;
		*)   echo "$1" ;;
	esac
}

ep_base()
{
	if [ "$1" = "www" ]; then echo "${TCB_WWW_BASE}"; else echo "${TCB_DL_BASE}"; fi
}

# pacu 各固件版本不同:--resolve / --connect-timeout 不支持时不能硬传(会直接 ret=2 全军覆没)
_net_opts_init()
{
	[ "${_NET_INIT}" = "1" ] && return 0
	_NET_INIT=1
	_net_probe_gc
	_nh=$( { ${CURL} --help; ${CURL} --help all; } 2>&1 )
	NET_HAS_RESOLVE=0
	echo "${_nh}" | grep -q -- '--resolve' && NET_HAS_RESOLVE=1
	NET_CT=""
	echo "${_nh}" | grep -q -- '--connect-timeout' && NET_CT="--connect-timeout 5"
	return 0
}

# ep_curl <节点> <输出文件> <超时秒> <路径含查询串>;stdout=HTTP 码,返回值=curl 退出码
ep_curl()
{
	_ip=$(ep_ip "$1")
	if [ -n "${_ip}" ]; then
		${CURL} -k -s ${NET_CT} -m "$3" --resolve "${TCB_DL_HOST}:${TCB_DL_PORT}:${_ip}" \
			-o "$2" -w "%{http_code}" "$(ep_base "$1")$4" 2>/dev/null
	else
		${CURL} -k -s ${NET_CT} -m "$3" -o "$2" -w "%{http_code}" "$(ep_base "$1")$4" 2>/dev/null
	fi
}

# ep_list <允许www 0|1>:输出尝试顺序(上次成功的节点排最前)
ep_list()
{
	_all="dns"
	[ "${NET_HAS_RESOLVE}" = "1" ] && _all="${_all} ct cu cm"
	[ "$1" = "1" ] && _all="${_all} www"
	_last=$(cat "${TCB_EPSTATE}" 2>/dev/null | head -1 | tr -cd 'a-z' | cut -c1-4)
	_first=""
	for _e in ${_all}; do [ "${_e}" = "${_last}" ] && _first="${_e}"; done
	_out="${_first}"
	for _e in ${_all}; do [ "${_e}" != "${_first}" ] && _out="${_out} ${_e}"; done
	echo ${_out}
}

# 清理残留探测文件(2026-09-29)
# 探测文件按进程号命名(tcb_probe.<pid>),探测完即删;但进程若在探测途中被杀
# (典型:页面「测试连接」的 CGI 被 httpd 超时杀掉),文件就永远留下,且名字不重复、不会被覆盖。
# /tmp 落在磁盘上的机型(诸暨)重启也不清 → 只增不减。
# 每个进程首次联网前扫一遍:属主进程已不存在的一律删掉;还活着的(别的进程正在探测)不碰。
_net_probe_gc()
{
	for _gf in "${NET_TMP:-/tmp}"/tcb_probe.*; do
		[ -f "${_gf}" ] || continue
		_gp="${_gf##*.}"
		case "${_gp}" in ''|*[!0-9]*) continue ;; esac
		[ "${_gp}" = "$$" ] && continue
		kill -0 "${_gp}" 2>/dev/null || rm -f "${_gf}" 2>/dev/null
	done
}

# ep_probe <节点>:0=可用。结果摘要留在 NET_PROBE_RES(curl退出码:HTTP码)
ep_probe()
{
	_net_opts_init
	_pf="${NET_TMP:-/tmp}/tcb_probe.$$"
	if [ "$1" = "www" ]; then _pp="/api/stats"; else _pp="/panabit/latest.json"; fi
	_pc=$(ep_curl "$1" "${_pf}" "${TCB_PROBE_TIMEOUT}" "${_pp}")
	_pr=$?
	rm -f "${_pf}"
	NET_PROBE_RES="${_pr}:${_pc}"
	[ "${_pr}" = "0" ] && [ "${_pc}" = "200" ]
}

# net_fetch <路径含查询串> <输出文件> <超时秒> <允许www 0|1>
#   返回 0 = 拿到了 HTTP 响应(是否 200 由调用方看 NET_HTTP);1 = 所有节点都连不上
#   输出变量:NET_HTTP  NET_RET(curl 退出码)  NET_EP(实际使用的节点)  NET_TRIED(失败节点摘要)
net_fetch()
{
	_net_opts_init
	NET_HTTP="000"; NET_RET=1; NET_EP=""; NET_TRIED=""
	_nf=0
	for _e in $(ep_list "$4"); do
		if ! ep_probe "${_e}"; then
			NET_RET="${NET_PROBE_RES%%:*}"; [ "${NET_RET}" = "0" ] && NET_RET=1   # 探测 HTTP 非 200 也算不通
			NET_TRIED="${NET_TRIED}${NET_TRIED:+,}$(ep_label "${_e}"):${NET_PROBE_RES}"
			continue
		fi
		[ ${_nf} -ge ${TCB_MAX_FETCH} ] && break
		_nf=$((_nf + 1))
		NET_HTTP=$(ep_curl "${_e}" "$2" "$3" "$1")
		NET_RET=$?
		if [ "${NET_RET}" = "0" ]; then
			NET_EP="${_e}"
			# 备用通道不记为"上次成功":否则下载节点恢复后仍一直先走主站,占主站带宽
			if [ "${_e}" != "www" ]; then
				echo "${_e}" > "${TCB_EPSTATE}" 2>/dev/null
			else
				rm -f "${TCB_EPSTATE}" 2>/dev/null
			fi
			return 0
		fi
		NET_TRIED="${NET_TRIED}${NET_TRIED:+,}$(ep_label "${_e}"):${NET_RET}:${NET_HTTP}"
		rm -f "$2"
	done
	return 1
}
