Spaces:
Sleeping
Sleeping
| FROM python:3.11-slim | |
| WORKDIR /app | |
| RUN pip install --no-cache-dir flask requests PySocks gunicorn | |
| RUN cat > main.py << 'PYEOF' | |
| #!/usr/bin/env python3 | |
| """ | |
| TunnelBear VPN Web Proxy — HuggingFace Spaces | |
| """ | |
| import base64, json, logging, os, socket, socketserver, ssl, struct, threading, time, uuid | |
| from datetime import datetime, timezone | |
| from urllib.parse import urlparse | |
| import requests as _r | |
| from flask import Flask, request, jsonify, Response | |
| import socks | |
| TB_EMAIL = os.environ.get("TB_EMAIL", "overwrite249@gmail.com") | |
| TB_PASSWORD = os.environ.get("TB_PASSWORD", "zaLV3uDsS_E+6VN") | |
| TB_COUNTRY = os.environ.get("TB_COUNTRY", None) | |
| PORT = int(os.environ.get("PORT", 7860)) | |
| SOCKS5_PORT = int(os.environ.get("SOCKS5_PORT", 1080)) | |
| NO_TLS = os.environ.get("NO_TLS", "0") == "1" | |
| DASHBOARD_API = "https://prod-api-dashboard.tunnelbear.com/dashboard/web" | |
| TB_API = "https://api.tunnelbear.com" | |
| PB_API = "https://api.polargrizzly.com" | |
| URLS = { | |
| "token": f"{DASHBOARD_API}/v2/token", | |
| "token_cookie": f"{DASHBOARD_API}/v2/tokenCookie", | |
| "cookie_token": f"{TB_API}/v2/cookieToken", | |
| "pb_auth": f"{PB_API}/auth", | |
| "pb_user": f"{PB_API}/user", | |
| "pb_vpns": f"{PB_API}/vpns", | |
| } | |
| APP_VER = "3.6.1" | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S") | |
| log = logging.getLogger("TB") | |
| app = Flask(__name__) | |
| class State: | |
| def __init__(self): | |
| self.lock = threading.Lock() | |
| self.connected = self.connecting = False | |
| self.proxy = None | |
| self.server_url = self.server_proto = self.vpn_token = "" | |
| self.region_name = self.country_iso = self.real_ip = self.vpn_ip = "" | |
| self.started_at = datetime.now(timezone.utc) | |
| self.last_check = None | |
| self.last_check_ok = False | |
| self.connect_errors = 0 | |
| self.total_requests = 0 | |
| self.message = "Not connected" | |
| S = State() | |
| class TBClient: | |
| def __init__(self): | |
| self.s = _r.Session() | |
| self.s.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0", | |
| "Accept": "application/json", "Content-Type": "application/json"}) | |
| self.device = f"browser-{uuid.uuid4()}" | |
| self.dash_tok = self.tb_tok = self.pb_tok = self.vpn_tok = "" | |
| self.servers = []; self.region_name = ""; self.country_iso = "" | |
| def login(self): | |
| try: | |
| r = self.s.post(URLS["token"], json={"username": TB_EMAIL, "password": TB_PASSWORD, | |
| "grant_type": "password", "device": self.device}, timeout=30) | |
| if r.status_code != 200: | |
| log.error(f" Login failed: {r.text[:200]}"); return False | |
| self.dash_tok = r.json().get("access_token", "") | |
| if not self.dash_tok: log.error(" No token"); return False | |
| log.info(f" Logged in: {self.dash_tok[:30]}..."); return True | |
| except Exception as e: log.error(f" Login: {e}"); return False | |
| def session_cookie(self): | |
| try: | |
| r = self.s.post(URLS["token_cookie"], json={}, | |
| headers={"Authorization": f"Bearer {self.dash_tok}"}, timeout=30) | |
| if r.status_code != 200: log.error(f" Cookie: {r.text[:200]}"); return False | |
| return True | |
| except Exception as e: log.error(f" Cookie: {e}"); return False | |
| def cookie_token(self): | |
| try: | |
| r = self.s.post(URLS["cookie_token"], headers={ | |
| "Authorization": f"Bearer {self.dash_tok}", "device": self.device, | |
| "tunnelbear-app-id": "com.tunnelbear.browser", "tunnelbear-app-version": APP_VER, | |
| "tunnelbear-platform": "Firefox", "tunnelbear-platform-version": "Firefox"}, timeout=30) | |
| if r.status_code != 200: log.error(f" CookieToken: {r.text[:200]}"); return False | |
| self.tb_tok = r.json().get("access_token", "") | |
| if not self.tb_tok: log.error(" No token"); return False | |
| log.info(f" TB token: {self.tb_tok[:30]}..."); return True | |
| except Exception as e: log.error(f" CookieToken: {e}"); return False | |
| def pb_auth(self): | |
| try: | |
| r = self.s.post(URLS["pb_auth"], json={"partner": "tunnelbear", "token": self.tb_tok}, timeout=30) | |
| if r.status_code != 200: log.error(f" PB auth: {r.text[:200]}"); return False | |
| h = r.headers.get("authorization", "") | |
| if h.startswith("Bearer "): self.pb_tok = h[7:] | |
| else: log.error(" No auth header"); return False | |
| log.info(f" PB token: {self.pb_tok[:30]}..."); return True | |
| except Exception as e: log.error(f" PB auth: {e}"); return False | |
| def user(self): | |
| try: | |
| r = self.s.get(URLS["pb_user"], headers={"Authorization": f"Bearer {self.pb_tok}"}, timeout=30) | |
| if r.status_code != 200: return False | |
| d = r.json(); self.vpn_tok = d.get("vpn_token", "") | |
| if not self.vpn_tok: return False | |
| log.info(f" VPN Token: {self.vpn_tok}"); return True | |
| except Exception as e: log.error(f" User: {e}"); return False | |
| def get_servers(self, country=None): | |
| url = f"{URLS['pb_vpns']}/countries/{country}" if country else URLS["pb_vpns"] | |
| try: | |
| r = self.s.get(url, headers={"Authorization": f"Bearer {self.pb_tok}"}, timeout=30) | |
| if r.status_code != 200: return False | |
| d = r.json(); vpns = d.get("vpns", []) | |
| self.region_name = d.get("region_name", "?"); self.country_iso = d.get("country_iso", "?") | |
| self.servers = [{"url": v["url"], "protocol": v.get("protocol", "udp")} for v in vpns if "url" in v] | |
| log.info(f" Region: {self.region_name} ({self.country_iso}), {len(self.servers)} servers") | |
| return bool(self.servers) | |
| except Exception as e: log.error(f" Servers: {e}"); return False | |
| def auth(self): | |
| return (self.login() and self.session_cookie() and self.cookie_token() | |
| and self.pb_auth() and self.user() and self.get_servers(TB_COUNTRY)) | |
| class TLSProxy: | |
| def __init__(self, host, token): | |
| self.host, self.port, self.token = host, 8080, token | |
| self._ctx = ssl.create_default_context() | |
| self._ctx.check_hostname = False; self._ctx.verify_mode = ssl.CERT_NONE | |
| def _ssl(self): | |
| raw = socket.create_connection((self.host, self.port), timeout=30) | |
| try: return self._ctx.wrap_socket(raw, server_hostname=self.host) | |
| except: raw.close(); raise | |
| def _rr(self, s): | |
| buf = b"" | |
| while b"\r\n\r\n" not in buf: | |
| c = s.recv(4096) | |
| if not c: raise ConnectionError("closed") | |
| buf += c | |
| idx = buf.index(b"\r\n\r\n") | |
| code = int(buf[:idx].decode(errors="ignore").split(" ", 2)[1].split()[0]) | |
| return code, buf[idx+4:] | |
| def connect(self, th, tp): | |
| ab = base64.b64encode(f"{self.token}:{self.token}".encode()).decode() | |
| s = self._ssl() | |
| s.sendall(f"CONNECT {th}:{tp} HTTP/1.1\r\nHost: {th}:{tp}\r\nProxy-Authorization: Basic {ab}\r\nUser-Agent: TunnelBear/{APP_VER}\r\nProxy-Connection: Keep-Alive\r\n\r\n".encode()) | |
| code, rem = self._rr(s) | |
| if code == 200: return s | |
| if code == 407: | |
| s.close(); s = self._ssl() | |
| s.sendall(f"CONNECT {th}:{tp} HTTP/1.1\r\nHost: {th}:{tp}\r\nUser-Agent: TunnelBear/{APP_VER}\r\n\r\n".encode()) | |
| code, _ = self._rr(s) | |
| if code == 407: | |
| s.sendall(f"CONNECT {th}:{tp} HTTP/1.1\r\nHost: {th}:{tp}\r\nProxy-Authorization: Basic {ab}\r\nUser-Agent: TunnelBear/{APP_VER}\r\n\r\n".encode()) | |
| code, rem = self._rr(s) | |
| if code == 200: return s | |
| s.close(); raise ConnectionError(f"CONNECT HTTP {code}") | |
| class TCPProxy: | |
| def __init__(self, host, token): | |
| self.host, self.port, self.token = host, 8080, token | |
| def connect(self, th, tp): | |
| ab = base64.b64encode(f"{self.token}:{self.token}".encode()).decode() | |
| s = socket.create_connection((self.host, self.port), timeout=30) | |
| s.sendall(f"CONNECT {th}:{tp} HTTP/1.1\r\nHost: {th}:{tp}\r\nProxy-Authorization: Basic {ab}\r\nUser-Agent: TunnelBear/{APP_VER}\r\n\r\n".encode()) | |
| buf = b"" | |
| while b"\r\n\r\n" not in buf: | |
| c = s.recv(4096) | |
| if not c: raise ConnectionError("closed") | |
| buf += c | |
| if b"200" not in buf.split(b"\r\n")[0]: s.close(); raise ConnectionError("CONNECT failed") | |
| return s | |
| class S5H(socketserver.StreamRequestHandler): | |
| proxy = None | |
| def handle(self): | |
| try: self._run() | |
| except: pass | |
| finally: | |
| try: self.connection.close() | |
| except: pass | |
| def _run(self): | |
| c = self.connection | |
| vn = self._rx(c, 2) | |
| if vn[0] != 5: return | |
| ms = self._rx(c, vn[1]) | |
| if 0 in ms: c.sendall(b"\x05\x00") | |
| elif 2 in ms: | |
| c.sendall(b"\x05\x02"); a = self._rx(c, 2); self._rx(c, a[1]) | |
| pl = self._rx(c, 1)[0]; self._rx(c, pl); c.sendall(b"\x01\x00") | |
| else: c.sendall(b"\x05\xFF"); return | |
| h = self._rx(c, 4) | |
| if h[1] != 1: c.sendall(b"\x05"+bytes([7,0,1,0,0,0,0,0,0,0])); return | |
| at = h[3] | |
| if at == 1: th = socket.inet_ntoa(self._rx(c, 4)) | |
| elif at == 3: th = self._rx(c, self._rx(c, 1)[0]).decode() | |
| elif at == 4: th = socket.inet_ntop(socket.AF_INET6, self._rx(c, 16)) | |
| else: c.sendall(b"\x05"+bytes([8,0,1,0,0,0,0,0,0,0])); return | |
| tp = struct.unpack("!H", self._rx(c, 2))[0] | |
| log.info(f" SOCKS5 -> {th}:{tp}") | |
| try: | |
| if not self.proxy: raise Exception("no proxy") | |
| remote = self.proxy.connect(th, tp) | |
| except Exception as e: | |
| log.error(f" Tunnel: {e}") | |
| c.sendall(b"\x05"+bytes([5,0,1,0,0,0,0,0,0,0])); return | |
| c.sendall(b"\x05"+bytes([0,0,1,0,0,0,0,0,0,0])) | |
| c.settimeout(300); remote.settimeout(300) | |
| def _pump(src, dst): | |
| try: | |
| while True: | |
| data = src.recv(8192) | |
| if not data: break | |
| dst.sendall(data) | |
| except: pass | |
| finally: | |
| try: dst.shutdown(socket.SHUT_WR) | |
| except: pass | |
| t1 = threading.Thread(target=_pump, args=(c, remote), daemon=True) | |
| t2 = threading.Thread(target=_pump, args=(remote, c), daemon=True) | |
| t1.start(); t2.start(); t1.join(timeout=300); t2.join(timeout=300) | |
| def _rx(s, n): | |
| b = bytearray() | |
| while len(b) < n: | |
| c = s.recv(n - len(b)) | |
| if not c: raise ConnectionError("closed") | |
| b.extend(c) | |
| return bytes(b) | |
| class S5Server(socketserver.ThreadingMixIn, socketserver.TCPServer): | |
| allow_reuse_address = True; daemon_threads = True | |
| def real_ip(): | |
| try: return _r.get("https://api.ipify.org?format=json", timeout=10).json().get("ip", "") | |
| except: return "" | |
| def vpn_test(p): | |
| try: | |
| s = p.connect("api.ipify.org", 80) | |
| s.sendall(b"GET /?format=json HTTP/1.1\r\nHost: api.ipify.org\r\nConnection: close\r\n\r\n") | |
| buf = b"" | |
| while True: | |
| c = s.recv(4096) | |
| if not c: break | |
| buf += c | |
| s.close() | |
| body = buf.decode(errors="ignore").split("\r\n\r\n", 1) | |
| if len(body) > 1: return json.loads(body[1]).get("ip", "") | |
| except: return "" | |
| def start_socks5(proxy): | |
| S5H.proxy = proxy | |
| srv = S5Server(("127.0.0.1", SOCKS5_PORT), S5H) | |
| threading.Thread(target=srv.serve_forever, daemon=True).start() | |
| log.info(f"SOCKS5 on :{SOCKS5_PORT}") | |
| return srv | |
| socks5_srv = None | |
| def vpn_loop(): | |
| global socks5_srv | |
| backoff = 5 | |
| while True: | |
| with S.lock: S.connecting = True; S.connected = False; S.message = "Connecting..." | |
| log.info("=== VPN: connecting ===") | |
| try: | |
| cl = TBClient() | |
| if not cl.auth(): raise Exception("Auth failed") | |
| proxy = None; wurl = None | |
| for sv in cl.servers: | |
| u, pr = sv["url"], sv["protocol"] | |
| log.info(f" Try {u} ({pr})") | |
| for PC in ([TLSProxy, TCPProxy] if not NO_TLS else [TCPProxy]): | |
| p = PC(u, cl.vpn_tok) | |
| ip = vpn_test(p) | |
| if ip: proxy = p; wurl = u; log.info(f" OK {PC.__name__} ip={ip}"); break | |
| if proxy: break | |
| if not wurl: raise Exception("No working server") | |
| rip = real_ip(); vip = vpn_test(proxy) | |
| with S.lock: | |
| S.connected = S.last_check_ok = True; S.connecting = False | |
| S.proxy = proxy; S.server_url = wurl; S.vpn_token = cl.vpn_tok | |
| S.region_name = cl.region_name; S.country_iso = cl.country_iso | |
| S.real_ip = rip; S.vpn_ip = vip; S.connect_errors = 0 | |
| S.message = f"Connected: {S.region_name} ({S.country_iso})" | |
| if socks5_srv: | |
| try: socks5_srv.shutdown() | |
| except: pass | |
| socks5_srv = start_socks5(proxy) | |
| log.info(f"=== VPN UP: {S.region_name} ip={vip} ===") | |
| while True: | |
| time.sleep(60) | |
| ip = vpn_test(proxy) | |
| now = datetime.now(timezone.utc).isoformat() | |
| with S.lock: S.last_check = now; S.vpn_ip = ip or S.vpn_ip; S.last_check_ok = bool(ip) | |
| if not ip: log.warning("Health fail"); break | |
| except Exception as e: | |
| log.error(f"VPN error: {e}") | |
| with S.lock: S.connected = False; S.connecting = False; S.connect_errors += 1; S.message = f"Error: {e}" | |
| if socks5_srv: | |
| try: socks5_srv.shutdown() | |
| except: pass | |
| log.info(f"Retry in {backoff}s"); time.sleep(backoff); backoff = min(backoff * 2, 300) | |
| def health(): | |
| with S.lock: conn, msg = S.connected, S.message | |
| if request.args.get("live") == "1" and conn: | |
| ip = vpn_test(S.proxy); now = datetime.now(timezone.utc).isoformat() | |
| with S.lock: S.last_check = now; S.last_check_ok = bool(ip); S.vpn_ip = ip or S.vpn_ip | |
| if ip: return jsonify(status="ok", connected=True, vpn_ip=ip, region=S.region_name, country=S.country_iso), 200 | |
| return jsonify(status="degraded", error="tunnel test failed"), 503 | |
| if conn: | |
| return jsonify(status="ok", connected=True, vpn_ip=S.vpn_ip, real_ip=S.real_ip, | |
| region=S.region_name, country=S.country_iso, server=S.server_url, | |
| last_check=S.last_check, last_ok=S.last_check_ok, | |
| uptime=int((datetime.now(timezone.utc)-S.started_at).total_seconds()), | |
| requests=S.total_requests, message=msg), 200 | |
| return jsonify(status="unhealthy", connected=False, message=msg, errors=S.connect_errors), 503 | |
| def ip(): | |
| if not S.connected: return jsonify(error="VPN not connected"), 503 | |
| ip = vpn_test(S.proxy) | |
| with S.lock: S.vpn_ip = ip or S.vpn_ip; S.last_check = datetime.now(timezone.utc).isoformat(); S.last_check_ok = bool(ip) | |
| if ip: return jsonify(vpn_ip=ip, real_ip=S.real_ip, working=ip != S.real_ip, region=S.region_name), 200 | |
| return jsonify(error="tunnel test failed"), 503 | |
| def fetch(): | |
| url = request.args.get("url") | |
| if not url: return jsonify(error="missing ?url="), 400 | |
| if urlparse(url).scheme not in ("http", "https"): return jsonify(error="http/https only"), 400 | |
| S.total_requests += 1 | |
| try: | |
| px = {"http": f"socks5h://127.0.0.1:{SOCKS5_PORT}", "https": f"socks5h://127.0.0.1:{SOCKS5_PORT}"} | |
| r = _r.get(url, proxies=px, timeout=int(request.args.get("timeout", 30))) | |
| return Response(r.content, mimetype="text/plain", headers={"X-VPN-IP": S.vpn_ip or "", "X-Region": S.region_name}) | |
| except Exception as e: return jsonify(error=str(e)), 502 | |
| def status(): | |
| with S.lock: | |
| return jsonify(connected=S.connected, connecting=S.connecting, message=S.message, | |
| server=S.server_url, vpn_ip=S.vpn_ip, real_ip=S.real_ip, | |
| region=S.region_name, country=S.country_iso, socks5=SOCKS5_PORT, | |
| uptime=int((datetime.now(timezone.utc)-S.started_at).total_seconds()), | |
| last_check=S.last_check, last_ok=S.last_check_ok, | |
| errors=S.connect_errors, requests=S.total_requests) | |
| def index(): | |
| return jsonify(service="TunnelBear VPN Proxy", | |
| endpoints={"/health": "health check (?live=1)", "/ip": "VPN exit IP", | |
| "/fetch?url=X": "fetch through VPN", "/status": "full status"}) | |
| log.info(f"Starting — country={TB_COUNTRY or 'auto'} tls={not NO_TLS} port={PORT}") | |
| threading.Thread(target=vpn_loop, daemon=True).start() | |
| if __name__ == "__main__": | |
| app.run(host="0.0.0.0", port=PORT, threaded=True) | |
| PYEOF | |
| EXPOSE 7860 | |
| CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--threads", "4", "--timeout", "120", "main:app"] |