#!/usr/bin/env bash
# =============================================================================
# V-Connect PBX — Continuous SIP diagnostic capture
# =============================================================================
# Leaves a SAFE, size-capped capture running so an intermittent call failure can
# be caught without anyone watching the screen. When the failure happens, note
# the time and run:  sip-capture.sh read HH:MM
#
# It captures TWO complementary records:
#   1. PACKET capture (tcpdump, SIP signalling on udp/5060) — proves whether an
#      INVITE actually left the box / arrived from the provider. This is the
#      decisive test for an upstream NAT/router or SIP-ALG dropping calls while
#      Asterisk itself looks healthy.
#   2. Asterisk PJSIP logger into /var/log/asterisk/full — shows Asterisk's view
#      (Dial, NO ANSWER, Hangup) correlated by timestamp.
#
# SAFETY
#   * tcpdump rotates: 12 files x 50 MB = 600 MB hard ceiling, oldest reused.
#   * SIP signalling only (port 5060) — NOT RTP — so it stays small.
#   * Nothing here changes call routing. Read-only diagnostics.
#
# USAGE
#   sudo bash scripts/sip-capture.sh start      # begin capturing (persists)
#   sudo bash scripts/sip-capture.sh status     # is it running?
#   sudo bash scripts/sip-capture.sh read 09:15 # show SIP around 09:15 today
#   sudo bash scripts/sip-capture.sh read 09:15 0827855735   # filter a number
#   sudo bash scripts/sip-capture.sh stop       # stop capturing
# =============================================================================

set -uo pipefail

CAP_DIR="/var/log/sip-capture"
PCAP_GLOB="$CAP_DIR/sip-*.pcap"
PCAP_NAME="$CAP_DIR/sip-%Y%m%d-%H%M.pcap"   # strftime: ends in .pcap (AppArmor-safe)
RTP_GLOB="$CAP_DIR/rtp-*.pcap"
RTP_NAME="$CAP_DIR/rtp-%Y%m%d-%H%M.pcap"    # headers-only RTP flow capture
RTP_PID_FILE="/var/run/sip-capture-rtp.pid"
RTP_PORTRANGE="10000-20000"                 # Asterisk RTP media range
TCPDUMP_ERR="$CAP_DIR/tcpdump.err"
PID_FILE="/var/run/sip-capture.pid"
AST_LOG="/var/log/asterisk/full"
SIP_PORT="5060"

# Resolve binaries by absolute path (cron/limited-PATH safe).
TCPDUMP=""; for c in /usr/bin/tcpdump /usr/sbin/tcpdump "$(command -v tcpdump 2>/dev/null)"; do
    [[ -n "$c" && -x "$c" ]] && { TCPDUMP="$c"; break; }; done
AST=""; for c in /usr/sbin/asterisk /usr/bin/asterisk "$(command -v asterisk 2>/dev/null)"; do
    [[ -n "$c" && -x "$c" ]] && { AST="$c"; break; }; done

ACTION="${1:-status}"

start() {
    if [[ -z "$TCPDUMP" ]]; then
        echo "tcpdump not found — install it:  apt-get install -y tcpdump"
        exit 1
    fi
    mkdir -p "$CAP_DIR"

    # Already running?
    if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then
        echo "Capture already running (pid $(cat "$PID_FILE")). Use 'status' or 'stop'."
        return 0
    fi

    # Bound disk: drop capture files older than 3 days from previous runs.
    find "$CAP_DIR" -name 'sip-*.pcap' -mtime +3 -delete 2>/dev/null || true
    find "$CAP_DIR" -name 'rtp-*.pcap' -mtime +3 -delete 2>/dev/null || true

    # Time-rotated packet capture: a new file every hour, name ENDS in .pcap so
    # the tcpdump AppArmor profile permits the write (numbered -C files like
    # sip.pcap00 do NOT end in .pcap and get denied). SIP signalling only, so
    # each hourly file stays small. stderr is logged for diagnosis.
    setsid "$TCPDUMP" -n -i any -s 0 -U \
        -G 3600 -w "$PCAP_NAME" \
        "udp and port $SIP_PORT" >/dev/null 2>"$TCPDUMP_ERR" &
    echo $! > "$PID_FILE"
    disown 2>/dev/null || true

    # RTP media flow capture — HEADERS ONLY (snaplen 80 = IP+UDP+RTP header, no
    # audio payload) so it stays tiny while still proving whether audio packets
    # flow in BOTH directions during a call. This is what tells us if a call
    # that set up cleanly actually has two-way audio (vs dead air / one-way).
    setsid "$TCPDUMP" -n -i any -s 80 -U \
        -G 3600 -w "$RTP_NAME" \
        "udp portrange $RTP_PORTRANGE" >/dev/null 2>>"$TCPDUMP_ERR" &
    echo $! > "$RTP_PID_FILE"
    disown 2>/dev/null || true

    # Asterisk side: turn the SIP logger on so /var/log/asterisk/full carries the
    # SIP exchange with timestamps.
    [[ -n "$AST" ]] && "$AST" -rx "pjsip set logger on" >/dev/null 2>&1

    sleep 1
    if kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then
        echo "Capture STARTED."
        echo "  SIP     -> $CAP_DIR/sip-YYYYMMDD-HHMM.pcap  (udp/$SIP_PORT, hourly, 3-day retention)"
        echo "  RTP     -> $CAP_DIR/rtp-YYYYMMDD-HHMM.pcap  (media headers, udp/$RTP_PORTRANGE)"
        echo "  asterisk-> $AST_LOG  (pjsip logger ON)"
        echo "When a call fails, note the time and run:  sudo bash $0 read HH:MM"
    else
        echo "Failed to start tcpdump. Error was:"
        sed 's/^/    /' "$TCPDUMP_ERR" 2>/dev/null
        rm -f "$PID_FILE"
        exit 1
    fi
}

stop() {
    for pf in "$PID_FILE" "$RTP_PID_FILE"; do
        if [[ -f "$pf" ]]; then
            kill "$(cat "$pf" 2>/dev/null)" 2>/dev/null || true
            rm -f "$pf"
        fi
    done
    # Belt-and-braces: kill any stray capture writing to our files.
    pkill -f "tcpdump.*$CAP_DIR/sip-" 2>/dev/null || true
    pkill -f "tcpdump.*$CAP_DIR/rtp-" 2>/dev/null || true
    [[ -n "$AST" ]] && "$AST" -rx "pjsip set logger off" >/dev/null 2>&1
    echo "Capture STOPPED. Files kept in $CAP_DIR (delete when done)."
}

status() {
    if [[ -f "$PID_FILE" ]] && kill -0 "$(cat "$PID_FILE" 2>/dev/null)" 2>/dev/null; then
        echo "RUNNING (SIP pid $(cat "$PID_FILE")$( [[ -f "$RTP_PID_FILE" ]] && echo ", RTP pid $(cat "$RTP_PID_FILE")" ))."
        du -sh "$CAP_DIR" 2>/dev/null | awk '{print "  capture size: "$1}'
        ls -1 $PCAP_GLOB $RTP_GLOB 2>/dev/null | sed 's/^/  /'
    else
        echo "NOT running.  Start with:  sudo bash $0 start"
    fi
}

# read HH:MM [filter]  — show the SIP exchange around a time, from BOTH sources.
read_window() {
    local hhmm="${1:-}"; local filter="${2:-}"
    if [[ -z "$hhmm" ]]; then
        echo "Usage: sudo bash $0 read HH:MM [phone-number-or-ip]"; exit 1
    fi
    local today; today="$(date '+%Y-%m-%d')"
    # Build a minute window: the given minute plus the next one.
    local hh="${hhmm%%:*}"; local mm="${hhmm##*:}"
    echo "==================================================================="
    echo " ASTERISK SIP view around ${today} ${hhmm} (and the following minute)"
    echo "==================================================================="
    local re="$today ${hh}:${mm}|$today ${hh}:$(printf '%02d' $((10#$mm+1)))"
    if [[ -n "$filter" ]]; then
        grep -aE "$re" "$AST_LOG" 2>/dev/null | grep -aiE "INVITE|SIP/2.0 [1-6]|Transmitting SIP|Received SIP|Dial\(|NO ANSWER|CONGESTION|CHANUNAVAIL|Hangup|$filter" | grep -ai "$filter"
    else
        grep -aE "$re" "$AST_LOG" 2>/dev/null | grep -aiE "INVITE sip|SIP/2.0 [1-6][0-9][0-9]|Transmitting SIP request|Received SIP request|Dial\(|NO ANSWER|CONGESTION|CHANUNAVAIL|Hangup|retransmission|timer" | head -200
    fi
    echo
    echo "==================================================================="
    echo " PACKET view — SIP request/status lines seen on the wire"
    echo "   (src>dst tells you if the INVITE crossed the router)"
    echo "==================================================================="
    if [[ -n "$TCPDUMP" ]]; then
        for f in $PCAP_GLOB; do
            [[ -e "$f" ]] || continue
            "$TCPDUMP" -n -tttt -A -r "$f" "udp port $SIP_PORT" 2>/dev/null
        done | awk -v h="$hh" -v m="$mm" '
            /^[0-9]{4}-[0-9]{2}-[0-9]{2}/ { ts=$2 }   # capture HH:MM:SS.frac
            {
              # only print payload lines for SIP first-lines, with the last ts
              if ($0 ~ /(INVITE|REGISTER|OPTIONS|BYE|CANCEL|ACK) sip:/ || $0 ~ /SIP\/2\.0 [1-6][0-9][0-9]/) {
                  split(ts,a,":");
                  if (a[1]==h && (a[2]==m || a[2]==sprintf("%02d",m+1))) print ts"  "$0
              }
            }' | { [[ -n "$filter" ]] && grep -ai "$filter" || cat; } | head -200
    else
        echo "(tcpdump not available to read pcap)"
    fi

    echo
    echo "==================================================================="
    echo " RTP media flow — packet counts per stream around ${hhmm}"
    echo "   (two-way counts = audio OK; one side ~0 = one-way; both 0 = dead air)"
    echo "==================================================================="
    if [[ -n "$TCPDUMP" ]]; then
        local rtpfound=0
        for f in $RTP_GLOB; do
            [[ -e "$f" ]] || continue
            rtpfound=1
            "$TCPDUMP" -n -tttt -r "$f" "udp portrange $RTP_PORTRANGE" 2>/dev/null
        done | awk -v h="$hh" -v m="$mm" '
            {
              # tcpdump default line: "DATE HH:MM:SS.frac IP a.b.c.d.port > e.f.g.h.port: UDP..."
              ts=$2; split(ts,a,":");
              if (a[1]==h && (a[2]==m || a[2]==sprintf("%02d",m+1))) {
                  for (i=1;i<=NF;i++) if ($i==">") { src=$(i-1); dst=$(i+1); sub(/:$/,"",dst); flow=src" > "dst; cnt[flow]++ }
              }
            }
            END {
              if (length(cnt)==0) { print "  (no RTP packets in this window — no media flowed)"; }
              else for (f in cnt) printf "  %-45s %6d pkts\n", f, cnt[f]
            }' | { [[ -n "$filter" ]] && grep -ai "$filter" || sort -k1; }
        [[ "$rtpfound" -eq 0 ]] && echo "  (no rtp-*.pcap files yet — restart capture with this version)"
    else
        echo "(tcpdump not available to read pcap)"
    fi
}

case "$ACTION" in
    start)  start ;;
    stop)   stop ;;
    status) status ;;
    read)   read_window "${2:-}" "${3:-}" ;;
    *) echo "Usage: sudo bash $0 {start|stop|status|read HH:MM [filter]}"; exit 1 ;;
esac
