From 02800ef496fa475aeb9253ad2c984e7f91dcee2c Mon Sep 17 00:00:00 2001 From: DAProgs Date: Tue, 10 Mar 2026 18:37:47 -0400 Subject: [PATCH] adding country codes --- portspoof_py/admin.py | 29 +++++++++++++++++++++++------ portspoof_py/geo.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 portspoof_py/geo.py diff --git a/portspoof_py/admin.py b/portspoof_py/admin.py index 884c3ed..9d8e29c 100644 --- a/portspoof_py/admin.py +++ b/portspoof_py/admin.py @@ -19,6 +19,7 @@ from typing import Optional, Tuple from urllib.parse import parse_qs, urlparse from .config import Config +from .geo import country as _geo_country from .notifier import Notifier from .stats import Stats @@ -303,6 +304,7 @@ tr:hover td { background: #1c2128; } .hex { color: #a5d6ff; font-size: 11px; max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.cc { color: #8b949e; font-size: 11px; } .badge { display: inline-block; background: #21262d; border-radius: 3px; padding: 1px 7px; font-size: 11px; @@ -452,12 +454,29 @@ def _empty_row(cols: int, msg: str) -> str: return f'{msg}' -def _render_dashboard(stats: Stats, cfg: Config) -> str: +async def _render_dashboard(stats: Stats, cfg: Config) -> str: # ── top IPs ── top_ips = stats.top_ips() + + # ── recent connections ── + recent = stats.recent_connections(50) + + # Prefetch geo for all unique IPs in one batch + unique_ips = {ip for ip, _ in top_ips} | {e['src_ip'] for e in recent} + if unique_ips: + await asyncio.gather(*(_geo_country(ip) for ip in unique_ips)) + + def _ip_cell(ip: str) -> str: + from .geo import _cache as _geo_cache + code = _geo_cache.get(ip, '') + esc = html.escape(ip) + if code: + return f'{esc} ({html.escape(code)})' + return f'{esc}' + ip_rows = ( ''.join( - f'{html.escape(ip)}{c}' + f'{_ip_cell(ip)}{c}' for ip, c in top_ips ) if top_ips else _empty_row(2, 'no data yet') ) @@ -474,13 +493,11 @@ def _render_dashboard(stats: Stats, cfg: Config) -> str: # ── CPM chart ── cpm_chart = _render_cpm_chart(stats.cpm_history(60)) - # ── recent connections ── - recent = stats.recent_connections(50) conn_rows = ( ''.join( '' f'{html.escape(e["timestamp"][:19].replace("T", " "))}' - f'{html.escape(e["src_ip"])}' + f'{_ip_cell(e["src_ip"])}' f'{e["src_port"]}' f'{e["dst_port"]}' f'' @@ -637,7 +654,7 @@ async def _handle( return elif path == '/': - body = _render_dashboard(stats, cfg) + body = await _render_dashboard(stats, cfg) ct = 'text/html; charset=utf-8' elif path == '/api/stats': diff --git a/portspoof_py/geo.py b/portspoof_py/geo.py new file mode 100644 index 0000000..4953998 --- /dev/null +++ b/portspoof_py/geo.py @@ -0,0 +1,29 @@ +"""IP geolocation — async lookup with in-memory cache. Zero external deps.""" +import asyncio +import urllib.request + +_cache: dict[str, str] = {} +_sem = asyncio.Semaphore(5) + + +async def country(ip: str) -> str: + """Return two-letter country code for *ip*, or '' on error/unknown.""" + if ip in _cache: + return _cache[ip] + async with _sem: + if ip in _cache: + return _cache[ip] + + def _fetch() -> str: + url = f'https://www.daprogs.com/ip/?raw=1&ip={ip}' + with urllib.request.urlopen(url, timeout=3) as r: + return r.read().decode('ascii', errors='replace') + + try: + text = await asyncio.to_thread(_fetch) + parts = text.split('|') + code = parts[3].strip() if len(parts) > 3 else '' + except Exception: + code = '' + _cache[ip] = code + return code