import os
import threading
import requests
import dns.resolver
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor
import re
import socket
import tkinter as tk
from tkinter import messagebox, scrolledtext, simpledialog, filedialog
from tkinter import ttk
from queue import Queue, Empty

# Constants
COMMON_SUBDOMAINS = ['www', 'ftp', 'mail', 'docs', 'files', 'blog']
FILE_TYPES = ['txt', 'csv', 'log', 'xml']
KEYWORDS = [
    "credit card numbers", "debit card details", "CVV codes", "cardholder name", "PAN",
    "expired credit card", "payment card fraud", "PCI-DSS", "bank account number",
    "routing number", "IBAN", "SWIFT code", "sort code", "bank transfer details",
    "account balance", "banking credentials", "username password leak", "credential stuffing",
    "login information", "account credentials", "password hash", "authentication token",
    "login history", "password manager leak", "PayPal account details", "Venmo account",
    "Cash App details", "digital wallet access", "mobile payment credentials",
    "e-wallet hack", "online payment leak", "private key", "wallet address",
    "crypto wallet leak", "cryptocurrency transaction", "Bitcoin address",
    "Ethereum wallet", "crypto account hack", "crypto key exposure",
    "social security number leak", "name address phone number", "identity theft",
    "personal data breach", "address history", "PII leak", "email and phone data",
    "data scraping", "data breach", "leaked database", "data leak forum",
    "hacked data", "dark web leaks", "exposed database", "compromised data",
    "breached credentials", "fraud detection", "unauthorized transaction",
    "fraudulent payment", "account takeover", "identity verification",
    "credit card fraud prevention", "chargeback fraud"
]


class SpiderCrawlApp:
    def __init__(self, master):
        self.master = master
        self.master.title("SpiderCrawl")
        self.master.geometry("700x600")

        # Initialize GUI and variables
        self.queue = Queue()
        self.executor = ThreadPoolExecutor(max_workers=20)
        self.save_path = os.path.join(os.path.expanduser("~"), "SpiderCrawl")
        self.create_directory(self.save_path)
        self.setup_gui()

    def create_directory(self, path):
        try:
            os.makedirs(path, exist_ok=True)
            self.log_message(f"Directory '{path}' is ready.")
        except Exception as e:
            messagebox.showerror("Error", f"Failed to create directory '{path}': {e}")
            self.master.destroy()

    def get_dns_records(self, domain):
        try:
            resolver = dns.resolver.Resolver()
            resolver.nameservers = ['8.8.8.8']  # Google DNS
            answers = resolver.resolve(domain, 'A')
            return [rdata.to_text() for rdata in answers]
        except dns.resolver.NXDOMAIN:
            self.log_message(f"Domain {domain} does not exist.")
            return []
        except dns.resolver.NoAnswer:
            self.log_message(f"No DNS answer for {domain}.")
            return []
        except Exception as e:
            self.log_message(f"DNS resolution error for {domain}: {e}")
            return []

    def check_subdomains(self, base_domain):
        self.log_message(f"Resolving subdomains for {base_domain}...")
        subdomains_found = []
        for sub in COMMON_SUBDOMAINS:
            subdomain = f"{sub}.{base_domain}"
            ips = self.get_dns_records(subdomain)
            if ips:
                subdomains_found.append(subdomain)
                self.log_message(f"Found subdomain: {subdomain} -> {', '.join(ips)}")
        if not subdomains_found:
            self.log_message(f"No subdomains found for {base_domain}.")
        return subdomains_found

    def get_nearby_domains(self):
        try:
            local_ip = socket.gethostbyname(socket.gethostname())
            self.log_message(f"Local IP: {local_ip}")
            ip_parts = local_ip.split('.')
            if len(ip_parts) < 4:
                raise ValueError("Invalid local IP address format.")
            base_domain = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}"
            nearby_domain = f"{base_domain}.com"
            self.log_message(f"Inferred nearby domain: {nearby_domain}")
            return nearby_domain
        except Exception as e:
            self.log_message(f"Error inferring nearby domains: {e}")
            return None

    def check_for_keywords(self, file_url):
        try:
            self.log_message(f"Checking file: {file_url}")
            response = requests.get(file_url, timeout=10)
            if response.status_code == 200 and 'text' in response.headers.get('Content-Type', ''):
                content = response.text.lower()
                matches = [kw for kw in KEYWORDS if re.search(r'\b' + re.escape(kw.lower()) + r'\b', content)]
                if matches:
                    filename = os.path.join(self.save_path, os.path.basename(file_url))
                    with open(filename, 'w', encoding='utf-8') as f:
                        f.write(response.text)
                    match_message = f"Found keywords {matches} in {file_url}. Saved to {filename}."
                    self.log_message(match_message)
                    self.queue.put(match_message)
            else:
                self.log_message(f"Failed to fetch or non-text content: {file_url} (Status: {response.status_code})")
        except requests.exceptions.RequestException as e:
            self.log_message(f"Request error for {file_url}: {e}")

    def crawl_and_check_files(self, url):
        try:
            self.log_message(f"Scanning URL: {url}")
            response = requests.get(url, timeout=10)
            if response.status_code == 200:
                soup = BeautifulSoup(response.text, 'html.parser')
                links = soup.find_all('a', href=True)
                file_links = [link['href'] for link in links if any(link['href'].lower().endswith(ext) for ext in FILE_TYPES)]
                self.log_message(f"Found {len(file_links)} file(s) in {url}.")
                for href in file_links:
                    full_url = href if href.startswith('http') else f"http://{url.split('//')[-1].split('/')[0]}/{href.lstrip('/')}"
                    self.executor.submit(self.check_for_keywords, full_url)
            else:
                self.log_message(f"Failed to access {url} (Status: {response.status_code})")
        except requests.exceptions.RequestException as e:
            self.log_message(f"Error accessing {url}: {e}")

    def crawl_subdomains_and_find_files(self, subdomains):
        self.log_message("Starting to crawl subdomains...")
        for subdomain in subdomains:
            self.executor.submit(self.crawl_and_check_files, f"http://{subdomain}")

    def start_crawling(self):
        base_domain = self.base_domain_entry.get().strip()
        if not base_domain:
            messagebox.showwarning("Input Needed", "Please enter a base domain to start crawling.")
            return
        self.status_label.config(text=f"Crawling {base_domain}...", fg="blue")
        self.log_message(f"Initiating crawl for {base_domain}...")
        self.progress.start()
        threading.Thread(target=self._start_crawling_thread, args=(base_domain,), daemon=True).start()

    def _start_crawling_thread(self, base_domain):
        subdomains = self.check_subdomains(base_domain)
        if subdomains:
            self.crawl_subdomains_and_find_files(subdomains)
        else:
            self.log_message("Crawling aborted: No subdomains to scan.")
        self.progress.stop()
        self.status_label.config(text="Status: Crawl Completed", fg="green")

    def log_message(self, message):
        self.log_text.config(state=tk.NORMAL)
        self.log_text.insert(tk.END, message + "\n")
        self.log_text.see(tk.END)
        self.log_text.config(state=tk.DISABLED)
        print(message)

    def update_gui(self):
        try:
            while True:
                message = self.queue.get_nowait()
                self.matches_text.config(state=tk.NORMAL)
                self.matches_text.insert(tk.END, message + "\n")
                self.matches_text.see(tk.END)
                self.matches_text.config(state=tk.DISABLED)
        except Empty:
            pass
        self.master.after(100, self.update_gui)

    def choose_directory(self):
        selected_dir = filedialog.askdirectory()
        if selected_dir:
            self.save_path = selected_dir
            self.log_message(f"Save path changed to: {self.save_path}")

    def setup_gui(self):
        # Log Frame
        log_label = tk.Label(self.master, text="Log:")
        log_label.pack(pady=5)
        self.log_text = scrolledtext.ScrolledText(self.master, height=10, width=80, state=tk.DISABLED)
        self.log_text.pack(pady=5)

        # Input Frame
        input_frame = tk.Frame(self.master)
        input_frame.pack(pady=10)

        tk.Label(input_frame, text="Base Domain:").grid(row=0, column=0, padx=5)
        self.base_domain_entry = tk.Entry(input_frame, width=40)
        self.base_domain_entry.grid(row=0, column=1, padx=5)

        # Autofill with a guessed domain, if possible
        auto_domain = self.get_nearby_domains()
        if auto_domain:
            self.base_domain_entry.insert(0, auto_domain)

        # Save Directory Button
        save_button = tk.Button(input_frame, text="Set Save Directory", command=self.choose_directory)
        save_button.grid(row=1, column=1, padx=5, sticky='e')

        # Crawling Button
        crawl_button = tk.Button(self.master, text="Start Crawling", command=self.start_crawling)
        crawl_button.pack(pady=5)

        # Status and Progress
        self.status_label = tk.Label(self.master, text="Status: Idle", fg="black")
        self.status_label.pack(pady=5)
        self.progress = ttk.Progressbar(self.master, mode="indeterminate")
        self.progress.pack(pady=5, fill=tk.X)

        # Matched Files Display
        matches_label = tk.Label(self.master, text="Matches Found:")
        matches_label.pack(pady=5)
        self.matches_text = scrolledtext.ScrolledText(self.master, height=10, width=80, state=tk.DISABLED)
        self.matches_text.pack(pady=5)

        # Update GUI Loop
        self.master.after(100, self.update_gui)


if __name__ == "__main__":
    root = tk.Tk()
    app = SpiderCrawlApp(root)
    root.mainloop()