diff --git a/macos/lib/logging.sh b/macos/lib/logging.sh index 4abb92c..ef5411d 100755 --- a/macos/lib/logging.sh +++ b/macos/lib/logging.sh @@ -47,10 +47,10 @@ log_ping() { log_drop() { local start="$1" end="$2" target="$3" diagnosis="$4" - local now_s end_s dur - now_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$end" +%s 2>/dev/null || date +%s) - end_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$start" +%s 2>/dev/null || date +%s) - dur=$(awk "BEGIN {printf \"%.2f\", $now_s - $end_s}") + local start_s end_s dur + start_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$start" +%s 2>/dev/null || date +%s) + end_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$end" +%s 2>/dev/null || date +%s) + dur=$(awk "BEGIN {printf \"%.2f\", $end_s - $start_s}") if awk "BEGIN {exit !($dur < 0)}"; then dur="0"; fi drop_starts+=("$start") @@ -67,10 +67,10 @@ log_drop() { log_threshold_breach() { local start="$1" end="$2" avg_lat="$3" - local now_s end_s dur - now_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$end" +%s 2>/dev/null || date +%s) - end_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$start" +%s 2>/dev/null || date +%s) - dur=$(awk "BEGIN {printf \"%.2f\", $now_s - $end_s}") + local start_s end_s dur + start_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$start" +%s 2>/dev/null || date +%s) + end_s=$(date -j -f "%Y-%m-%d %H:%M:%S" "$end" +%s 2>/dev/null || date +%s) + dur=$(awk "BEGIN {printf \"%.2f\", $end_s - $start_s}") if awk "BEGIN {exit !($dur < 0)}"; then dur="0"; fi breach_starts+=("$start") diff --git a/macos/lib/network.sh b/macos/lib/network.sh index 99e2872..4b1a19c 100755 --- a/macos/lib/network.sh +++ b/macos/lib/network.sh @@ -78,7 +78,7 @@ get_local_ip() { # ================================================================ detect_public_ip() { local resp - resp=$(curl -s --connect-timeout 5 --max-time 8 "http://ip-api.com/json" 2>/dev/null) + resp=$(curl -s --connect-timeout 5 --max-time 8 "https://ip-api.com/json" 2>/dev/null) if [[ -n "$resp" ]]; then public_ip=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('query','N/A'))" 2>/dev/null) isp_name=$(echo "$resp" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('isp','N/A'))" 2>/dev/null) diff --git a/python/connectivity_monitor/config.py b/python/connectivity_monitor/config.py index c6b5aed..54a9cbc 100644 --- a/python/connectivity_monitor/config.py +++ b/python/connectivity_monitor/config.py @@ -58,19 +58,30 @@ def save_config(cfg): def prompt_default(prompt, default): - """Prompt user with a default value.""" + """Prompt user with a default value. Returns the user's input or the default.""" val = input("{} [{}]: ".format(prompt, default)).strip() return val if val else str(default) def prompt_yes_no(prompt, default="Y"): - """Prompt user for yes/no.""" + """Prompt user for yes/no. Returns True for yes, False for no.""" val = input("{} [{}]: ".format(prompt, default)).strip() if not val: val = default return val.upper().startswith("Y") +def _prompt_int(prompt, default): + """Prompt user for an integer, re-prompting on invalid input.""" + while True: + raw = input("{} [{}]: ".format(prompt, default)).strip() + val = raw if raw else str(default) + try: + return int(val) + except ValueError: + print(" Invalid value '{}' — please enter a whole number.".format(val)) + + def interactive_setup(saved_cfg=None): """Run interactive configuration and return config dict.""" print() @@ -97,24 +108,19 @@ def interactive_setup(saved_cfg=None): return cfg cfg = dict(DEFAULTS) - poll = prompt_default(" Poll interval (seconds)", cfg["poll"]) - cfg["poll"] = int(poll) - - threshold = prompt_default(" Failure threshold for drop", cfg["threshold"]) - cfg["threshold"] = int(threshold) + cfg["poll"] = _prompt_int(" Poll interval (seconds)", cfg["poll"]) + cfg["threshold"] = _prompt_int(" Failure threshold for drop", cfg["threshold"]) targets = prompt_default(" Ping targets (comma-sep)", cfg["targets"]) cfg["targets"] = targets - lat_warn = prompt_default(" Latency warning (ms)", cfg["lat_warn"]) - cfg["lat_warn"] = int(lat_warn) + cfg["lat_warn"] = _prompt_int(" Latency warning (ms)", cfg["lat_warn"]) cfg["enable_dns"] = prompt_yes_no(" Enable DNS health check?", "Y") if cfg["enable_dns"]: cfg["dns_target"] = prompt_default(" DNS test hostname", cfg["dns_target"]) - web_port = prompt_default(" Web dashboard port", cfg["web_port"]) - cfg["web_port"] = int(web_port) + cfg["web_port"] = _prompt_int(" Web dashboard port", cfg["web_port"]) save_config(cfg) print(" Config saved to {}".format(get_config_path())) @@ -130,9 +136,9 @@ def headless_config(args): if args.targets: cfg["targets"] = args.targets - if args.poll: + if args.poll is not None: cfg["poll"] = args.poll - if args.threshold: + if args.threshold is not None: cfg["threshold"] = args.threshold if args.web_port is not None: cfg["web_port"] = args.web_port diff --git a/python/connectivity_monitor/html_report.py b/python/connectivity_monitor/html_report.py index ecdebad..251dc50 100644 --- a/python/connectivity_monitor/html_report.py +++ b/python/connectivity_monitor/html_report.py @@ -4,6 +4,7 @@ """ import datetime +import html import os from . import metrics @@ -141,7 +142,11 @@ def generate_html_report(state, report_file): report_gen_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") total_lost = state.total_pings - state.total_success - html = _build_html( + # Escape external/user-supplied strings before injecting into HTML + safe_isp = html.escape(str(state.isp_name)) + safe_public_ip = html.escape(str(state.public_ip)) + + html_out = _build_html( labels_js=labels_js, data_js=data_js, gw_js=gw_js, hist_js=hist_js, target_rows=target_rows, drops_table=drops_table, breach_table=breach_table, heatmap_html=heatmap_html, report_date=report_date, @@ -152,14 +157,14 @@ def generate_html_report(state, report_file): total_downtime=total_downtime, baseline_text=baseline_text, health_css=health_css, uptime_css=uptime_css, avg_css=avg_css, loss_css=loss_css, jitter_css=jitter_css, drops_css=drops_css, - drops_count=len(state.drops), isp=state.isp_name, public_ip=state.public_ip, + drops_count=len(state.drops), isp=safe_isp, public_ip=safe_public_ip, ) d = os.path.dirname(report_file) if d: os.makedirs(d, exist_ok=True) with open(report_file, "w", encoding="utf-8") as f: - f.write(html) + f.write(html_out) def _score_color(value, thresholds, default): @@ -190,7 +195,7 @@ def _build_target_rows(state): rows += ( "