#!/bin/sh
# Custom PC Republic dial-home collector.
# Health + conntrack five-tuples. No DNS log, no pcap, no URL list.
# A reply with {"op":"sysupgrade","id":"..."} runs `owut upgrade --force`
# once per command id, and only if owut is installed.
# Needs: uclient-fetch (default on LuCI images), optional owut, optional jsonfilter.

ONCE=0
[ "$1" = "--once" ] && ONCE=1

cfg() { uci -q get "cpr_dialhome.main.$1"; }

json_escape() {
	printf '%s' "$1" | sed 's/\\/\\\\/g; s/"/\\"/g'
}

sample_flows() {
	limit="$(cfg flow_sample)"
	limit="${limit:-8}"
	[ "$limit" = "0" ] && return 0
	[ -r /proc/net/nf_conntrack ] || return 0
	# First src/dst/dport on a row is the original direction. Best-effort.
	awk -v n="$limit" '
		$0 ~ /src=/ && $0 ~ /dst=/ && count < n {
			proto="ip"; src=""; dst=""; dport=""; pkts=""
			for (i = 1; i <= NF; i++) {
				if ($i ~ /^tcp$/ || $i ~ /^udp$/) proto=$i
				else if ($i ~ /^src=/ && src == "") { split($i, a, "="); src=a[2] }
				else if ($i ~ /^dst=/ && dst == "") { split($i, a, "="); dst=a[2] }
				else if ($i ~ /^dport=/ && dport == "") { split($i, a, "="); dport=a[2] }
				else if ($i ~ /^packets=/ && pkts == "") { split($i, a, "="); pkts=a[2] }
			}
			if (src != "" && dst != "" && dport != "") {
				if (count++) printf ","
				gsub(/[^0-9A-Za-z.:]/, "", src)
				gsub(/[^0-9A-Za-z.:]/, "", dst)
				gsub(/[^0-9]/, "", dport)
				gsub(/[^0-9]/, "", pkts)
				printf "{\"proto\":\"%s\",\"src\":\"%s\",\"dst\":\"%s\",\"dport\":%s,\"packets\":%s}", proto, src, dst, dport+0, pkts+0
			}
		}
	' /proc/net/nf_conntrack
}

build_body() {
	host="$(uci -q get system.@system[0].hostname)"
	host="$(json_escape "${host:-OpenWrt}")"
	rel="$(json_escape "$(. /etc/openwrt_release 2>/dev/null; printf '%s' "${DISTRIB_RELEASE:-unknown}")")"
	model="$(json_escape "$(jsonfilter -e '@.model' /etc/board.json 2>/dev/null || cat /tmp/sysinfo/model 2>/dev/null)")"
	read -r l1 l5 l15 _ < /proc/loadavg
	mem_total="$(awk '/MemTotal/ {print $2}' /proc/meminfo)"
	mem_avail="$(awk '/MemAvailable/ {print $2}' /proc/meminfo)"
	mem_pct=0
	if [ -n "$mem_total" ] && [ "$mem_total" -gt 0 ]; then
		mem_pct=$(( (mem_total - ${mem_avail:-0}) * 100 / mem_total ))
	fi
	ct=0
	ctmax=0
	[ -r /proc/net/nf_conntrack ] && ct="$(wc -l < /proc/net/nf_conntrack | tr -d ' ')"
	[ -r /proc/sys/net/netfilter/nf_conntrack_max ] && ctmax="$(tr -d ' ' < /proc/sys/net/netfilter/nf_conntrack_max)"
	up="$(cut -d. -f1 /proc/uptime)"
	flows="$(sample_flows)"
	printf '{"hostname":"%s","release":"%s","model":"%s","uptime":%s,"load":[%s,%s,%s],"mem_used_pct":%s,"conntrack":{"count":%s,"max":%s},"flows":[%s]}\n' \
		"$host" "$rel" "$model" "${up:-0}" "$l1" "$l5" "$l15" "$mem_pct" "${ct:-0}" "${ctmax:-0}" "$flows"
}

apply_reply() {
	reply="$1"
	printf '%s' "$reply" | grep -q '"op":"sysupgrade"' || return 0
	cmd_id="$(printf '%s' "$reply" | sed -n 's/.*"id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)"
	if [ -n "$cmd_id" ] && [ -f /tmp/cpr-last-cmd ] && [ "$(cat /tmp/cpr-last-cmd)" = "$cmd_id" ]; then
		return 0
	fi
	if command -v owut >/dev/null 2>&1; then
		logger -t cpr-dialhome "attended sysupgrade requested${cmd_id:+ id=$cmd_id}"
		[ -n "$cmd_id" ] && printf '%s\n' "$cmd_id" > /tmp/cpr-last-cmd
		# stdin closed so a prompt cannot stall the heartbeat loop
		owut upgrade --force </dev/null || logger -t cpr-dialhome "owut upgrade failed"
	else
		logger -t cpr-dialhome "sysupgrade command ignored: owut is not installed"
		echo "sysupgrade queued but owut is not installed" >&2
	fi
}

post_once() {
	enabled="$(cfg enabled)"
	url="$(cfg endpoint)"
	token="$(cfg token)"
	if [ "$enabled" != "1" ] || [ -z "$url" ]; then
		echo "dial-home disabled or endpoint empty" >&2
		return 1
	fi
	body="$(build_body)"
	reply="$(uclient-fetch -q -O - \
		--header "Content-Type: application/json" \
		--header "Authorization: Bearer ${token}" \
		--post-data "$body" \
		"$url" 2>/dev/null)" || {
		echo "heartbeat failed" >&2
		return 1
	}
	printf '%s\n' "$reply"
	apply_reply "$reply"
}

if [ "$ONCE" = "1" ]; then
	post_once
	exit $?
fi

while true; do
	interval="$(cfg interval)"
	interval="${interval:-60}"
	case "$interval" in
		''|*[!0-9]*) interval=60 ;;
	esac
	[ "$interval" -lt 30 ] && interval=30
	post_once >/dev/null 2>&1
	sleep "$interval"
done
