proxy / Dockerfile
overwrite69's picture
Update Dockerfile
743f793 verified
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"]