diff --git a/app/app.py b/app/app.py index 9f2c608..38bbb0b 100644 --- a/app/app.py +++ b/app/app.py @@ -731,6 +731,36 @@ def _lookup_ip_asn(ip: str) -> dict: return result +def _batch_lookup_ip_asn(ips: list) -> dict: + """Single SQL query for all IPs in cache. Falls back to individual lookup for misses (capped at 20).""" + if not ips: + return {} + unique = list(dict.fromkeys(ips)) + result: dict = {} + + with _pg() as conn: + if conn: + try: + with conn.cursor() as cur: + cur.execute( + "SELECT ip, asn, name, country FROM ip_asn_cache " + "WHERE ip = ANY(%s) AND fetched_at > now() - interval '30 days'", + (unique,) + ) + for row in cur.fetchall(): + result[row[0]] = {"asn": row[1], "name": row[2], "country": row[3]} + except Exception: + try: conn.rollback() + except: pass + + misses = [ip for ip in unique if ip not in result] + for ip in misses[:20]: + result[ip] = _lookup_ip_asn(ip) + for ip in misses[20:]: + result.setdefault(ip, {"asn": "", "name": "", "country": ""}) + + return result + def _lookup_as_prefixes(asn: str) -> list: """Returns IPv4 CIDRs for an ASN via RIPE Stat. Cached 30 days.""" _AS_CACHE_DIR.mkdir(exist_ok=True) @@ -1034,27 +1064,16 @@ def errors_404(): entries = sorted(filtered.items(), key=lambda x: x[1]["count"], reverse=True) ignored = _load_ignored_ips() - # ── Onglet Bannissements ── + # Count only — ASN lookup is deferred to /errors/banned-groups (AJAX) banned_ips, banned_nets = _get_banned_ips() - all_banned = sorted(banned_ips) + sorted(str(n) for n in banned_nets) - by_asn: dict = {} - for b_entry in all_banned: - rep_ip = b_entry.split("/")[0] - info = _lookup_ip_asn(rep_ip) - key = info.get("asn") or "?" - if key not in by_asn: - by_asn[key] = {"asn": info.get("asn", ""), "name": info.get("name", ""), - "country": info.get("country", ""), "entries": []} - by_asn[key]["entries"].append(b_entry) - banned_groups = sorted(by_asn.values(), key=lambda g: len(g["entries"]), reverse=True) + banned_total = len(banned_ips) + len(banned_nets) return render_template("errors_404.html", entries=entries, ignored_ips=sorted(ignored), total=sum(v["count"] for v in filtered.values()), log_configured=bool(STATS_LOG_FILE), - banned_groups=banned_groups, - banned_total=len(all_banned), + banned_total=banned_total, ) @@ -1162,6 +1181,31 @@ def errors_banned(): return redirect(url_for('errors_404') + '#banned') +@app.route("/errors/banned-groups") +def errors_banned_groups(): + redir = _require_admin() + if redir: + return jsonify({"error": "not authorized"}), 403 + + banned_ips, banned_nets = _get_banned_ips() + all_banned = sorted(banned_ips) + sorted(str(n) for n in banned_nets) + + rep_ips = [b.split("/")[0] for b in all_banned] + asn_map = _batch_lookup_ip_asn(rep_ips) + + by_asn: dict = {} + for b_entry, rep_ip in zip(all_banned, rep_ips): + info = asn_map.get(rep_ip, {}) + key = info.get("asn") or "?" + if key not in by_asn: + by_asn[key] = {"asn": info.get("asn", ""), "name": info.get("name", ""), + "country": info.get("country", ""), "entries": []} + by_asn[key]["entries"].append(b_entry) + + groups = sorted(by_asn.values(), key=lambda g: len(g["entries"]), reverse=True) + return jsonify({"groups": groups, "total": len(all_banned)}) + + @app.route("/errors/unban", methods=["POST"]) def errors_unban(): global _BANNED_CACHE_TS diff --git a/app/templates/errors_404.html b/app/templates/errors_404.html index 7875762..920c17d 100644 --- a/app/templates/errors_404.html +++ b/app/templates/errors_404.html @@ -81,56 +81,29 @@ {% endif %} {# /tab-errors #} - {# ─────────────── Onglet Bannissements ─────────────── #} + {# ─────────────── Onglet Bannissements (chargé en AJAX) ─────────────── #}