#!/bin/bash set -euo pipefail SCRIPT_VERSION="1.0.0" SCRIPT_NAME="dynamic-ip-observer" CONFIG_DIR="/etc/${SCRIPT_NAME}" CONFIG_FILE="${CONFIG_DIR}/config" DATA_DIR="/var/lib/${SCRIPT_NAME}" CACHE_FILE="${DATA_DIR}/current_ip.json" LOG_DIR="/var/log/${SCRIPT_NAME}" HISTORY_FILE="${LOG_DIR}/ip_history.log" INSTALL_PATH="/usr/local/bin/${SCRIPT_NAME}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" } error() { log "ERROR: $*" >&2 } load_config() { if [ ! -f "$CONFIG_FILE" ]; then error "Config file not found: ${CONFIG_FILE}" error "Run '${SCRIPT_NAME} install' first." exit 1 fi TELEGRAM_BOT_TOKEN="" TELEGRAM_CHAT_ID="" CHECK_INTERVAL="" NODE_NAME="" while IFS='=' read -r key value; do value="${value%\"}" value="${value#\"}" case "$key" in TELEGRAM_BOT_TOKEN) TELEGRAM_BOT_TOKEN="$value" ;; TELEGRAM_CHAT_ID) TELEGRAM_CHAT_ID="$value" ;; CHECK_INTERVAL) CHECK_INTERVAL="$value" ;; NODE_NAME) NODE_NAME="$value" ;; esac done < "$CONFIG_FILE" if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then error "Invalid config: missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID" exit 1 fi } json_extract() { local key="$1" json="$2" if command -v jq &>/dev/null; then printf '%s' "$json" | jq -r --arg k "$key" '.[$k] // empty' else printf '%s' "$json" | grep -o "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed 's/.*":\s*"//;s/"$//' fi } fetch_ip_info() { local retries=3 delay=10 attempt=0 response="" while [ $attempt -lt $retries ]; do response=$(curl -s --max-time 30 "https://ipinfo.io") && break attempt=$((attempt + 1)) [ $attempt -lt $retries ] && sleep $delay done if [ -z "$response" ]; then error "Failed to fetch IP info after ${retries} attempts" return 1 fi echo "$response" } send_telegram() { local message="$1" local url="https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" local result result=$(curl -s --max-time 30 -X POST "$url" \ --data-urlencode "chat_id=${TELEGRAM_CHAT_ID}" \ --data-urlencode "text=${message}" \ --data-urlencode "parse_mode=Markdown") || true if printf '%s' "$result" | grep -q '"ok":true'; then return 0 else error "Telegram send failed" return 1 fi } cmd_check() { load_config local ip_info new_ip old_ip="" ip_info=$(fetch_ip_info) || exit 1 new_ip=$(json_extract "ip" "$ip_info") if [ -z "$new_ip" ]; then error "Could not parse IP from response" exit 1 fi if [ -f "$CACHE_FILE" ]; then old_ip=$(json_extract "ip" "$(cat "$CACHE_FILE")") fi if [ "$new_ip" = "$old_ip" ]; then exit 0 fi echo "$ip_info" > "$CACHE_FILE" local hostname city country org timestamp hostname=$(json_extract "hostname" "$ip_info") city=$(json_extract "city" "$ip_info") country=$(json_extract "country" "$ip_info") org=$(json_extract "org" "$ip_info") timestamp=$(date '+%Y-%m-%d %H:%M:%S %Z') echo "${timestamp} | ${old_ip:-none} -> ${new_ip} | ${hostname} | ${city}, ${country}" >> "$HISTORY_FILE" local message if [ -z "$old_ip" ]; then message=$(cat < ${new_ip}" } cmd_status() { load_config echo "=== ${SCRIPT_NAME} status ===" if [ -f "$CACHE_FILE" ]; then local ip hostname last_check ip=$(json_extract "ip" "$(cat "$CACHE_FILE")") hostname=$(json_extract "hostname" "$(cat "$CACHE_FILE")") last_check=$(stat -c %y "$CACHE_FILE" 2>/dev/null || stat -f %Sm "$CACHE_FILE" 2>/dev/null) echo "Current IP: ${ip}" echo "Hostname: ${hostname}" echo "Last check: ${last_check}" else echo "No IP data cached yet." fi echo "" if command -v systemctl &>/dev/null && systemctl list-timers "${SCRIPT_NAME}.timer" &>/dev/null; then systemctl status "${SCRIPT_NAME}.timer" --no-pager 2>/dev/null || true elif crontab -l 2>/dev/null | grep -q "$SCRIPT_NAME"; then echo "Cron job: active" crontab -l 2>/dev/null | grep "$SCRIPT_NAME" else echo "Scheduler: not configured" fi } interval_to_cron() { local interval="$1" local num unit hours num=$(echo "$interval" | grep -o '[0-9]*') unit=$(echo "$interval" | grep -o '[a-z]*') case "$unit" in h) hours=$num ;; m) echo "*/${num} * * * *"; return ;; d) echo "0 0 */${num} * *"; return ;; *) hours=12 ;; esac if [ "$hours" -le 23 ]; then echo "0 */${hours} * * *" else echo "0 0 * * *" fi } setup_systemd() { local interval="$1" cat > "/etc/systemd/system/${SCRIPT_NAME}.service" < "/etc/systemd/system/${SCRIPT_NAME}.timer" </dev/null | grep -v "$SCRIPT_NAME"; echo "${cron_expr} ${INSTALL_PATH} check") | crontab - } cmd_install() { local token="" chat_id="" interval="12h" node_name="" # Check dependencies local missing=() if ! command -v curl &>/dev/null; then missing+=("curl"); fi if ! command -v grep &>/dev/null; then missing+=("grep"); fi if ! command -v sed &>/dev/null; then missing+=("sed"); fi if ! command -v systemctl &>/dev/null && ! command -v crontab &>/dev/null; then missing+=("cron (or systemd)") fi if [ ${#missing[@]} -gt 0 ]; then error "Missing dependencies: ${missing[*]}" error "Install them first, e.g.: apt install ${missing[*]} / apk add ${missing[*]}" exit 1 fi if ! command -v jq &>/dev/null; then log "WARN: jq not found, using grep fallback for JSON parsing. Install jq for better reliability." fi while [ $# -gt 0 ]; do case "$1" in --telegram-bot-token) [ -z "${2:-}" ] && { error "--telegram-bot-token requires a value"; exit 1; }; token="$2"; shift 2 ;; --telegram-chat-id) [ -z "${2:-}" ] && { error "--telegram-chat-id requires a value"; exit 1; }; chat_id="$2"; shift 2 ;; --interval) [ -z "${2:-}" ] && { error "--interval requires a value"; exit 1; }; interval="$2"; shift 2 ;; --node-name) [ -z "${2:-}" ] && { error "--node-name requires a value"; exit 1; }; node_name="$2"; shift 2 ;; *) error "Unknown option: $1"; exit 1 ;; esac done if [ -z "$token" ] || [ -z "$chat_id" ]; then error "Required: --telegram-bot-token and --telegram-chat-id" echo "Usage: ${SCRIPT_NAME} install --telegram-bot-token TOKEN --telegram-chat-id CHAT_ID [--interval 12h]" exit 1 fi if ! echo "$interval" | grep -qE '^[0-9]+(h|m|d)$'; then error "Invalid interval format: ${interval}. Use Nh, Nm, or Nd (e.g. 12h, 30m, 1d)" exit 1 fi if ! echo "$chat_id" | grep -qE '^-?[0-9]+$'; then error "Invalid chat_id: must be numeric" exit 1 fi mkdir -p "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR" chmod 700 "$CONFIG_DIR" chmod 750 "$DATA_DIR" chmod 750 "$LOG_DIR" if ! echo "$token" | grep -qE '^[0-9]+:[A-Za-z0-9_-]+$'; then error "Invalid bot token format. Expected: 123456:ABC-DEF..." exit 1 fi : "${node_name:=$(hostname)}" if ! printf '%s' "$node_name" | grep -qE '^[A-Za-z0-9 _.:-]+$'; then error "Invalid node-name: only alphanumeric, spaces, dots, hyphens, underscores, colons allowed" exit 1 fi printf 'TELEGRAM_BOT_TOKEN="%s"\n' "$token" > "$CONFIG_FILE" printf 'TELEGRAM_CHAT_ID="%s"\n' "$chat_id" >> "$CONFIG_FILE" printf 'CHECK_INTERVAL="%s"\n' "$interval" >> "$CONFIG_FILE" printf 'NODE_NAME="%s"\n' "$node_name" >> "$CONFIG_FILE" chmod 600 "$CONFIG_FILE" local script_url="https://raw.githubusercontent.com/GuangChen2333/DynamicIPObserver/main/dynamic-ip-observer.sh" if [ -f "$0" ] && [ "$0" != "bash" ] && [ "$0" != "-bash" ]; then cp "$(readlink -f "$0")" "$INSTALL_PATH" else curl -fsSL "$script_url" -o "$INSTALL_PATH" fi chmod +x "$INSTALL_PATH" if command -v systemctl &>/dev/null && systemctl --version &>/dev/null 2>&1; then setup_systemd "$interval" log "Installed with systemd timer (interval: ${interval})" else setup_cron "$interval" log "Installed with cron (interval: ${interval})" fi log "Running initial check..." cmd_check log "Installation complete." } cmd_uninstall() { if command -v systemctl &>/dev/null; then systemctl disable --now "${SCRIPT_NAME}.timer" 2>/dev/null || true rm -f "/etc/systemd/system/${SCRIPT_NAME}.service" rm -f "/etc/systemd/system/${SCRIPT_NAME}.timer" systemctl daemon-reload 2>/dev/null || true fi crontab -l 2>/dev/null | grep -v "$SCRIPT_NAME" | crontab - 2>/dev/null || true rm -rf "$CONFIG_DIR" "$DATA_DIR" "$LOG_DIR" rm -f "$INSTALL_PATH" log "Uninstalled ${SCRIPT_NAME}." } usage() { cat <