diff --git a/README.md b/README.md index 3505106..e709578 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,37 @@ python -m omni pump thehackernews,cvefeed --limit 200 --apply - путь `syndication.twitter.com` (429 для всех IP, включая VPS и прокси) и nitter-зеркала (410/403/0) выпилены — не тратить время. - свой хендл добавляется как источник: `{"name": "x_ник", "category": "abuse", "kind": "tweet", "url": "https://x.com/ник", "adapter": "x_user", "extra": {"handle": "ник"}}`. +## Ключи парсеров: один пул на весь комбайн + +Ключи не лежат в коде и не уходят в git — только локально и в env. + +- `python tools/build_vault.py` — собирает пул из `MASTER_KEYS.csv` и локальных дампов в `data/vault.json` (файл в `.gitignore`, значения никогда не печатаются). +- `python -m omni keys` / `python -m omni keys --service firecrawl -v` — сводка: сервис, сколько ключей, сколько готовых, маски. +- `python tools/vault_check.py` — живая проверка: помечает `live / dead / rate / nofund`. +- Рантайм `omni/vault.py`: сначала env (`FIRECRAWL_API_KEY`, `TAVILY_API_KEY`, …), потом пул; ротация по кругу; остывание ключа после 401/402/403 — час, после 429 — 10 минут. Один мёртвый ключ не ломает прогон. +- Провайдеры поиска/скрейпа (`firecrawl`, `tavily`, `exa`, `serper`, `jina`, `apify`) берут ключ из пула автоматически — отдельная настройка не нужна. +- Панель: `GET /api/keys` — сводка по сервисам без значений, строка «N ключей · M сервисов» в сайдбаре. +- В пуле сейчас 66 сервисов и ~9.9k записей; из парсерных: `firecrawl` 70 (живые, поиск проверен), `2captcha` 36, `anticaptcha` 15, `capsolver` 1, `telegram` 77 ботов; ключи `openai/deepseek/openrouter` идут на перевод и саммари. +- Капча для закрытых площадок: `2captcha`/`anticaptcha`/`capsolver` из того же пула. + +## abuse-radar внутрь реестра + +Радар (домены + каналы + посты) больше не отдельная система: + +- `python -m omni radar` — dry-run: сколько доменов с каким score и сколько живых каналов. +- `python -m omni radar --apply --min-score 20` — пишет `data/radar_sources.json`: домены становятся HTML-источниками, каналы — Telegram-источниками (`tg_web`). +- `python -m omni radar --import-posts` — заливает посты каналов радара в общую базу как обычные записи (типы, score, фильтры, бот работают сразу). +- Источники радара помечены `opt_in`: в обычном обходе их нет, чтобы 2.5k новых источников не тормозили ленту. Берутся явно: `omni feed --tags radar` или `--category abuse`. +- Дефолтный путь к базе радара: `C:/Users/User/tmp/abuse_radar/radar_server.db`, переопределяется `--db`. + +## Китайские и дарк-форумы + +- `python tools/wave_cn.py [--apply]` — проверяет живые китайские/азиатские площадки и генерирует `omni/sources_cn.py`. Живых 9: 52pojie, nodeseek, kanxue, anquanke, 4hou, xz.aliyun, ithome, cnbeta, v2ex. +- `omni/sources_dark.py` — дарк/крэк-форумы, реально отдающие темы без логина: `cracked.st`, `nulledbb.com`, `forum.tuts4you.com`, `leak.sx` (плюс существующий набор `ab_*`). +- Для площадок под JS-челленджем: `python tools/cf_cookies.py [host …]` — открывает сайт настоящим браузером (Camoufox), ждёт прохода проверки, снимает cookies в `data/cookies.json` (вне git), после чего `omni/net.py` подставляет их сам. Проверка: `python tools/cf_cookies.py --check`. +- `net.get(..., proxy=…)` — принудительный прокси для отдельного источника (например CF-форумы через чистый DE-выход). +- Честное ограничение: `xss.is`, `exploit.in` (форум), `blackhatworld`, `hackforums` отдают контент только аккаунту — cookie-прохода мало, нужен логин. + ## Автоаудит и непрерывные коммиты - `python tools/selfcheck.py` — компиляция пакета, счётчики базы, распределение типов, топ источников, поиск дублей и пустых источников; пишет блок в `REPORT.md` и историю в `data/selfcheck.jsonl`. diff --git a/omni/abuse_sources.py b/omni/abuse_sources.py index df30bc9..6365661 100644 --- a/omni/abuse_sources.py +++ b/omni/abuse_sources.py @@ -5,9 +5,10 @@ """ -def A(name, category, kind, url, adapter, tags=(), limit=20): +def A(name, category, kind, url, adapter, tags=(), limit=20, extra=None): return {"name": name, "category": category, "kind": kind, "url": url, - "adapter": adapter, "tags": tuple(tags), "limit": limit, "detail": False, "extra": {}} + "adapter": adapter, "tags": tuple(tags), "limit": limit, + "detail": False, "extra": dict(extra or {})} ABUSE = [ @@ -61,8 +62,10 @@ ABUSE = [ A("ab_carder_su", "abuse", "html", "https://carder.su/", "html_listing", ('abuse', 'abuse'), limit=20), A("ab_procarders_com", "abuse", "html", "https://procarders.com/", "html_listing", ('abuse', 'abuse'), limit=20), A("ab_darkpro_net", "abuse", "html", "https://darkpro.net/", "html_listing", ('abuse', 'abuse'), limit=20), - A("ab_tuts4you_com", "abuse", "html", "https://tuts4you.com/", "xenforo_latest", ('abuse', 'abuse'), limit=20), - A("ab_forum_tuts4you_com", "abuse", "html", "https://forum.tuts4you.com/", "xenforo_latest", ('abuse', 'abuse'), limit=20), + A("ab_tuts4you_com", "abuse", "html", "https://tuts4you.com/", "html_listing", ('abuse', 'abuse'), limit=20, + extra={"include_contains": ["/topic/"]}), + A("ab_forum_tuts4you_com", "abuse", "html", "https://forum.tuts4you.com/", "html_listing", ('abuse', 'abuse'), limit=20, + extra={"include_contains": ["/topic/"]}), A("ab_forum_exetools_com", "abuse", "html", "https://forum.exetools.com/", "html_listing", ('abuse', 'abuse'), limit=20), A("ab_tlgrm_eu", "abuse", "html", "https://tlgrm.eu/channels", "html_listing", ('abuse', 'abuse'), limit=20), A("ab_telemetr_me", "abuse", "html", "https://telemetr.me/channels", "html_listing", ('abuse', 'abuse'), limit=20), diff --git a/omni/config.py b/omni/config.py index 03f5ae0..c0b6608 100644 --- a/omni/config.py +++ b/omni/config.py @@ -69,6 +69,15 @@ def load(): except Exception: pass CONFIG = cfg + # cookies, снятые браузером (tools/cf_cookies.py) — файл вне git + ck_file = DATA / "cookies.json" + if ck_file.exists(): + try: + extra = json.loads(ck_file.read_text(encoding="utf-8")) + if isinstance(extra, dict): + cfg["cookies"] = {**extra, **(cfg.get("cookies") or {})} + except Exception: + pass raw = os.environ.get("OMNI_COOKIES") or "" if raw: try: diff --git a/omni/feeds.py b/omni/feeds.py index f7cc570..df125a6 100644 --- a/omni/feeds.py +++ b/omni/feeds.py @@ -432,7 +432,7 @@ def _html_listing(src, limit): continue if inc and not any(path.startswith(p) for p in inc): continue - if inc_c and not any(c in path for c in inc_c): + if inc_c and not any(c in href for c in inc_c): continue if href in seen: continue diff --git a/omni/sources.py b/omni/sources.py index f99ef0b..edaa0cf 100644 --- a/omni/sources.py +++ b/omni/sources.py @@ -245,18 +245,19 @@ def radar_sources(): return rows -def _cn_sources(): - """Китайские/азиатские источники из сгенерированного omni/sources_cn.py.""" - try: - from .sources_cn import CN - except Exception: - return [] +def _gen_sources(): + """Сгенерированные наборы: китайские (sources_cn) + дарк-форумы (sources_dark).""" out = [] - for d in CN: + for mod, attr in (("sources_cn", "CN"), ("sources_dark", "DARK")): try: - out.append(Source(**d)) + rows = getattr(__import__("omni." + mod, fromlist=[attr]), attr) except Exception: continue + for d in rows: + try: + out.append(Source(**d)) + except Exception: + continue return out @@ -271,7 +272,7 @@ def all_sources(): ("name", "category", "kind", "url", "adapter", "tags", "limit", "detail", "extra")} extra.append(Source(**d)) seen, out = set(), [] - for s in SOURCES + _cn_sources() + extra + radar_sources(): + for s in SOURCES + _gen_sources() + extra + radar_sources(): key = (s.url or "").rstrip("/").lower() if key and key in seen: continue diff --git a/omni/sources_abuse.py b/omni/sources_abuse.py index 826cb6e..e22ccc4 100644 --- a/omni/sources_abuse.py +++ b/omni/sources_abuse.py @@ -5,9 +5,9 @@ """ -def A(name, category, kind, url, adapter, tags=(), limit=20): +def A(name, category, kind, url, adapter, tags=(), limit=20, extra=None): return {"name": name, "category": category, "kind": kind, "url": url, - "adapter": adapter, "tags": tuple(tags), "limit": limit, "detail": False, "extra": {}} + "adapter": adapter, "tags": tuple(tags), "limit": limit, "detail": False, "extra": dict(extra or {})} ABUSE_LIVE = [ @@ -18,8 +18,10 @@ ABUSE_LIVE = [ A("ab_blackhatworld_com", "abuse", "html", "https://www.blackhatworld.com/seo/up-to-date-list-of-specialized-software-for-telegram.1642970/", "xenforo_latest", ('abuse', 'abuse'), limit=20), A("ab_cracked_io", "abuse", "html", "https://cracked.io/", "xenforo_latest", ('abuse', 'abuse'), limit=20), A("ab_t_me", "abuse", "html", "https://t.me/s/Abuz_ai_soft", "tg_web", ('abuse', 'abuse'), limit=20), - A("ab_tuts4you_com", "abuse", "html", "https://tuts4you.com/", "xenforo_latest", ('abuse', 'abuse'), limit=20), - A("ab_forum_tuts4you_com", "abuse", "html", "https://forum.tuts4you.com/", "xenforo_latest", ('abuse', 'abuse'), limit=20), + A("ab_tuts4you_com", "abuse", "html", "https://tuts4you.com/", "html_listing", ('abuse', 'abuse'), limit=20, + extra={"include_contains": ["/topic/"]}), + A("ab_forum_tuts4you_com", "abuse", "html", "https://forum.tuts4you.com/", "html_listing", ('abuse', 'abuse'), limit=20, + extra={"include_contains": ["/topic/"]}), A("ab_nodeloc_com", "abuse", "json", "https://www.nodeloc.com/latest.json", "discourse", ('abuse', 'abuse'), limit=20), A("ab_v2ex_com", "abuse", "json", "https://www.v2ex.com/api/topics/hot.json", "generic_json", ('abuse', 'abuse'), limit=20), A("ab_hostloc_com", "abuse", "rss", "https://hostloc.com/forum.php?mod=rss", "rss", ('abuse', 'abuse'), limit=20), diff --git a/omni/sources_dark.py b/omni/sources_dark.py new file mode 100644 index 0000000..7a181c1 --- /dev/null +++ b/omni/sources_dark.py @@ -0,0 +1,37 @@ +"""Дарк/крэк-форумы, реально отдающие контент без логина. + +Проверено живыми запросами: cracked.st (переехал с cracked.io), nulledbb.com, +forum.tuts4you.com (реверс/крэкинг). Закрыты JS-челленджем без cookie: +exploit.in, xss.is, blackhatworld, hackforums, patched.to, darkforums.st. + +Подключается в реестр omni через sources.all_sources(). +""" + +DARK = [ + {"name": "dk_cracked_st", "category": "forum", "kind": "site", + "url": "https://cracked.st/", "adapter": "html_listing", + "tags": ("dark", "leaks", "abuse"), "limit": 25, + "extra": {"include_contains": ["showthread.php?tid="]}}, + + {"name": "dk_nulledbb", "category": "forum", "kind": "site", + "url": "https://nulledbb.com/", "adapter": "html_listing", + "tags": ("dark", "leaks", "abuse"), "limit": 25, + "extra": {"include_contains": ["showthread.php?tid="]}}, + + {"name": "dk_tuts4you", "category": "forum", "kind": "site", + "url": "https://forum.tuts4you.com/", "adapter": "html_listing", + "tags": ("dark", "reverse", "abuse"), "limit": 25, + "extra": {"include_contains": ["/topic/"]}}, + + {"name": "dk_leaksx", "category": "forum", "kind": "site", + "url": "https://leak.sx/", "adapter": "html_listing", + "tags": ("dark", "leaks", "abuse"), "limit": 20, + "extra": {}}, +] + +# Домены, которые открываются только с живой cookie (cf_clearance / сессия). +# Их можно поднять: python tools/cf_cookies.py -> data/cookies.json +NEED_COOKIE = [ + "exploit.in", "xss.is", "www.blackhatworld.com", "hackforums.net", + "patched.to", "darkforums.st", +] \ No newline at end of file diff --git a/tools/cf_cookies.py b/tools/cf_cookies.py new file mode 100644 index 0000000..e1418ed --- /dev/null +++ b/tools/cf_cookies.py @@ -0,0 +1,138 @@ +"""Снять cookies с CF-защищённых площадок через реальный браузер (Camoufox). + +Домены вроде exploit.in / xss.is / blackhatworld отдают JS-челлендж в ответ на +любой HTTP-клиент. Скрипт открывает площадку настоящим браузером, ждёт прохода +проверки и складывает cookies в data/cookies.json — оттуда их берёт omni/net.py +(config.cookies_for), и html_listing начинает получать контент. + +Запуск: + python tools/cf_cookies.py # все из omni.sources_dark.NEED_COOKIE + python tools/cf_cookies.py xss.is # конкретные хосты + python tools/cf_cookies.py --check # только проверить, что уже есть +""" +import json +import sys +import time +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) + +OUT = ROOT / "data" / "cookies.json" +WAIT = 18 # секунд на проход челленджа + + +def load(): + try: + return json.loads(OUT.read_text(encoding="utf-8")) + except Exception: + return {} + + +def save(d): + OUT.parent.mkdir(parents=True, exist_ok=True) + OUT.write_text(json.dumps(d, ensure_ascii=False, indent=1), encoding="utf-8") + + +def grab(host): + """Открыть https://host, дождаться прохода и вернуть строку cookie.""" + from camoufox.sync_api import Camoufox + + url = host if host.startswith("http") else "https://" + host + with Camoufox(headless=True, humanize=True, geoip=False) as br: + pg = br.new_page() + pg.goto(url, wait_until="domcontentloaded", timeout=60000) + deadline = time.time() + WAIT + last = "" + while time.time() < deadline: + try: + title = pg.title() or "" + html = pg.content() or "" + except Exception: + title, html = "", "" + last = title + low = (title + html[:3000]).lower() + if not any(k in low for k in ("just a moment", "verify humanity", "checking your browser", + "attention required", "ddos-guard")): + break + time.sleep(2.5) + ck = pg.context.cookies() + html_len = len(pg.content() or "") + br.close() + + parts = [] + for c in ck: + dom = (c.get("domain") or "").lstrip(".") + if dom and dom not in host and host not in dom: + continue + parts.append("%s=%s" % (c["name"], c["value"])) + return "; ".join(parts), last, html_len + + +def probe(host, cookie): + """Проверить, что с cookie приходит настоящая страница.""" + import urllib.request + url = host if host.startswith("http") else "https://" + host + req = urllib.request.Request(url, headers={ + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/131.0 Safari/537.36", + "Cookie": cookie, + "Accept-Language": "ru,en;q=0.8", + }) + try: + with urllib.request.urlopen(req, timeout=25) as r: + body = r.read(300_000).decode("utf-8", "ignore") + low = body[:4000].lower() + bad = any(k in low for k in ("just a moment", "verify humanity", "checking your browser")) + return r.status, len(body), not bad + except Exception as e: + return 0, 0, False + + +def main(): + args = [a for a in sys.argv[1:] if not a.startswith("-")] + check_only = "--check" in sys.argv + + if not args: + try: + from omni.sources_dark import NEED_COOKIE + args = list(NEED_COOKIE) + except Exception: + args = ["exploit.in", "xss.is", "www.blackhatworld.com"] + + store = load() + print("cookies-файл:", OUT, "| записей уже:", len(store)) + + if check_only: + for h in args: + ck = store.get(h) or "" + if not ck: + print("%-26s нет cookie" % h) + continue + st, ln, ok = probe(h, ck) + print("%-26s %s len %-7d %s" % (h, st, ln, "OK" if ok else "закрыт")) + return 0 + + ok_n = 0 + for h in args: + print("→", h) + try: + cookie, title, html_len = grab(h) + except Exception as e: + print(" браузер: %s: %s" % (type(e).__name__, str(e)[:90])) + continue + st, ln, ok = probe(h, cookie) if cookie else (0, 0, False) + print(" title=%r html=%d cookie=%d | проверка %s len %d %s" + % (title[:40], html_len, cookie.count("="), st, ln, "OK" if ok else "закрыт")) + if cookie and ok: + store[h] = cookie + ok_n += 1 + + save(store) + print("\nсохранено рабочих: %d | всего в файле: %d" % (ok_n, len(store))) + print("теперь: python -m omni feed --only <источник>") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) \ No newline at end of file