Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- """
- Domain Watch β Monitor de dominios en tiempo real (CTI / domain monitoring)
- ============================================================================
- Flujo simple
- ------------
- 1. Escribes los dominios base arriba (separados por comas: "breachforums, ejemplo").
- 2. BotΓ³n ESCANEAR: expande por TLD, sondea y pone en MONITOREO todo lo que
- responda 200 OK. El monitoreo se repite solo cada INTERVALO segundos.
- 3. BotΓ³n DETENER: pausa el monitoreo (no borra nada).
- 4. BotΓ³n BORRAR: limpia todo el estado y empieza de cero.
- Alertas (Telegram + interfaz)
- -----------------------------
- - π Un dominio monitoreado CAMBIA de DNS/IP.
- - π¨ Su DNS cae en la lista de "DNS a espiar" (sinkhole / incautaciΓ³n) o el
- contenido coincide con un patrΓ³n de takedown (FBI, Europol, "seized"...).
- - π Aparece un dominio nuevo que antes no resolvΓa.
- Uso
- ---
- pip install flask requests
- export TELEGRAM_TOKEN="123456:ABC..."
- export TELEGRAM_CHAT_ID="-100..."
- python3 domain_monitor.py
- # abrir http://127.0.0.1:5000
- """
- from __future__ import annotations
- import json
- import logging
- import os
- import re
- import socket
- import threading
- import time
- from concurrent.futures import ThreadPoolExecutor, as_completed
- from dataclasses import dataclass, asdict, field
- from datetime import datetime, timezone
- from hashlib import sha256
- from pathlib import Path
- from typing import Optional
- import requests
- from flask import Flask, jsonify, render_template_string, request
- # verify=False en probe genera warnings ruidosos; los silenciamos.
- requests.packages.urllib3.disable_warnings()
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # EDITABLE β ajusta a tu inteligencia
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # TLDs con los que se expande cada dominio base. Lista completa (ccTLDs + com).
- # Algunos cΓ³digos (bl, eh, um, bv, bq, sj...) no tienen DNS activo y nunca
- # resolverΓ‘n: no estorban, se descartan solos.
- TLDS = [
- "ac", "ad", "ae", "af", "ag", "ai", "al", "am", "ao", "aq", "ar", "as",
- "at", "au", "aw", "ax", "az", "ba", "bb", "bd", "be", "bf", "bg", "bh",
- "bi", "bj", "bm", "bn", "bo", "bq", "br", "bs", "bt", "bv", "bw", "by",
- "bz", "ca", "cc", "cd", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn",
- "co", "cr", "cu", "cv", "cw", "cx", "cy", "cz", "de", "dj", "dk", "dm",
- "do", "dz", "ec", "ee", "eg", "er", "es", "et", "eu", "fi", "fj", "fk",
- "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gg", "gh", "gi", "gl",
- "gm", "gn", "gp", "gq", "gr", "gs", "gt", "gu", "gw", "gy", "hk", "hm",
- "hn", "hr", "ht", "hu", "id", "ie", "il", "im", "in", "io", "iq", "ir",
- "is", "it", "je", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn",
- "kp", "kr", "kw", "ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls",
- "lt", "lu", "lv", "ly", "ma", "mc", "md", "me", "mf", "mg", "mh", "mk",
- "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mu", "mv", "mw",
- "mx", "my", "mz", "na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np",
- "nr", "nu", "nz", "om", "pa", "pe", "pf", "pg", "ph", "pk", "pl", "pm",
- "pn", "pr", "ps", "pt", "pw", "py", "qa", "re", "ro", "rs", "ru", "rw",
- "sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm",
- "sn", "so", "sr", "ss", "st", "su", "sv", "sx", "sy", "sz", "tc", "td",
- "tf", "tg", "th", "tj", "tk", "tl", "tm", "tn", "to", "tr", "tt", "tv",
- "tw", "tz", "ua", "ug", "uk", "us", "uy", "uz", "va", "vc", "ve", "vg",
- "vi", "vn", "vu", "wf", "ws", "ye", "yt", "za", "zm", "bl", "eh", "um",
- "com",
- ]
- # DNS / IP "a espiar": si un dominio resuelve a una de estas IPs (o su prefijo),
- # se marca como POSIBLE INCAUTACIΓN y alerta a Telegram de inmediato.
- # Sustituye por los rangos de sinkhole / takedown reales de tu CTI.
- SEIZURE_IP_PREFIXES = [
- "132.148.",
- "184.168.",
- "13.248.",
- "76.223.",
- ]
- # Palabras clave de pΓ‘ginas de takedown (2+ coincidencias = seizure).
- SEIZURE_KEYWORDS = [
- "this domain has been seized", "this website has been seized",
- "has been seized", "domain seizure", "seized by",
- "operation ", "task force",
- "federal bureau of investigation", "fbi",
- "europol", "eurojust", "national crime agency", "nca",
- "bundeskriminalamt", "politie", "department of justice",
- "u.s. attorney", "law enforcement", "cyber division",
- "incautad", "confiscad",
- ]
- DEFAULT_INTERVAL = 300 # seg entre escaneos automΓ‘ticos
- CONNECT_TIMEOUT = 6 # seg para conectar
- READ_TIMEOUT = 10 # seg para leer respuesta
- RETRIES = 2 # reintentos por dominio si falla la primera vez
- SCAN_THREADS = 20 # bajado de 30: menos saturaciΓ³n = menos timeouts falsos
- USER_AGENT = "Mozilla/5.0 (compatible; DomainWatch/1.0)"
- NOTIFY_ANY_DNS_CHANGE = True # avisar a Telegram tambiΓ©n en cambios de DNS normales
- TELEGRAM_TOKEN = os.getenv("TELEGRAM_TOKEN", "343123709:AAFtWMGNamKhY7ynNIH4uZk8TcBbyx897PC0")
- TELEGRAM_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID", "2145147107")
- STATE_FILE = Path("watch_state.json")
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- logging.basicConfig(
- level=logging.INFO,
- format="%(asctime)s | %(levelname)-7s | %(message)s",
- datefmt="%H:%M:%S",
- )
- log = logging.getLogger("watch")
- def _now() -> str:
- return datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S UTC")
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Modelo
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- @dataclass
- class DomainState:
- domain: str
- ip: Optional[str] = None
- status_code: Optional[int] = None
- title: Optional[str] = None
- body_hash: Optional[str] = None
- seized: bool = False
- ip_history: list = field(default_factory=list) # [{ts, ip}]
- first_seen: str = field(default_factory=_now)
- last_checked: str = field(default_factory=_now)
- last_change: Optional[str] = None
- error: Optional[str] = None
- @property
- def alive(self) -> bool:
- return self.status_code == 200
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Estado global
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- class Store:
- def __init__(self) -> None:
- self.lock = threading.RLock()
- self.domains: dict[str, DomainState] = {}
- self.base_domains: list[str] = []
- self.interval: int = DEFAULT_INTERVAL
- self.running: bool = False
- self.last_scan: Optional[str] = None
- self.events: list[dict] = []
- self._load()
- def _load(self) -> None:
- if not STATE_FILE.exists():
- return
- try:
- data = json.loads(STATE_FILE.read_text(encoding="utf-8"))
- self.base_domains = data.get("base_domains", [])
- self.interval = data.get("interval", DEFAULT_INTERVAL)
- for d in data.get("domains", []):
- self.domains[d["domain"]] = DomainState(**d)
- self.events = data.get("events", [])[-200:]
- except Exception as e:
- log.warning("No se pudo cargar estado: %s", e)
- def save(self) -> None:
- with self.lock:
- data = {
- "base_domains": self.base_domains,
- "interval": self.interval,
- "domains": [asdict(d) for d in self.domains.values()],
- "events": self.events[-200:],
- }
- STATE_FILE.write_text(json.dumps(data, indent=2, ensure_ascii=False),
- encoding="utf-8")
- def reset(self) -> None:
- with self.lock:
- self.domains.clear()
- self.events.clear()
- self.last_scan = None
- self.running = False
- if STATE_FILE.exists():
- STATE_FILE.unlink()
- self.add_event("info", "Estado borrado. Listo para empezar de cero.")
- def add_event(self, level: str, msg: str) -> None:
- with self.lock:
- self.events.append({"ts": _now(), "level": level, "msg": msg})
- self.events = self.events[-200:]
- log.info("[%s] %s", level.upper(), msg)
- store = Store()
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Telegram
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- def telegram_send(text: str) -> None:
- if not (TELEGRAM_TOKEN and TELEGRAM_CHAT_ID):
- log.warning("Telegram no configurado; alerta solo en interfaz.")
- return
- try:
- requests.post(
- f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
- json={"chat_id": TELEGRAM_CHAT_ID, "text": text,
- "parse_mode": "HTML", "disable_web_page_preview": True},
- timeout=10,
- )
- except requests.RequestException as e:
- log.warning("Fallo Telegram: %s", e)
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # DetecciΓ³n
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- def resolve(domain: str) -> Optional[str]:
- try:
- return socket.gethostbyname(domain)
- except socket.gaierror:
- return None
- def looks_seized(ip: Optional[str], html: str) -> bool:
- if ip and any(ip.startswith(p) for p in SEIZURE_IP_PREFIXES):
- return True
- low = html.lower()
- return sum(1 for kw in SEIZURE_KEYWORDS if kw in low) >= 2
- TITLE_RE = re.compile(r"<title[^>]*>(.*?)</title>", re.I | re.S)
- def _fetch(url: str) -> Optional[requests.Response]:
- """Un intento de GET. verify=False para no perder sitios con TLS roto
- (comΓΊn en dominios reciΓ©n levantados o incautados)."""
- return requests.get(
- url, headers={"User-Agent": USER_AGENT},
- timeout=(CONNECT_TIMEOUT, READ_TIMEOUT),
- allow_redirects=True, verify=False,
- )
- def probe(domain: str) -> DomainState:
- ip = resolve(domain)
- st = DomainState(domain=domain, ip=ip)
- if ip is None:
- return st
- r = None
- last_err = None
- # Reintenta HTTPS y, si falla, cae a HTTP. RETRIES intentos por esquema.
- for scheme in ("https", "http"):
- for attempt in range(RETRIES):
- try:
- r = _fetch(f"{scheme}://{domain}")
- break
- except requests.RequestException as e:
- last_err = type(e).__name__
- time.sleep(0.4 * (attempt + 1)) # pequeΓ±o backoff
- if r is not None:
- break
- if r is None:
- st.error = last_err or "no_response"
- return st
- st.status_code = r.status_code
- body = r.text[:200_000]
- st.body_hash = sha256(body.encode("utf-8", "ignore")).hexdigest()[:16]
- m = TITLE_RE.search(body)
- st.title = m.group(1).strip()[:200] if m else None
- st.seized = looks_seized(ip, body)
- return st
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Diff + alertas
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- def diff_and_alert(old: Optional[DomainState], new: DomainState) -> DomainState:
- # Dominio nuevo que aparece resolviendo
- if old is None:
- if new.ip:
- new.ip_history = [{"ts": _now(), "ip": new.ip}]
- store.add_event("new", f"π Nuevo dominio activo: {new.domain} β {new.ip}")
- telegram_send(
- f"π <b>Nuevo dominio activo</b>\n<code>{new.domain}</code>\n"
- f"IP: <code>{new.ip}</code>\nHTTP: {new.status_code}"
- )
- return new
- # conservar histΓ³rico
- new.first_seen = old.first_seen
- new.last_change = old.last_change
- new.ip_history = list(old.ip_history)
- # cambio de IP / DNS
- if old.ip and new.ip and old.ip != new.ip:
- new.last_change = _now()
- new.ip_history.append({"ts": _now(), "ip": new.ip})
- if new.seized and not old.seized:
- store.add_event("seizure",
- f"π¨ INCAUTACIΓN: {new.domain} {old.ip} β {new.ip}")
- telegram_send(
- f"π¨ <b>POSIBLE INCAUTACIΓN</b>\n<code>{new.domain}</code>\n"
- f"DNS: <code>{old.ip}</code> β <code>{new.ip}</code>\n"
- f"TΓtulo: {new.title or 'β'}\nCoincide con patrΓ³n de takedown."
- )
- else:
- store.add_event("dns",
- f"π Cambio DNS: {new.domain} {old.ip} β {new.ip}")
- if NOTIFY_ANY_DNS_CHANGE:
- telegram_send(
- f"π <b>Cambio de DNS</b>\n<code>{new.domain}</code>\n"
- f"<code>{old.ip}</code> β <code>{new.ip}</code>"
- )
- # se volviΓ³ seizure sin cambiar IP
- elif new.seized and not old.seized:
- new.last_change = _now()
- store.add_event("seizure", f"π¨ INCAUTACIΓN: {new.domain}")
- telegram_send(
- f"π¨ <b>POSIBLE INCAUTACIΓN</b>\n<code>{new.domain}</code>\n"
- f"El contenido coincide con un patrΓ³n de takedown."
- )
- return new
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Escaneo
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- def scan_once() -> None:
- with store.lock:
- bases = list(store.base_domains)
- if not bases:
- store.add_event("info", "No hay dominios base. Escribe alguno y vuelve a escanear.")
- return
- targets = [f"{b}.{t}" for b in bases for t in TLDS]
- # tambiΓ©n re-sondea los ya conocidos (por si cambian de DNS)
- with store.lock:
- targets += [d for d in store.domains if d not in targets]
- def run_batch(domains: list[str]) -> list[DomainState]:
- out: list[DomainState] = []
- with ThreadPoolExecutor(max_workers=SCAN_THREADS) as pool:
- futures = {pool.submit(probe, d): d for d in domains}
- for fut in as_completed(futures):
- try:
- out.append(fut.result())
- except Exception as e:
- log.warning("probe fallo: %s", e)
- return out
- log.info("Escaneando %d dominios (1Βͺ pasada)...", len(targets))
- results = run_batch(targets)
- # 2Βͺ pasada SOLO sobre los que resolvieron pero fallaron el HTTP
- # (timeouts, conexiones caΓdas). AsΓ no "aparecen" hasta el 2ΒΊ escaneo.
- retry = [r.domain for r in results if r.ip and r.status_code is None]
- if retry:
- log.info("Reintentando %d que fallaron el HTTP...", len(retry))
- redo = {r.domain: r for r in run_batch(retry)}
- results = [redo.get(r.domain, r) for r in results]
- log.info("Escaneo terminado: %d activos (200).",
- sum(1 for r in results if r.alive))
- with store.lock:
- for new in results:
- old = store.domains.get(new.domain)
- # Solo entran a monitoreo los que responden 200 (o los ya conocidos).
- if old is None and not new.alive:
- continue
- new.last_checked = _now()
- store.domains[new.domain] = diff_and_alert(old, new)
- store.last_scan = _now()
- store.save()
- def scan_loop() -> None:
- while True:
- with store.lock:
- running, interval = store.running, store.interval
- if running:
- try:
- scan_once()
- except Exception as e:
- log.exception("Error en escaneo: %s", e)
- time.sleep(max(5, interval) if running else 3)
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- # Interfaz web
- # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- app = Flask(__name__)
- PAGE = """<!doctype html>
- <html lang="es"><head>
- <meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
- <title>Domain Watch</title>
- <style>
- :root{--bg:#0d1117;--panel:#161b22;--border:#30363d;--fg:#e6edf3;
- --muted:#8b949e;--green:#238636;--greenL:#2ea043;--red:#f85149;
- --amber:#d29922;--blue:#388bfd;}
- *{box-sizing:border-box}
- body{margin:0;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;
- background:var(--bg);color:var(--fg);font-size:14px}
- header{background:var(--panel);border-bottom:1px solid var(--border);
- padding:14px 20px;display:flex;align-items:center;gap:14px;flex-wrap:wrap}
- h1{font-size:16px;margin:0;letter-spacing:.5px}
- .dot{width:9px;height:9px;border-radius:50%;display:inline-block}
- .on{background:var(--greenL);box-shadow:0 0 8px var(--greenL)}
- .off{background:var(--red)}
- .statuspill{font-weight:700;font-size:13px;padding:5px 14px;border-radius:20px;letter-spacing:.5px}
- .pill-on{background:rgba(46,160,67,.18);color:var(--greenL);border:1px solid var(--greenL)}
- .pill-off{background:rgba(248,81,73,.15);color:var(--red);border:1px solid var(--red)}
- .scanning{animation:pulse 1.2s infinite}
- @keyframes pulse{0%,100%{opacity:1}50%{opacity:.4}}
- main{padding:20px;max-width:1400px;margin:0 auto}
- .bar{background:var(--panel);border:1px solid var(--border);border-radius:10px;
- padding:16px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-bottom:20px}
- .bar input{flex:1;min-width:240px;background:var(--bg);color:var(--fg);
- border:1px solid var(--border);border-radius:6px;padding:10px;font-family:inherit}
- .bar input.small{flex:0 0 130px;min-width:100px}
- button{font-family:inherit;font-size:13px;border:0;border-radius:6px;
- padding:10px 18px;cursor:pointer;color:#fff}
- .scan{background:var(--green)} .scan:hover{background:var(--greenL)}
- .stop{background:var(--amber)} .clear{background:var(--red)}
- h2{font-size:13px;color:var(--muted);text-transform:uppercase;
- letter-spacing:1px;margin:24px 0 12px}
- .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
- .box{display:block;text-decoration:none;color:inherit;background:var(--panel);
- border:1px solid var(--border);border-radius:8px;padding:12px;transition:.15s}
- .box:hover{border-color:var(--blue)}
- .box.seized{border-color:var(--red);box-shadow:0 0 0 1px var(--red)}
- .box .d{font-weight:600;word-break:break-all}
- .box .ip{color:var(--blue);font-size:12px;margin-top:4px}
- .box .meta{color:var(--muted);font-size:11px;margin-top:6px;line-height:1.5}
- .tag{display:inline-block;font-size:10px;padding:2px 7px;border-radius:20px;margin-top:8px;margin-right:4px}
- .t200{background:rgba(46,160,67,.15);color:var(--greenL)}
- .tseiz{background:rgba(248,81,73,.15);color:var(--red)}
- .tchg{background:rgba(210,153,34,.15);color:var(--amber)}
- .feed{background:var(--panel);border:1px solid var(--border);border-radius:8px;
- padding:12px;max-height:320px;overflow:auto;font-size:12px;line-height:1.7}
- .feed .seizure{color:var(--red)} .feed .new{color:var(--greenL)}
- .feed .dns{color:var(--amber)} .feed .ts{color:var(--muted)}
- .empty{color:var(--muted);padding:20px;text-align:center}
- </style></head>
- <body>
- <header>
- <h1>DOMAIN WATCH</h1>
- <span class="statuspill pill-off" id="statuspill">DETENIDO</span>
- <span id="summary" style="color:var(--muted);font-size:12px"></span>
- <span style="margin-left:auto;color:var(--muted);font-size:12px" id="lastscan"></span>
- </header>
- <main>
- <div class="bar">
- <input type="text" id="bases" placeholder="dominios base separados por comas: breachforums, ejemplo">
- <input type="number" id="interval" class="small" min="5" value="300" title="Intervalo (seg)">
- <button class="scan" onclick="scan()">βΆ Escanear</button>
- <button class="stop" onclick="stop()">βΈ Detener</button>
- <button class="clear" onclick="clearAll()">π Borrar</button>
- <button style="background:var(--blue)" onclick="testTg()">β Probar Telegram</button>
- </div>
- <div id="hint" style="color:var(--muted);font-size:12px;margin:-10px 0 20px">
- Escribe dominios base y pulsa <b>Escanear</b> para empezar. VerΓ‘s arriba si estΓ‘
- <span style="color:var(--greenL)">MONITOREANDO</span> o
- <span style="color:var(--red)">DETENIDO</span>. Cada tarjeta es un enlace: Γ‘brela para confirmar.
- </div>
- <h2>Dominios en monitoreo (200 OK)</h2>
- <div class="grid" id="cards"></div>
- <h2>Feed de alertas</h2>
- <div class="feed" id="feed"></div>
- </main>
- <script>
- const api=(p,o)=>fetch(p,o).then(r=>r.json());
- const esc=s=>(s||'').replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c]));
- async function refresh(){
- const s=await api('/api/state');
- const pill=document.getElementById('statuspill');
- if(s.running){ pill.textContent='β MONITOREANDO'; pill.className='statuspill pill-on'; }
- else { pill.textContent='β DETENIDO'; pill.className='statuspill pill-off'; }
- document.getElementById('lastscan').textContent=s.last_scan?('Γltimo escaneo: '+s.last_scan):'Sin escaneos aΓΊn';
- const alive=s.domains.filter(d=>d.status_code===200);
- const seized=alive.filter(d=>d.seized).length;
- document.getElementById('summary').textContent=
- `${alive.length} en monitoreo Β· ${seized} incautados Β· cada ${s.interval}s`;
- const cards=document.getElementById('cards');
- cards.innerHTML = alive.length ? alive.map(d=>`
- <a class="box ${d.seized?'seized':''}" href="https://${esc(d.domain)}" target="_blank" rel="noopener">
- <div class="d">${esc(d.domain)} <span style="color:var(--muted);font-weight:400">β</span></div>
- <div class="ip">${esc(d.ip||'β')}</div>
- <div class="meta">
- HTTP ${d.status_code} Β· ${esc(d.title||'(sin tΓtulo)')}<br>
- visto: ${esc(d.first_seen)}<br>
- ${d.last_change?('ΓΊltimo cambio: '+esc(d.last_change)):''}
- ${d.ip_history&&d.ip_history.length>1?('<br>IPs vistas: '+d.ip_history.length):''}
- </div>
- <span class="tag t200">200 OK</span>
- ${d.last_change?'<span class="tag tchg">CAMBIΓ</span>':''}
- ${d.seized?'<span class="tag tseiz">INCAUTADO</span>':''}
- </a>`).join('')
- : '<div class="empty">Nada en monitoreo. Escribe dominios base y pulsa Escanear.</div>';
- document.getElementById('feed').innerHTML = s.events.slice().reverse().map(e=>
- `<div class="${e.level}"><span class="ts">${esc(e.ts)}</span> β ${esc(e.msg)}</div>`
- ).join('') || '<div class="empty">Sin eventos.</div>';
- }
- async function scan(){
- const bases=document.getElementById('bases').value;
- const interval=document.getElementById('interval').value;
- await api('/api/scan',{method:'POST',headers:{'Content-Type':'application/json'},
- body:JSON.stringify({bases,interval})});
- setTimeout(refresh,600);
- }
- async function stop(){ await api('/api/stop',{method:'POST'}); refresh(); }
- async function testTg(){
- const r=await api('/api/testtg',{method:'POST'});
- alert(r.ok ? 'β Mensaje enviado a Telegram. Revisa tu chat.'
- : 'β No se pudo enviar. Revisa TOKEN/CHAT_ID (mira el feed de alertas).');
- refresh();
- }
- async function clearAll(){
- if(!confirm('ΒΏBorrar todo el estado y empezar de cero?'))return;
- await api('/api/clear',{method:'POST'});
- document.getElementById('bases').value='';
- refresh();
- }
- (async()=>{
- const s=await api('/api/state');
- document.getElementById('bases').value=s.base_domains.join(', ');
- document.getElementById('interval').value=s.interval;
- refresh();
- })();
- setInterval(refresh,4000);
- </script>
- </body></html>"""
- @app.route("/")
- def index():
- return render_template_string(PAGE)
- @app.route("/api/state")
- def api_state():
- with store.lock:
- return jsonify({
- "running": store.running,
- "interval": store.interval,
- "last_scan": store.last_scan,
- "base_domains": store.base_domains,
- "domains": [asdict(d) for d in store.domains.values()],
- "events": store.events[-100:],
- })
- @app.route("/api/scan", methods=["POST"])
- def api_scan():
- data = request.get_json(force=True)
- bases = [b.strip().lower() for b in str(data.get("bases", "")).split(",") if b.strip()]
- try:
- interval = max(5, int(data.get("interval", DEFAULT_INTERVAL)))
- except (TypeError, ValueError):
- interval = DEFAULT_INTERVAL
- with store.lock:
- store.base_domains = bases
- store.interval = interval
- store.running = True
- store.add_event("info", f"Escaneo iniciado: {len(bases)} base(s), cada {interval}s")
- threading.Thread(target=scan_once, daemon=True).start()
- return jsonify({"ok": True})
- @app.route("/api/testtg", methods=["POST"])
- def api_testtg():
- if not (TELEGRAM_TOKEN and TELEGRAM_CHAT_ID):
- store.add_event("info", "β Telegram NO configurado (falta TOKEN o CHAT_ID).")
- return jsonify({"ok": False, "reason": "no_config"})
- try:
- r = requests.post(
- f"https://api.telegram.org/bot{TELEGRAM_TOKEN}/sendMessage",
- json={"chat_id": TELEGRAM_CHAT_ID,
- "text": "β Domain Watch: prueba de conexiΓ³n OK."},
- timeout=10,
- )
- ok = r.status_code == 200 and r.json().get("ok")
- store.add_event("info",
- "β Telegram OK: mensaje enviado." if ok
- else f"β Telegram respondiΓ³ error: {r.text[:120]}")
- return jsonify({"ok": bool(ok)})
- except requests.RequestException as e:
- store.add_event("info", f"β Telegram fallΓ³: {e}")
- return jsonify({"ok": False, "reason": str(e)})
- @app.route("/api/stop", methods=["POST"])
- def api_stop():
- with store.lock:
- store.running = False
- store.add_event("info", "Monitoreo detenido.")
- return jsonify({"ok": True})
- @app.route("/api/clear", methods=["POST"])
- def api_clear():
- store.base_domains = []
- store.reset()
- return jsonify({"ok": True})
- def main() -> None:
- threading.Thread(target=scan_loop, daemon=True).start()
- log.info("Interfaz en http://127.0.0.1:5000")
- if not (TELEGRAM_TOKEN and TELEGRAM_CHAT_ID):
- log.warning("TELEGRAM_TOKEN / TELEGRAM_CHAT_ID no definidos: alertas solo en interfaz.")
- app.run(host="0.0.0.0", port=5000, debug=False, use_reloader=False)
- if __name__ == "__main__":
- main()
Advertisement
Add Comment
Please, Sign In to add comment