#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ SPIP Scanner – Refined detection, no false positives from generic "SPIP" string. """ from __future__ import annotations import asyncio import os import random import re import sys import time from dataclasses import dataclass, field from pathlib import Path from typing import Optional WEBSHARE_HOST = "p.webshare.io" WEBSHARE_PORT = "80" WEBSHARE_USER = "mcwdzeji-rotate" WEBSHARE_PASS = "i3918xwqkrjv" WEBSHARE_URL = f"http://{WEBSHARE_USER}:{WEBSHARE_PASS}@{WEBSHARE_HOST}:{WEBSHARE_PORT}" R = "\033[1;31m" G = "\033[1;32m" Y = "\033[1;33m" B = "\033[1;34m" P = "\033[1;35m" C = "\033[1;36m" W = "\033[1;37m" RESET = "\033[0m" PROMPT = f"{Y}┌──-[root]\n" f"{Y}└Number{Y}${RESET} " list_input = f"{Y}┌──-[root]\n" f"{Y}└Input List{Y}${RESET} " try: import aiohttp except ImportError: print("Missing dependency 'aiohttp'. Install with: pip install aiohttp") sys.exit(1) try: from aiohttp_socks import ProxyConnector except ImportError: print("Missing dependency 'aiohttp-socks'. Install with: pip install aiohttp-socks") sys.exit(1) try: import aiofiles except ImportError: print("Missing dependency 'aiofiles'. Install with: pip install aiofiles") sys.exit(1) from colorama import Fore, Style, init as colorama_init from rich.console import Console from rich.live import Live from rich.panel import Panel from rich.table import Table try: import orjson as _orjson def json_dumps(obj) -> str: return _orjson.dumps(obj).decode("utf-8") except ImportError: import json as _json def json_dumps(obj) -> str: return _json.dumps(obj) if sys.platform.startswith("linux"): try: import uvloop uvloop.install() except ImportError: pass colorama_init(autoreset=True) console = Console() @dataclass class Config: CONCURRENCY: int = 100 CONNECT_TIMEOUT: float = 8.0 READ_TIMEOUT: float = 8.0 TOTAL_TIMEOUT: float = 15.0 RETRY: int = 2 VERIFY_SSL: bool = False MAX_REDIRECT: int = 5 BUFFER_SIZE: int = 200 AUTO_SAVE: float = 1.0 SHOW_SPEED: bool = True USER_AGENT_ROTATION: bool = True USE_PROXY: bool = False PROXY_FILE: str = "proxies.txt" INPUT_FILE: str = "domains.txt" OUTPUT_DIR: str = "output" def as_table_rows(self) -> list[tuple[str, str]]: return [ ("CONCURRENCY", str(self.CONCURRENCY)), ("CONNECT_TIMEOUT", f"{self.CONNECT_TIMEOUT}s"), ("READ_TIMEOUT", f"{self.READ_TIMEOUT}s"), ("TOTAL_TIMEOUT", f"{self.TOTAL_TIMEOUT}s"), ("RETRY", str(self.RETRY)), ("VERIFY_SSL", str(self.VERIFY_SSL)), ("MAX_REDIRECT", str(self.MAX_REDIRECT)), ("BUFFER_SIZE", str(self.BUFFER_SIZE)), ("AUTO_SAVE", f"{self.AUTO_SAVE}s"), ("SHOW_SPEED", str(self.SHOW_SPEED)), ("USER_AGENT_ROTATION", str(self.USER_AGENT_ROTATION)), ("USE_PROXY", str(self.USE_PROXY)), ("PROXY_FILE", str(self.PROXY_FILE)), ] CMS_CATEGORIES: tuple[str, ...] = ("spip", "unknown", "dead") CMS_COLORS: dict[str, str] = { "spip": Fore.CYAN, "unknown": Fore.WHITE, "dead": Fore.LIGHTBLACK_EX, } class ProxyPool: def __init__(self, proxy_file: str, default_proxy: str = WEBSHARE_URL) -> None: self.proxy_file = Path(proxy_file) self.default_proxy = default_proxy self.proxies: list[str] = [] def load_proxies(self) -> int: if not self.proxy_file.exists(): self.proxies = [self.default_proxy] return len(self.proxies) with self.proxy_file.open("r", encoding="utf-8", errors="ignore") as f: lines = f.readlines() cleaned = [] for line in lines: p = line.strip() if not p: continue if not p.startswith(("http://", "https://", "socks4://", "socks5://")): p = f"http://{p}" cleaned.append(p) if not cleaned: self.proxies = [self.default_proxy] else: self.proxies = list(set(cleaned)) return len(self.proxies) def get_random(self) -> Optional[str]: if not self.proxies: return self.default_proxy return random.choice(self.proxies) class UserAgentPool: _CHROME_VERSIONS = [ "120.0.0.0", "121.0.0.0", "122.0.0.0", "123.0.0.0", "124.0.0.0", "125.0.0.0", "126.0.0.0", "127.0.0.0", "128.0.0.0", "129.0.0.0", "130.0.0.0", "131.0.0.0", ] _FIREFOX_VERSIONS = [ "118.0", "119.0", "120.0", "121.0", "122.0", "123.0", "124.0", "125.0", "126.0", "127.0", "128.0", "129.0", ] _EDGE_VERSIONS = _CHROME_VERSIONS _SAFARI_VERSIONS = ["16.6", "17.0", "17.1", "17.2", "17.3", "17.4", "17.5"] _WINDOWS_PLATFORMS = ["Windows NT 10.0; Win64; x64", "Windows NT 11.0; Win64; x64"] _MAC_PLATFORMS = [ "Macintosh; Intel Mac OS X 10_15_7", "Macintosh; Intel Mac OS X 13_6", "Macintosh; Intel Mac OS X 14_4", ] _LINUX_PLATFORMS = ["X11; Linux x86_64", "X11; Ubuntu; Linux x86_64"] def __init__(self) -> None: self._pool: list[str] = self._build_pool() def _build_pool(self) -> list[str]: agents: list[str] = [] for platform in self._WINDOWS_PLATFORMS + self._MAC_PLATFORMS + self._LINUX_PLATFORMS: for version in self._CHROME_VERSIONS: agents.append( f"Mozilla/5.0 ({platform}) AppleWebKit/537.36 " f"(KHTML, like Gecko) Chrome/{version} Safari/537.36" ) for version in self._FIREFOX_VERSIONS: agents.append( f"Mozilla/5.0 ({platform}; rv:{version}) " f"Gecko/20100101 Firefox/{version}" ) seen: set[str] = set() unique_agents = [] for agent in agents: if agent not in seen: seen.add(agent) unique_agents.append(agent) return unique_agents def random(self) -> str: return random.choice(self._pool) def __len__(self) -> int: return len(self._pool) def clear_screen() -> None: os.system("cls" if os.name == "nt" else "clear") def normalize_domain(raw: str) -> Optional[str]: value = raw.strip() if not value: return None value = re.sub(r"^https?://", "", value, flags=re.IGNORECASE) value = value.split("/")[0] value = value.strip().strip(".") return value or None def dedupe_preserve_order(items: list[str]) -> list[str]: seen: set[str] = set() result: list[str] = [] for item in items: if item not in seen: seen.add(item) result.append(item) return result def format_duration(seconds: float) -> str: seconds = max(0, int(seconds)) hours, remainder = divmod(seconds, 3600) minutes, secs = divmod(remainder, 60) return f"{hours:02d}:{minutes:02d}:{secs:02d}" class FileManager: def __init__(self, config: Config) -> None: self.config = config self.output_dir = Path(config.OUTPUT_DIR) self._buffers: dict[str, list[str]] = {cat: [] for cat in CMS_CATEGORIES} self._locks: dict[str, asyncio.Lock] = {cat: asyncio.Lock() for cat in CMS_CATEGORIES} def ensure_output_dir(self) -> None: self.output_dir.mkdir(parents=True, exist_ok=True) for category in CMS_CATEGORIES: path = self.output_dir / f"{category}.txt" if not path.exists(): path.touch() def load_domains_from(self, path: str) -> list[str]: input_path = Path(path) if not input_path.exists(): return [] with input_path.open("r", encoding="utf-8", errors="ignore") as handle: lines = handle.readlines() cleaned = [normalize_domain(line) for line in lines] domains = [item for item in cleaned if item] return dedupe_preserve_order(domains) def load_processed_domains(self) -> set[str]: processed: set[str] = set() for category in CMS_CATEGORIES: path = self.output_dir / f"{category}.txt" if not path.exists(): continue with path.open("r", encoding="utf-8", errors="ignore") as handle: for line in handle: domain = normalize_domain(line) if domain: processed.add(domain) return processed async def record(self, category: str, url: str) -> None: domain = normalize_domain(url) if not domain: return async with self._locks[category]: self._buffers[category].append(domain) async def flush_all(self) -> None: for category in CMS_CATEGORIES: async with self._locks[category]: await self._flush_category(category) async def _flush_category(self, category: str) -> None: buffer = self._buffers[category] if not buffer: return path = self.output_dir / f"{category}.txt" async with aiofiles.open(path, "a", encoding="utf-8") as handle: await handle.write("\n".join(buffer) + "\n") buffer.clear() async def periodic_flush(self, interval: float, stop_event: asyncio.Event) -> None: while not stop_event.is_set(): try: await asyncio.wait_for(stop_event.wait(), timeout=interval) except asyncio.TimeoutError: await self.flush_all() await self.flush_all() class CMSDetector: """ Strict SPIP detection. A domain is classified as SPIP ONLY when the fetched HTML/source contains the exact filename "spip.php" (case-insensitive). Headers, cookies, meta generator, /ecrire/, plugins, etc. are ignored. """ SPIP_PATTERN = re.compile(r"(? str: if not html: return "unknown" return "spip" if self.SPIP_PATTERN.search(html) else "unknown" class HTTPClientFactory: def __init__(self, config: Config, user_agents: UserAgentPool) -> None: self.config = config self.user_agents = user_agents def build_session(self) -> aiohttp.ClientSession: connector = aiohttp.TCPConnector( limit=self.config.CONCURRENCY, limit_per_host=0, ttl_dns_cache=300, use_dns_cache=True, ssl=self.config.VERIFY_SSL, enable_cleanup_closed=True, keepalive_timeout=30, ) timeout = aiohttp.ClientTimeout( total=self.config.TOTAL_TIMEOUT, connect=self.config.CONNECT_TIMEOUT, sock_read=self.config.READ_TIMEOUT, ) headers = { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, br", "Accept-Language": "en-US,en;q=0.9", "Connection": "keep-alive", } return aiohttp.ClientSession( connector=connector, timeout=timeout, headers=headers, trust_env=True, ) def request_headers(self) -> dict: if self.config.USER_AGENT_ROTATION: return {"User-Agent": self.user_agents.random()} return {"User-Agent": self.user_agents.random() if len(self.user_agents) else "CMSChecker/1.0"} @dataclass class ScanStats: total: int = 0 scanned: int = 0 live: int = 0 dead: int = 0 per_cms: dict[str, int] = field(default_factory=lambda: {c: 0 for c in CMS_CATEGORIES}) started_at: float = field(default_factory=time.monotonic) requests_made: int = 0 @property def remaining(self) -> int: return max(0, self.total - self.scanned) @property def elapsed(self) -> float: return time.monotonic() - self.started_at @property def cpm(self) -> float: minutes = self.elapsed / 60 or 1e-9 return self.scanned / minutes @property def rps(self) -> float: return self.requests_made / (self.elapsed or 1e-9) @property def eta_seconds(self) -> float: if self.cpm <= 0: return 0.0 return (self.remaining / self.cpm) * 60 class Scanner: def __init__( self, config: Config, file_manager: FileManager, detector: CMSDetector, client_factory: HTTPClientFactory, proxy_pool: ProxyPool, stats: ScanStats, ) -> None: self.config = config self.file_manager = file_manager self.detector = detector self.client_factory = client_factory self.proxy_pool = proxy_pool self.stats = stats async def run(self, domains: list[str]) -> None: self.stats.total = len(domains) queue: asyncio.Queue[str] = asyncio.Queue() for domain in domains: queue.put_nowait(domain) semaphore = asyncio.Semaphore(self.config.CONCURRENCY) async with self.client_factory.build_session() as session: workers = [ asyncio.create_task(self._worker(queue, session, semaphore)) for _ in range(min(self.config.CONCURRENCY, max(1, len(domains)))) ] await queue.join() for worker in workers: worker.cancel() await asyncio.gather(*workers, return_exceptions=True) async def _worker( self, queue: "asyncio.Queue[str]", session: aiohttp.ClientSession, semaphore: asyncio.Semaphore, ) -> None: while True: domain = await queue.get() try: async with semaphore: await self._scan_domain(domain, session) finally: queue.task_done() async def _scan_domain(self, domain: str, session: aiohttp.ClientSession) -> None: for scheme in ("https", "http"): url = f"{scheme}://{domain}" response = await self._fetch_with_retry(url, session) if response is None: continue headers, cookies, html, final_url = response cms = self.detector.detect(html) if cms == "spip": await self.file_manager.record("spip", final_url) self._update_stats("spip", alive=True) self._print_result("spip", final_url) return # Non-SPIP domains are ignored. # We still count the domain as scanned, but do not save it. self.stats.scanned += 1 return # Unreachable/dead domains are ignored as well. self.stats.scanned += 1 async def _fetch_with_retry( self, url: str, session: aiohttp.ClientSession ) -> Optional[tuple[dict, str, str, str]]: attempts = self.config.RETRY + 1 for attempt in range(attempts): self.stats.requests_made += 1 headers = self.client_factory.request_headers() proxy = self.proxy_pool.get_random() if self.config.USE_PROXY else None try: if proxy and proxy.startswith(("socks4://", "socks5://")): connector = ProxyConnector.from_url(proxy, ssl=self.config.VERIFY_SSL) timeout = aiohttp.ClientTimeout( total=self.config.TOTAL_TIMEOUT, connect=self.config.CONNECT_TIMEOUT, sock_read=self.config.READ_TIMEOUT, ) async with aiohttp.ClientSession(connector=connector, timeout=timeout) as socks_session: async with socks_session.get( url, headers=headers, allow_redirects=True, max_redirects=self.config.MAX_REDIRECT, ) as resp: body = await resp.text(errors="ignore") response_headers = dict(resp.headers) cookie_header = response_headers.get("Set-Cookie", "") return response_headers, cookie_header, body, str(resp.url) else: async with session.get( url, headers=headers, proxy=proxy, allow_redirects=True, max_redirects=self.config.MAX_REDIRECT, ssl=self.config.VERIFY_SSL, ) as resp: body = await resp.text(errors="ignore") response_headers = dict(resp.headers) cookie_header = response_headers.get("Set-Cookie", "") return response_headers, cookie_header, body, str(resp.url) except (asyncio.TimeoutError, aiohttp.ClientConnectionError, Exception): if attempt < attempts - 1: continue return None return None def _update_stats(self, cms: str, alive: bool) -> None: self.stats.scanned += 1 self.stats.per_cms[cms] = self.stats.per_cms.get(cms, 0) + 1 if alive: self.stats.live += 1 else: self.stats.dead += 1 @staticmethod def _print_result(cms: str, url: str) -> None: print(f"{G}[+]{RESET} " f"{C}{cms.upper():<12}{RESET} " f"{W}{url}{RESET}") class LiveProgressDisplay: def __init__(self, stats: ScanStats) -> None: self.stats = stats _COLUMNS_PER_ROW = 4 def _build_table(self) -> Panel: s = self.stats cells: list[tuple[str, str]] = [ ("Total", str(s.total)), ("Scanned", str(s.scanned)), ("Remaining", str(s.remaining)), ("Live", str(s.live)), ("Dead", str(s.dead)), ("Unknown", str(s.per_cms.get("unknown", 0))), ("SPIP", str(s.per_cms.get("spip", 0))), ("CPM", f"{s.cpm:.1f}"), ("Request/s", f"{s.rps:.1f}"), ("Elapsed", format_duration(s.elapsed)), ("ETA", format_duration(s.eta_seconds)), ] grid = Table.grid(padding=(0, 3)) for _ in range(self._COLUMNS_PER_ROW): grid.add_column(justify="left") for row_start in range(0, len(cells), self._COLUMNS_PER_ROW): row_cells = cells[row_start : row_start + self._COLUMNS_PER_ROW] formatted = [f"[bold]{name}[/bold] {value}" for name, value in row_cells] while len(formatted) < self._COLUMNS_PER_ROW: formatted.append("") grid.add_row(*formatted) return Panel( grid, title="[bold cyan]LIVE STATISTICS[/bold cyan]", border_style="cyan" ) async def run(self, stop_event: asyncio.Event, refresh_seconds: float = 1.0) -> None: with Live(self._build_table(), refresh_per_second=1, console=console) as live: while not stop_event.is_set(): live.update(self._build_table()) try: await asyncio.wait_for(stop_event.wait(), timeout=refresh_seconds) except asyncio.TimeoutError: continue live.update(self._build_table()) def _build_banner(): green = "\033[92m" reset = "\033[0m" return rf"""{green} ________ ______ __ _ __ ______ /_ __/ /_ ___ / ____/_ __/ /_ ___ _____ | | / /___ / / __/ / / / __ \/ _ \ / / / / / / __ \/ _ \/ ___/ | | /| / / __ \/ / /_ / / / / / / __/ / /___/ /_/ / /_/ / / __/ | / |/ / / / / / __/ /_/ /_/ /_/\___/ \____/\__, /_.___/\___/_/ |__/|__/\____/_/_/ /____/ ╔════════════════════════════════════════════════════════════════════════════╗ ║ ▸ Mode 1 : Start scanning (SPIP only) ║ ║ ▸ Mode 2 : Settings ║ ║ ▸ Mode 3 : Exit ║ ╠════════════════════════════════════════════════════════════════════════════╣ ║ ╭─[ SYSTEM CORE ] ║ ║ ├─● STATUS : ACTIVE ║ ║ ├─● ENGINE : MR ║ ║ ├─● MODULE : SCAN SPIP ║ ╚════════════════════════════════════════════════════════════════════════════╝ {reset}""" BANNER = _build_banner() class Application: def __init__(self) -> None: self.config = Config() self.user_agents = UserAgentPool() self.proxy_pool = ProxyPool(self.config.PROXY_FILE, default_proxy=WEBSHARE_URL) def show_banner(self) -> None: clear_screen() print(BANNER) def show_settings_menu(self) -> None: while True: clear_screen() print("SETTINGS\n" + "─" * 48) rows = self.config.as_table_rows() for idx, (name, value) in enumerate(rows, start=1): print(f"[{idx}] {name:<22} = {value}") print(f"[{len(rows) + 1}] Back to main menu") print("─" * 48) choice = input(PROMPT).strip() if choice == str(len(rows) + 1): break if not choice.isdigit() or not (1 <= int(choice) <= len(rows)): print("Invalid selection.") time.sleep(1) continue field_name = rows[int(choice) - 1][0] self._edit_config_field(field_name) def _edit_config_field(self, field_name: str) -> None: current = getattr(self.config, field_name) new_value = input(f"New value for {field_name} (current: {current}): ").strip() if not new_value: return try: if isinstance(current, bool): setattr(self.config, field_name, new_value.lower() in ("1", "true", "yes", "y")) elif isinstance(current, int): setattr(self.config, field_name, int(new_value)) elif isinstance(current, float): setattr(self.config, field_name, float(new_value)) else: setattr(self.config, field_name, new_value) except ValueError: print("Invalid value, keeping previous setting.") time.sleep(1) def _prompt_for_domain_list(self, file_manager: FileManager) -> Optional[list[str]]: default_path = self.config.INPUT_FILE while True: raw = input(f"{list_input}").strip() if raw.lower() == "q": return None path = raw or default_path domains = file_manager.load_domains_from(path) if domains: self.config.INPUT_FILE = path return domains print(f"'{path}' not found or empty -- check the path and try again.") async def start_scan(self) -> None: file_manager = FileManager(self.config) file_manager.ensure_output_dir() if self.config.USE_PROXY: self.proxy_pool.proxy_file = Path(self.config.PROXY_FILE) loaded_proxies = self.proxy_pool.load_proxies() console.print(f"[bold green]Proxy enabled. Using Webshare Rotating Proxy / {loaded_proxies} loaded proxies.[/bold green]") all_domains = self._prompt_for_domain_list(file_manager) if all_domains is None: return processed = file_manager.load_processed_domains() pending = [d for d in all_domains if d not in processed] console.print(f"[bold green]Loaded {len(all_domains)} domains ({len(processed)} already processed, {len(pending)} pending).[/bold green]") if not pending: console.print("[bold yellow]Nothing to scan -- all domains already processed.[/bold yellow]") input("\nPress Enter to return to the menu...") return detector = CMSDetector() client_factory = HTTPClientFactory(self.config, self.user_agents) stats = ScanStats() stats.total = len(pending) scanner = Scanner(self.config, file_manager, detector, client_factory, self.proxy_pool, stats) progress = LiveProgressDisplay(stats) stop_event = asyncio.Event() progress_task = asyncio.create_task(progress.run(stop_event)) flush_task = asyncio.create_task(file_manager.periodic_flush(self.config.AUTO_SAVE, stop_event)) try: await scanner.run(pending) finally: stop_event.set() await asyncio.gather(progress_task, flush_task, return_exceptions=True) await file_manager.flush_all() console.print("\n[bold green]Scan complete.[/bold green]") input("Press Enter to return to main menu...") def run(self) -> None: while True: self.show_banner() choice = input(f"{Y}└Mode{Y}$ {RESET}").strip() if choice == "1": asyncio.run(self.start_scan()) elif choice == "2": self.show_settings_menu() elif choice == "3": print("\nExiting...") sys.exit(0) else: print(f"{R}Invalid mode.{RESET}") time.sleep(1) if __name__ == "__main__": try: # Semua verifikasi lisensi dan password telah dihapus app = Application() app.run() except KeyboardInterrupt: print("\nAborted by user.") sys.exit(0)