""" app.py — CroxyProxy Rotating Proxy API L'interface (UI) est dans ui.py. UI GET / - Interface navigateur API GET /api - Infos API GET /health - Status + stats GET /servers - Liste des serveurs GET /view - Rend une page via proxy (utilisé par l'UI) POST /proxy/fetch - Proxy rotatif POST /proxy/random - Serveur aléatoire POST /proxy/batch - Plusieurs URLs """ import json, base64, re, random, time, threading, socket, ipaddress from datetime import datetime, timezone from urllib.parse import urlparse from flask import Flask, request, jsonify, Response from bs4 import BeautifulSoup import cloudscraper from html import unescape, escape import warnings warnings.filterwarnings("ignore") from ui import BROWSER_UI, error_page BASE = "https://www.croxyproxy.com" app = Flask(__name__) KEEP_HEADERS = { "content-type", "content-length", "content-encoding", "server", "date", "connection", "access-control-allow-origin", "access-control-allow-credentials", "cache-control", "etag", "last-modified", "x-ratelimit-limit", "x-ratelimit-remaining", "x-request-id", "location", "retry-after", } DROP_HEADERS = { "set-cookie", "__cph", "__cpc", "content-security-policy", "strict-transport-security", "referrer-policy", "access-control-allow-headers", "x-frame-options", "x-content-type-options", "permissions-policy", "cross-origin-opener-policy", "cross-origin-embedder-policy", } class S: servers = [] idx = 0 lock = threading.Lock() last = None stats = {"req": 0, "ok": 0, "fail": 0} def dec(e): try: return json.loads(bytes.fromhex(base64.b64decode(e).decode()).decode()) except Exception: return None def filter_headers(raw_headers, include_all=False): if include_all: return dict(raw_headers) cleaned = {} for k, v in raw_headers.items(): kl = k.lower() if kl in DROP_HEADERS: continue if kl in KEEP_HEADERS: cleaned[k] = v return cleaned def parse_body(text, content_type=""): if not text: return None if "json" in content_type.lower() or text.strip().startswith(("{", "[")): try: return json.loads(text) except (json.JSONDecodeError, ValueError): pass if "html" in content_type.lower() or text.strip().startswith("<"): return { "_type": "html", "_length": len(text), "content": text, "_preview": text[:300].strip() + ("..." if len(text) > 300 else ""), } if len(text) > 2000: return { "_type": "text", "_length": len(text), "content": text, "_preview": text[:500].strip() + "...", } return text def extract_ip(url_str): return (url_str or "").replace("https://", "").replace("http://", "").split("/")[0] def format_result(raw, include_raw_headers=False): if not raw.get("success"): return {"success": False, "error": raw.get("error"), "server": raw.get("server")} ct = "" if raw.get("headers"): ct = raw["headers"].get("Content-Type", raw["headers"].get("content-type", "")) result = { "success": True, "status": raw.get("status"), "url": raw.get("url"), "body": parse_body(raw.get("body", ""), ct), "proxy": raw.get("proxy"), "servers_available": raw.get("servers_available"), } if raw.get("headers"): result["headers"] = filter_headers(raw["headers"], include_all=include_raw_headers) return result def fetch_raw(url, sid=None): sc = cloudscraper.create_scraper( browser={"browser": "chrome", "platform": "windows", "desktop": True} ) S.stats["req"] += 1 try: r1 = sc.get(BASE, timeout=30) if r1.status_code != 200: S.stats["fail"] += 1 return {"success": False, "error": f"Homepage {r1.status_code}"} s1 = BeautifulSoup(r1.text, "lxml") ci = s1.find("input", {"name": "csrf"}) if not ci: S.stats["fail"] += 1 return {"success": False, "error": "No CSRF"} r2 = sc.post( f"{BASE}/servers", data={"url": url, "proxyServerId": "274", "csrf": ci["value"], "demo": "0", "frontOrigin": BASE}, headers={"Content-Type": "application/x-www-form-urlencoded", "Origin": BASE, "Referer": BASE + "/"}, allow_redirects=True, timeout=30, ) if r2.status_code != 200: S.stats["fail"] += 1 return {"success": False, "error": f"Servers {r2.status_code}"} s2 = BeautifulSoup(r2.text, "lxml") sel = s2.find("script", {"id": "serverSelectorScript"}) if not sel: S.stats["fail"] += 1 return {"success": False, "error": "No selector"} ss = [x for x in (dec(i) for i in json.loads(unescape(sel.get("data-ss", "")))) if x and x.get("id")] csrf2 = unescape(sel.get("data-csrf", "")).strip('"') fo = unescape(sel.get("data-fo", "")).strip('"') if not ss: S.stats["fail"] += 1 return {"success": False, "error": "No servers"} S.servers = ss S.last = datetime.now(timezone.utc).isoformat() ch = None if sid: ch = next((x for x in ss if x["id"] == sid), None) if not ch: with S.lock: ch = ss[S.idx % len(ss)] S.idx += 1 r3 = sc.post( f"{BASE}/requests?fso=", data={"url": url, "proxyServerId": str(ch["id"]), "csrf": csrf2, "demo": "0", "frontOrigin": fo}, headers={"Content-Type": "application/x-www-form-urlencoded", "Origin": BASE, "Referer": f"{BASE}/servers"}, allow_redirects=False, timeout=30, ) loc = r3.headers.get("Location") or r3.headers.get("location") if not loc: S.stats["fail"] += 1 return {"success": False, "error": f"No redirect ({r3.status_code})", "server": ch.get("name")} r4 = sc.get(loc, timeout=30, allow_redirects=True) dr = re.search(r'data-r="([^"]+)"', r4.text) if not dr: S.stats["fail"] += 1 return {"success": False, "error": "No data-r", "server": ch.get("name")} final = base64.b64decode(dr.group(1)).decode() r5 = sc.get(final, timeout=30, allow_redirects=True) S.stats["ok"] += 1 return { "success": True, "status": r5.status_code, "headers": dict(r5.headers), "body": r5.text, "url": url, "proxy": { "server_id": ch["id"], "server_name": ch.get("name"), "ip": extract_ip(ch.get("url", "")), }, "servers_available": len(ss), } except Exception as e: S.stats["fail"] += 1 return {"success": False, "error": str(e)} # ═══════════════════════════════════════════════ # RÉÉCRITURE HTML (mode navigation) # ═══════════════════════════════════════════════ # JS injecté dans chaque page : reroute clics + formulaires GET par le proxy. INJECT_JS = r""" (function(){ var sidParam = __SID__ ? ("&server_id=" + __SID__) : ""; function proxify(u){ return "/view?url=" + encodeURIComponent(u) + sidParam; } document.addEventListener("click", function(e){ var a = e.target && e.target.closest ? e.target.closest("a") : null; if(!a || !a.href) return; var raw = a.getAttribute("href") || ""; if(raw.startsWith("#")) return; var h = a.href; if(/^(javascript:|mailto:|tel:|blob:|data:)/i.test(h)) return; e.preventDefault(); window.location.href = proxify(h); }, true); document.addEventListener("submit", function(e){ var f = e.target; if(!f || f.tagName !== "FORM") return; if((f.getAttribute("method") || "get").toLowerCase() !== "get") return; e.preventDefault(); var action = f.action || __BASE__; var params = new URLSearchParams(new FormData(f)).toString(); var sep = action.indexOf("?") === -1 ? "?" : "&"; window.location.href = proxify(action + (params ? sep + params : "")); }, true); })(); """ def rewrite_html(html, base_url, server_id=None): """Injecte (résolution des ressources relatives) + script de navigation.""" try: soup = BeautifulSoup(html, "lxml") head = soup.find("head") if head is None: head = soup.new_tag("head") (soup.html or soup).insert(0, head) for b in soup.find_all("base"): b.decompose() head.insert(0, soup.new_tag("base", href=base_url)) # Retire CSP / refresh meta qui cassent l'affichage for meta in soup.find_all("meta", attrs={"http-equiv": True}): if meta.get("http-equiv", "").lower() in ("content-security-policy", "refresh"): meta.decompose() body = soup.find("body") or soup sid_js = json.dumps(str(server_id)) if server_id else '""' js = INJECT_JS.replace("__SID__", sid_js).replace("__BASE__", json.dumps(base_url)) script_tag = soup.new_tag("script") script_tag.string = js body.append(script_tag) return str(soup) except Exception: # Fallback : injection texte minimale si BeautifulSoup échoue base_tag = f'' return base_tag + html # ═══════════════════════════════════════════════ # FETCH DIRECT (mode navigation — exit IP = la Space) # ═══════════════════════════════════════════════ def is_blocked_host(url): """Bloque les adresses internes/privées (anti-SSRF).""" try: host = urlparse(url).hostname if not host: return True if host == "localhost" or host.endswith(".local") or host.endswith(".internal"): return True for info in socket.getaddrinfo(host, None): ip = ipaddress.ip_address(info[4][0]) if (ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_reserved or ip.is_multicast): return True return False except Exception: # Si la résolution échoue, on laisse le fetch échouer naturellement return False def fetch_direct(url): """Récupère une page directement depuis le serveur.""" sc = cloudscraper.create_scraper( browser={"browser": "chrome", "platform": "windows", "desktop": True} ) S.stats["req"] += 1 try: r = sc.get( url, timeout=30, allow_redirects=True, headers={ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9," "image/avif,image/webp,*/*;q=0.8", "Accept-Language": "fr-FR,fr;q=0.9,en;q=0.8", }, ) S.stats["ok"] += 1 ct = r.headers.get("Content-Type", "") out = { "success": True, "status": r.status_code, "content_type": ct, "final_url": r.url, "content": r.content, } ctl = ct.lower() if "html" in ctl or "json" in ctl or ctl.startswith("text/") or not ct: out["text"] = r.text return out except Exception as e: S.stats["fail"] += 1 return {"success": False, "error": str(e)} def warmup(): print("Warming up — populating server list...") result = fetch_raw("https://httpbin.org/ip") if result.get("success"): print(f"✓ {len(S.servers)} servers loaded") else: print(f"✗ Warm-up failed: {result.get('error')}") def post_fork(server, worker): warmup() warmup() # ═══════════════════════════════════════════════ # ROUTES # ═══════════════════════════════════════════════ @app.route("/") def ui(): return Response(BROWSER_UI, mimetype="text/html") @app.route("/view") def view(): try: url = request.args.get("url", "").strip() if not url: return Response(error_page("Aucune URL fournie."), mimetype="text/html", status=400) if not re.match(r"^https?://", url, re.I): url = "https://" + url if is_blocked_host(url): return Response(error_page("Hôte non autorisé (adresse interne ou privée)."), mimetype="text/html", status=403) res = fetch_direct(url) if not res.get("success"): return Response(error_page(res.get("error", "Erreur inconnue."), url), mimetype="text/html", status=502) ct = (res.get("content_type") or "").lower() final_url = res.get("final_url", url) text = res.get("text", "") # HTML → réécriture pour garder la navigation dans le proxy if "html" in ct or (not ct and text.lstrip().startswith("<")): return Response(rewrite_html(text, final_url), mimetype="text/html") # JSON / texte → affichage lisible if "json" in ct or ct.startswith("text/"): wrap = ( '' '' + escape(text) + '' ) return Response(wrap, mimetype="text/html") # Binaire (images, etc.) → renvoi brut avec le bon content-type return Response(res.get("content", b""), mimetype=ct or "application/octet-stream") except Exception as e: return Response(error_page(f"Erreur serveur : {e}"), mimetype="text/html", status=500) @app.route("/api") def api_index(): return jsonify({ "name": "CroxyProxy Rotating Proxy API", "version": "2.1", "ui": "GET / (interface navigateur)", "endpoints": { "GET /health": "Status + stats", "GET /servers": "Liste des serveurs", "GET /view?url=&server_id=": "Rend une page via proxy (utilisé par l'UI)", "POST /proxy/fetch": "Proxy rotatif {url, server_id?, raw_headers?}", "POST /proxy/random": "Serveur aléatoire {url, raw_headers?}", "POST /proxy/batch": "Plusieurs URLs {urls: [...], raw_headers?}", }, }) @app.route("/health") def health(): return jsonify({ "status": "ready", "servers": len(S.servers), "last_refresh": S.last, "stats": S.stats, }) @app.route("/servers") def servers(): return jsonify({ "count": len(S.servers), "servers": [ {"id": s.get("id"), "name": s.get("name"), "ip": extract_ip(s.get("url", ""))} for s in S.servers ], }) @app.route("/proxy/fetch", methods=["POST"]) def proxy_fetch(): d = request.get_json() or {} if not d.get("url"): return jsonify({"error": "url required"}), 400 raw = fetch_raw(d["url"], d.get("server_id")) return jsonify(format_result(raw, include_raw_headers=d.get("raw_headers", False))) @app.route("/proxy/random", methods=["POST"]) def proxy_random(): d = request.get_json() or {} if not d.get("url"): return jsonify({"error": "url required"}), 400 sid = random.choice(S.servers)["id"] if S.servers else None raw = fetch_raw(d["url"], sid) return jsonify(format_result(raw, include_raw_headers=d.get("raw_headers", False))) @app.route("/proxy/batch", methods=["POST"]) def proxy_batch(): d = request.get_json() or {} urls = d.get("urls", []) if not urls: return jsonify({"error": "urls required"}), 400 include_raw = d.get("raw_headers", False) results = [] for u in urls: raw = fetch_raw(u) results.append(format_result(raw, include_raw_headers=include_raw)) time.sleep(0.5) return jsonify({ "count": len(results), "success_count": sum(1 for r in results if r.get("success")), "results": results, }) if __name__ == "__main__": app.run(host="0.0.0.0", port=7860)