router_local_update.sh 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391
  1. #!/bin/sh
  2. set -eu
  3. timestamp() {
  4. date '+%Y-%m-%d %H:%M:%S'
  5. }
  6. log() {
  7. printf '[%s] %s\n' "$(timestamp)" "$*"
  8. }
  9. log_err() {
  10. printf '[%s] %s\n' "$(timestamp)" "$*" >&2
  11. }
  12. SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
  13. if [ -f "$SCRIPT_DIR/../config_router.conf" ] || [ -f "$SCRIPT_DIR/../configs/config_router.conf" ]; then
  14. APP_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)
  15. else
  16. APP_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
  17. fi
  18. CONFIG_PATH=${1:-}
  19. if [ -z "$CONFIG_PATH" ]; then
  20. if [ -r "$APP_DIR/configs/config_router.conf" ]; then
  21. CONFIG_PATH="$APP_DIR/configs/config_router.conf"
  22. else
  23. CONFIG_PATH="$APP_DIR/config_router.conf"
  24. fi
  25. elif [ ! -r "$CONFIG_PATH" ] && [ -r "$APP_DIR/configs/$CONFIG_PATH" ]; then
  26. CONFIG_PATH="$APP_DIR/configs/$CONFIG_PATH"
  27. fi
  28. if [ ! -r "$CONFIG_PATH" ]; then
  29. log_err "[router-local] config not found: $CONFIG_PATH"
  30. exit 1
  31. fi
  32. # shellcheck disable=SC1090
  33. . "$CONFIG_PATH"
  34. CFST_WORK_DIR=${CFST_WORK_DIR:-"$APP_DIR/cfst"}
  35. CFST_BIN=${CFST_BIN:-"./cfst"}
  36. CFST_IP_FILE=${CFST_IP_FILE:-"ip.txt"}
  37. CFST_RESULT_FILE=${CFST_RESULT_FILE:-"result.csv"}
  38. CFST_DISPLAY_COUNT=${CFST_DISPLAY_COUNT:-10}
  39. CFST_THREADS=${CFST_THREADS:-}
  40. CFST_TEST_COUNT=${CFST_TEST_COUNT:-}
  41. CFST_DOWNLOAD_COUNT=${CFST_DOWNLOAD_COUNT:-}
  42. CFST_DOWNLOAD_TIME=${CFST_DOWNLOAD_TIME:-}
  43. CFST_PORT=${CFST_PORT:-443}
  44. CFST_URL=${CFST_URL:-"https://speed.cloudflare.com/__down?bytes=50000000"}
  45. CFST_HTTPING=${CFST_HTTPING:-0}
  46. CFST_HTTPING_CODE=${CFST_HTTPING_CODE:-}
  47. CFST_CFCOLO=${CFST_CFCOLO:-}
  48. CFST_LATENCY_LIMIT=${CFST_LATENCY_LIMIT:-}
  49. CFST_LATENCY_LOWER=${CFST_LATENCY_LOWER:-}
  50. CFST_LOSS_LIMIT=${CFST_LOSS_LIMIT:-}
  51. CFST_SPEED_LIMIT=${CFST_SPEED_LIMIT:-}
  52. CFST_DISABLE_DOWNLOAD=${CFST_DISABLE_DOWNLOAD:-0}
  53. CFST_ALL_IP=${CFST_ALL_IP:-0}
  54. CFST_DEBUG=${CFST_DEBUG:-0}
  55. CFST_SKIP_RUN=${CFST_SKIP_RUN:-0}
  56. CFST_QUIET_LOG=${CFST_QUIET_LOG:-1}
  57. CFST_SHOW_LOG_ON_TTY=${CFST_SHOW_LOG_ON_TTY:-1}
  58. TOP_N=${TOP_N:-3}
  59. RUNTIME_DIR=${RUNTIME_DIR:-"$APP_DIR/cfip_runtime"}
  60. VALUE_TEXT_FILE=${VALUE_TEXT_FILE:-"current_ip.txt"}
  61. VALUE_JSON_FILE=${VALUE_JSON_FILE:-"current_ip.json"}
  62. STATE_FILE=${STATE_FILE:-"state.json"}
  63. EXPORT_VARS_FILE=${EXPORT_VARS_FILE:-"substore_vars.json"}
  64. VALUE_JSON_KEY=${VALUE_JSON_KEY:-ip}
  65. STATE_LAST_GOOD_KEY=${STATE_LAST_GOOD_KEY:-last_good_ip}
  66. EXPORT_VALUE_KEY=${EXPORT_VALUE_KEY:-AUTO_CFIP}
  67. resolve_path() {
  68. _p="$1"
  69. _base_dir="$2"
  70. case "$_p" in
  71. /*) printf '%s\n' "$_p" ;;
  72. *) printf '%s/%s\n' "$_base_dir" "$_p" ;;
  73. esac
  74. }
  75. CURRENT_TEXT_PATH=$(resolve_path "$VALUE_TEXT_FILE" "$RUNTIME_DIR")
  76. CURRENT_JSON_PATH=$(resolve_path "$VALUE_JSON_FILE" "$RUNTIME_DIR")
  77. STATE_PATH=$(resolve_path "$STATE_FILE" "$RUNTIME_DIR")
  78. EXPORT_VARS_PATH=$(resolve_path "$EXPORT_VARS_FILE" "$RUNTIME_DIR")
  79. INDEX_PATH="$RUNTIME_DIR/index.html"
  80. RESULT_PATH=$(resolve_path "$CFST_RESULT_FILE" "$CFST_WORK_DIR")
  81. UPDATED_AT=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
  82. mkdir -p "$RUNTIME_DIR"
  83. json_get_string() {
  84. key="$1"
  85. file="$2"
  86. if [ ! -f "$file" ]; then
  87. return 0
  88. fi
  89. sed -n "s/.*\"$key\":[[:space:]]*\"\([^\"]*\)\".*/\1/p" "$file" | head -n 1
  90. }
  91. json_escape() {
  92. printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g; s/ /\\t/g' | tr -d '\n\r'
  93. }
  94. build_top_candidates_json() {
  95. awk -F',' -v top_n="$TOP_N" '
  96. function trim(v) {
  97. gsub(/\r/, "", v)
  98. sub(/^[[:space:]]+/, "", v)
  99. sub(/[[:space:]]+$/, "", v)
  100. return v
  101. }
  102. function esc(v) {
  103. gsub(/\\/,"\\\\",v)
  104. gsub(/"/,"\\\"",v)
  105. return v
  106. }
  107. NR == 1 { next }
  108. count >= top_n { exit }
  109. NF < 1 { next }
  110. { if (trim($1) == "") next }
  111. {
  112. ip = esc(trim($1))
  113. sent = esc(trim($2))
  114. received = esc(trim($3))
  115. loss = esc(trim($4))
  116. latency = esc(trim($5))
  117. speed = esc(trim($6))
  118. region = esc(trim($7))
  119. if (count > 0) {
  120. printf(",")
  121. }
  122. printf("{\"domain\":\"%s\",\"ip\":\"%s\",\"source_type\":\"cfst_local_busybox\",\"sent\":\"%s\",\"received\":\"%s\",\"loss_rate\":\"%s\",\"avg_latency\":\"%s\",\"download_speed\":\"%s\",\"region\":\"%s\",\"location_country\":\"\",\"location_city\":\"\",\"host_provider\":\"\",\"score_value\":null,\"scores\":[],\"created_raw\":\"\"}", ip, ip, sent, received, loss, latency, speed, region)
  123. count++
  124. }
  125. END {
  126. if (count == 0) {
  127. printf("")
  128. }
  129. }
  130. ' "$RESULT_PATH"
  131. }
  132. atomic_write_file() {
  133. _dest="$1"
  134. _tmp="${_dest}.tmp.$$"
  135. cat > "$_tmp" && mv -f "$_tmp" "$_dest"
  136. }
  137. write_text_value() {
  138. selected_ip="$1"
  139. _tmp_txt="${CURRENT_TEXT_PATH}.tmp.$$"
  140. printf '%s\n' "$selected_ip" > "$_tmp_txt" && mv -f "$_tmp_txt" "$CURRENT_TEXT_PATH"
  141. rm -f "$INDEX_PATH"
  142. cp "$CURRENT_TEXT_PATH" "$INDEX_PATH"
  143. }
  144. write_success_outputs() {
  145. selected_ip="$1"
  146. top_candidates_json="$2"
  147. source_count="$3"
  148. escaped_ip=$(json_escape "$selected_ip")
  149. write_text_value "$selected_ip"
  150. atomic_write_file "$CURRENT_JSON_PATH" <<EOF
  151. {
  152. "$VALUE_JSON_KEY": "$escaped_ip",
  153. "updated_at": "$UPDATED_AT",
  154. "status": "ok",
  155. "source_type": "cfst_local_busybox",
  156. "source_count": $source_count,
  157. "checked_count": 0,
  158. "top_candidates": [${top_candidates_json}]
  159. }
  160. EOF
  161. if [ "$selected_ip" != "${LAST_GOOD_IP:-}" ] || [ ! -f "$STATE_PATH" ] || [ "${LAST_STATE_STATUS:-}" != "ok" ]; then
  162. atomic_write_file "$STATE_PATH" <<EOF
  163. {
  164. "updated_at": "$UPDATED_AT",
  165. "$STATE_LAST_GOOD_KEY": "$escaped_ip",
  166. "status": "ok",
  167. "source_count": $source_count,
  168. "checked_count": 0,
  169. "source_type": "cfst_local_busybox"
  170. }
  171. EOF
  172. fi
  173. atomic_write_file "$EXPORT_VARS_PATH" <<EOF
  174. {
  175. "$EXPORT_VALUE_KEY": "$escaped_ip",
  176. "UPDATED_AT": "$UPDATED_AT",
  177. "STATUS": "ok"
  178. }
  179. EOF
  180. }
  181. write_error_with_fallback() {
  182. last_good_ip="$1"
  183. error_message="$2"
  184. escaped_ip=$(json_escape "$last_good_ip")
  185. escaped_error=$(json_escape "$error_message")
  186. write_text_value "$last_good_ip"
  187. atomic_write_file "$CURRENT_JSON_PATH" <<EOF
  188. {
  189. "$VALUE_JSON_KEY": "$escaped_ip",
  190. "updated_at": "$UPDATED_AT",
  191. "status": "error_use_last_good",
  192. "error": "$escaped_error",
  193. "source_type": "cfst_local_busybox"
  194. }
  195. EOF
  196. if [ "$last_good_ip" != "${LAST_GOOD_IP:-}" ] || [ ! -f "$STATE_PATH" ]; then
  197. atomic_write_file "$STATE_PATH" <<EOF
  198. {
  199. "updated_at": "$UPDATED_AT",
  200. "$STATE_LAST_GOOD_KEY": "$escaped_ip",
  201. "status": "error",
  202. "error": "$escaped_error",
  203. "source_type": "cfst_local_busybox"
  204. }
  205. EOF
  206. fi
  207. atomic_write_file "$EXPORT_VARS_PATH" <<EOF
  208. {
  209. "$EXPORT_VALUE_KEY": "$escaped_ip",
  210. "UPDATED_AT": "$UPDATED_AT",
  211. "STATUS": "error_use_last_good"
  212. }
  213. EOF
  214. }
  215. run_cfst() (
  216. cd "$CFST_WORK_DIR"
  217. set -- "$CFST_BIN" -f "$CFST_IP_FILE" -o "$CFST_RESULT_FILE" -p "$CFST_DISPLAY_COUNT" -tp "$CFST_PORT"
  218. if [ -n "$CFST_THREADS" ]; then
  219. set -- "$@" -n "$CFST_THREADS"
  220. fi
  221. if [ -n "$CFST_TEST_COUNT" ]; then
  222. set -- "$@" -t "$CFST_TEST_COUNT"
  223. fi
  224. if [ -n "$CFST_DOWNLOAD_COUNT" ]; then
  225. set -- "$@" -dn "$CFST_DOWNLOAD_COUNT"
  226. fi
  227. if [ -n "$CFST_DOWNLOAD_TIME" ]; then
  228. set -- "$@" -dt "$CFST_DOWNLOAD_TIME"
  229. fi
  230. if [ -n "$CFST_URL" ]; then
  231. set -- "$@" -url "$CFST_URL"
  232. fi
  233. if [ "$CFST_HTTPING" = "1" ]; then
  234. set -- "$@" -httping
  235. fi
  236. if [ -n "$CFST_HTTPING_CODE" ]; then
  237. set -- "$@" -httping-code "$CFST_HTTPING_CODE"
  238. fi
  239. if [ -n "$CFST_CFCOLO" ]; then
  240. set -- "$@" -cfcolo "$CFST_CFCOLO"
  241. fi
  242. if [ -n "$CFST_LATENCY_LIMIT" ]; then
  243. set -- "$@" -tl "$CFST_LATENCY_LIMIT"
  244. fi
  245. if [ -n "$CFST_LATENCY_LOWER" ]; then
  246. set -- "$@" -tll "$CFST_LATENCY_LOWER"
  247. fi
  248. if [ -n "$CFST_LOSS_LIMIT" ]; then
  249. set -- "$@" -tlr "$CFST_LOSS_LIMIT"
  250. fi
  251. if [ -n "$CFST_SPEED_LIMIT" ]; then
  252. set -- "$@" -sl "$CFST_SPEED_LIMIT"
  253. fi
  254. if [ "$CFST_DISABLE_DOWNLOAD" = "1" ]; then
  255. set -- "$@" -dd
  256. fi
  257. if [ "$CFST_ALL_IP" = "1" ]; then
  258. set -- "$@" -allip
  259. fi
  260. if [ "$CFST_DEBUG" = "1" ]; then
  261. set -- "$@" -debug
  262. fi
  263. # Show cfst live logs for manual interactive runs, keep quiet for redirected/background runs.
  264. is_interactive_tty=0
  265. if [ "$CFST_SHOW_LOG_ON_TTY" = "1" ] && [ -t 1 ]; then
  266. is_interactive_tty=1
  267. fi
  268. if [ "$CFST_QUIET_LOG" = "1" ] && [ "$CFST_DEBUG" != "1" ] && [ "$is_interactive_tty" != "1" ]; then
  269. "$@" >/dev/null 2>&1
  270. else
  271. "$@"
  272. fi
  273. )
  274. log_update_summary() {
  275. selected_ip="$1"
  276. source_count="$2"
  277. log "[router-local] update_at=$UPDATED_AT selected_ip=$selected_ip candidates=$source_count"
  278. awk -F',' -v top_n="$TOP_N" '
  279. function trim(v) {
  280. gsub(/\r/, "", v)
  281. sub(/^[[:space:]]+/, "", v)
  282. sub(/[[:space:]]+$/, "", v)
  283. return v
  284. }
  285. NR == 1 { next }
  286. count >= top_n { exit }
  287. NF < 1 { next }
  288. { if (trim($1) == "") next }
  289. {
  290. ip = trim($1)
  291. sent = trim($2)
  292. received = trim($3)
  293. loss = trim($4)
  294. latency = trim($5)
  295. speed = trim($6)
  296. region = trim($7)
  297. count++
  298. printf("[router-local] candidate_%d ip=%s sent=%s recv=%s loss=%s latency=%s speed=%s region=%s\n", count, ip, sent, received, loss, latency, speed, region)
  299. }
  300. ' "$RESULT_PATH"
  301. }
  302. LAST_GOOD_IP=$(json_get_string "$STATE_LAST_GOOD_KEY" "$STATE_PATH")
  303. LAST_STATE_STATUS=$(json_get_string "status" "$STATE_PATH")
  304. if [ "$CFST_SKIP_RUN" != "1" ]; then
  305. if ! run_cfst; then
  306. if [ -n "$LAST_GOOD_IP" ]; then
  307. write_error_with_fallback "$LAST_GOOD_IP" "cfst run failed"
  308. log "[router-local] cfst run failed, fallback to last good ip: $LAST_GOOD_IP"
  309. exit 0
  310. fi
  311. log_err "[router-local] cfst run failed and no last good ip available"
  312. exit 1
  313. fi
  314. fi
  315. if [ ! -s "$RESULT_PATH" ]; then
  316. if [ -n "$LAST_GOOD_IP" ]; then
  317. write_error_with_fallback "$LAST_GOOD_IP" "cfst result file missing or empty"
  318. log "[router-local] empty result, fallback to last good ip: $LAST_GOOD_IP"
  319. exit 0
  320. fi
  321. log_err "[router-local] result file missing or empty: $RESULT_PATH"
  322. exit 1
  323. fi
  324. if [ "${CFST_DISABLE_DOWNLOAD:-0}" = "1" ]; then
  325. BEST_LINE=$(sed -n '2p' "$RESULT_PATH" | tr -d '\r')
  326. else
  327. # 优先挑选下载测速大于 0 的最优行
  328. BEST_LINE=$(awk -F',' '
  329. function trim(v) { gsub(/\r/, "", v); sub(/^[[:space:]]+/, "", v); sub(/[[:space:]]+$/, "", v); return v }
  330. NR > 1 && NF >= 6 {
  331. speed = trim($6) + 0;
  332. if (speed > 0) { print $0; exit }
  333. }
  334. ' "$RESULT_PATH" | tr -d '\r')
  335. # 若所有候选均为 0.00 下载速度(测速失败)且有 last_good,置空以触发 fallback
  336. if [ -z "$BEST_LINE" ] && [ -n "$LAST_GOOD_IP" ]; then
  337. BEST_LINE=""
  338. elif [ -z "$BEST_LINE" ]; then
  339. BEST_LINE=$(sed -n '2p' "$RESULT_PATH" | tr -d '\r')
  340. fi
  341. fi
  342. BEST_IP=$(printf '%s' "$BEST_LINE" | cut -d',' -f1 | tr -d ' ')
  343. SOURCE_COUNT=$(awk -F',' 'NR > 1 && NF > 0 { count++ } END { print count + 0 }' "$RESULT_PATH")
  344. TOP_CANDIDATES_JSON=$(build_top_candidates_json)
  345. if [ -z "$BEST_IP" ]; then
  346. if [ -n "$LAST_GOOD_IP" ]; then
  347. write_error_with_fallback "$LAST_GOOD_IP" "no valid ip found in cfst result"
  348. log "[router-local] no valid ip found, fallback to last good ip: $LAST_GOOD_IP"
  349. exit 0
  350. fi
  351. log_err "[router-local] no valid ip found in result file"
  352. exit 1
  353. fi
  354. write_success_outputs "$BEST_IP" "$TOP_CANDIDATES_JSON" "$SOURCE_COUNT"
  355. log_update_summary "$BEST_IP" "$SOURCE_COUNT"