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) @staticmethod 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) @app.route("/health") 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 @app.route("/ip") 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 @app.route("/fetch") 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 @app.route("/status") 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) @app.route("/") 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"]