Spaces:
Paused
Paused
| """ | |
| 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 <base> (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'<base href="{escape(base_url, quote=True)}">' | |
| 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 | |
| # βββββββββββββββββββββββββββββββββββββββββββββββ | |
| def ui(): | |
| return Response(BROWSER_UI, mimetype="text/html") | |
| 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 = ( | |
| '<!doctype html><html><head><meta charset="utf-8">' | |
| '<style>body{background:#0d0f17;color:#e6e8f0;margin:0;padding:20px;' | |
| 'font-family:ui-monospace,Menlo,monospace;font-size:13px;line-height:1.6;' | |
| 'white-space:pre-wrap;word-break:break-word}</style></head><body>' | |
| + escape(text) + '</body></html>' | |
| ) | |
| 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) | |
| 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?}", | |
| }, | |
| }) | |
| def health(): | |
| return jsonify({ | |
| "status": "ready", | |
| "servers": len(S.servers), | |
| "last_refresh": S.last, | |
| "stats": S.stats, | |
| }) | |
| 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 | |
| ], | |
| }) | |
| 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))) | |
| 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))) | |
| 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) | |