overwrite69 commited on
Commit
ce2d9f3
·
verified ·
1 Parent(s): ad047ec

Create Dockerfile

Browse files
Files changed (1) hide show
  1. Dockerfile +377 -0
Dockerfile ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+ WORKDIR /app
3
+ RUN pip install --no-cache-dir flask requests PySocks gunicorn
4
+
5
+ RUN cat > main.py << 'PYEOF'
6
+ #!/usr/bin/env python3
7
+ """
8
+ TunnelBear VPN Web Proxy — HuggingFace Spaces
9
+ """
10
+
11
+ import base64, json, logging, os, socket, socketserver, ssl, struct, threading, time, uuid
12
+ from datetime import datetime, timezone
13
+ from urllib.parse import urlparse
14
+
15
+ import requests as _r
16
+ from flask import Flask, request, jsonify, Response
17
+ import socks
18
+
19
+ TB_EMAIL = os.environ.get("TB_EMAIL", "overwrite249@gmail.com")
20
+ TB_PASSWORD = os.environ.get("TB_PASSWORD", "zaLV3uDsS_E+6VN")
21
+ TB_COUNTRY = os.environ.get("TB_COUNTRY", None)
22
+ PORT = int(os.environ.get("PORT", 7860))
23
+ SOCKS5_PORT = int(os.environ.get("SOCKS5_PORT", 1080))
24
+ NO_TLS = os.environ.get("NO_TLS", "0") == "1"
25
+
26
+ DASHBOARD_API = "https://prod-api-dashboard.tunnelbear.com/dashboard/web"
27
+ TB_API = "https://api.tunnelbear.com"
28
+ PB_API = "https://api.polargrizzly.com"
29
+ URLS = {
30
+ "token": f"{DASHBOARD_API}/v2/token",
31
+ "token_cookie": f"{DASHBOARD_API}/v2/tokenCookie",
32
+ "cookie_token": f"{TB_API}/v2/cookieToken",
33
+ "pb_auth": f"{PB_API}/auth",
34
+ "pb_user": f"{PB_API}/user",
35
+ "pb_vpns": f"{PB_API}/vpns",
36
+ }
37
+ APP_VER = "3.6.1"
38
+
39
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", datefmt="%H:%M:%S")
40
+ log = logging.getLogger("TB")
41
+
42
+ app = Flask(__name__)
43
+
44
+ class State:
45
+ def __init__(self):
46
+ self.lock = threading.Lock()
47
+ self.connected = self.connecting = False
48
+ self.proxy = None
49
+ self.server_url = self.server_proto = self.vpn_token = ""
50
+ self.region_name = self.country_iso = self.real_ip = self.vpn_ip = ""
51
+ self.started_at = datetime.now(timezone.utc)
52
+ self.last_check = None
53
+ self.last_check_ok = False
54
+ self.connect_errors = 0
55
+ self.total_requests = 0
56
+ self.message = "Not connected"
57
+ S = State()
58
+
59
+ class TBClient:
60
+ def __init__(self):
61
+ self.s = _r.Session()
62
+ self.s.headers.update({"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/115.0",
63
+ "Accept": "application/json", "Content-Type": "application/json"})
64
+ self.device = f"browser-{uuid.uuid4()}"
65
+ self.dash_tok = self.tb_tok = self.pb_tok = self.vpn_tok = ""
66
+ self.servers = []; self.region_name = ""; self.country_iso = ""
67
+
68
+ def login(self):
69
+ try:
70
+ r = self.s.post(URLS["token"], json={"username": TB_EMAIL, "password": TB_PASSWORD,
71
+ "grant_type": "password", "device": self.device}, timeout=30)
72
+ if r.status_code != 200:
73
+ log.error(f" Login failed: {r.text[:200]}"); return False
74
+ self.dash_tok = r.json().get("access_token", "")
75
+ if not self.dash_tok: log.error(" No token"); return False
76
+ log.info(f" Logged in: {self.dash_tok[:30]}..."); return True
77
+ except Exception as e: log.error(f" Login: {e}"); return False
78
+
79
+ def session_cookie(self):
80
+ try:
81
+ r = self.s.post(URLS["token_cookie"], json={},
82
+ headers={"Authorization": f"Bearer {self.dash_tok}"}, timeout=30)
83
+ if r.status_code != 200: log.error(f" Cookie: {r.text[:200]}"); return False
84
+ return True
85
+ except Exception as e: log.error(f" Cookie: {e}"); return False
86
+
87
+ def cookie_token(self):
88
+ try:
89
+ r = self.s.post(URLS["cookie_token"], headers={
90
+ "Authorization": f"Bearer {self.dash_tok}", "device": self.device,
91
+ "tunnelbear-app-id": "com.tunnelbear.browser", "tunnelbear-app-version": APP_VER,
92
+ "tunnelbear-platform": "Firefox", "tunnelbear-platform-version": "Firefox"}, timeout=30)
93
+ if r.status_code != 200: log.error(f" CookieToken: {r.text[:200]}"); return False
94
+ self.tb_tok = r.json().get("access_token", "")
95
+ if not self.tb_tok: log.error(" No token"); return False
96
+ log.info(f" TB token: {self.tb_tok[:30]}..."); return True
97
+ except Exception as e: log.error(f" CookieToken: {e}"); return False
98
+
99
+ def pb_auth(self):
100
+ try:
101
+ r = self.s.post(URLS["pb_auth"], json={"partner": "tunnelbear", "token": self.tb_tok}, timeout=30)
102
+ if r.status_code != 200: log.error(f" PB auth: {r.text[:200]}"); return False
103
+ h = r.headers.get("authorization", "")
104
+ if h.startswith("Bearer "): self.pb_tok = h[7:]
105
+ else: log.error(" No auth header"); return False
106
+ log.info(f" PB token: {self.pb_tok[:30]}..."); return True
107
+ except Exception as e: log.error(f" PB auth: {e}"); return False
108
+
109
+ def user(self):
110
+ try:
111
+ r = self.s.get(URLS["pb_user"], headers={"Authorization": f"Bearer {self.pb_tok}"}, timeout=30)
112
+ if r.status_code != 200: return False
113
+ d = r.json(); self.vpn_tok = d.get("vpn_token", "")
114
+ if not self.vpn_tok: return False
115
+ log.info(f" VPN Token: {self.vpn_tok}"); return True
116
+ except Exception as e: log.error(f" User: {e}"); return False
117
+
118
+ def get_servers(self, country=None):
119
+ url = f"{URLS['pb_vpns']}/countries/{country}" if country else URLS["pb_vpns"]
120
+ try:
121
+ r = self.s.get(url, headers={"Authorization": f"Bearer {self.pb_tok}"}, timeout=30)
122
+ if r.status_code != 200: return False
123
+ d = r.json(); vpns = d.get("vpns", [])
124
+ self.region_name = d.get("region_name", "?"); self.country_iso = d.get("country_iso", "?")
125
+ self.servers = [{"url": v["url"], "protocol": v.get("protocol", "udp")} for v in vpns if "url" in v]
126
+ log.info(f" Region: {self.region_name} ({self.country_iso}), {len(self.servers)} servers")
127
+ return bool(self.servers)
128
+ except Exception as e: log.error(f" Servers: {e}"); return False
129
+
130
+ def auth(self):
131
+ return (self.login() and self.session_cookie() and self.cookie_token()
132
+ and self.pb_auth() and self.user() and self.get_servers(TB_COUNTRY))
133
+
134
+ class TLSProxy:
135
+ def __init__(self, host, token):
136
+ self.host, self.port, self.token = host, 8080, token
137
+ self._ctx = ssl.create_default_context()
138
+ self._ctx.check_hostname = False; self._ctx.verify_mode = ssl.CERT_NONE
139
+
140
+ def _ssl(self):
141
+ raw = socket.create_connection((self.host, self.port), timeout=30)
142
+ try: return self._ctx.wrap_socket(raw, server_hostname=self.host)
143
+ except: raw.close(); raise
144
+
145
+ def _rr(self, s):
146
+ buf = b""
147
+ while b"\r\n\r\n" not in buf:
148
+ c = s.recv(4096)
149
+ if not c: raise ConnectionError("closed")
150
+ buf += c
151
+ idx = buf.index(b"\r\n\r\n")
152
+ code = int(buf[:idx].decode(errors="ignore").split(" ", 2)[1].split()[0])
153
+ return code, buf[idx+4:]
154
+
155
+ def connect(self, th, tp):
156
+ ab = base64.b64encode(f"{self.token}:{self.token}".encode()).decode()
157
+ s = self._ssl()
158
+ 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())
159
+ code, rem = self._rr(s)
160
+ if code == 200: return s
161
+ if code == 407:
162
+ s.close(); s = self._ssl()
163
+ s.sendall(f"CONNECT {th}:{tp} HTTP/1.1\r\nHost: {th}:{tp}\r\nUser-Agent: TunnelBear/{APP_VER}\r\n\r\n".encode())
164
+ code, _ = self._rr(s)
165
+ if code == 407:
166
+ 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())
167
+ code, rem = self._rr(s)
168
+ if code == 200: return s
169
+ s.close(); raise ConnectionError(f"CONNECT HTTP {code}")
170
+
171
+ class TCPProxy:
172
+ def __init__(self, host, token):
173
+ self.host, self.port, self.token = host, 8080, token
174
+ def connect(self, th, tp):
175
+ ab = base64.b64encode(f"{self.token}:{self.token}".encode()).decode()
176
+ s = socket.create_connection((self.host, self.port), timeout=30)
177
+ 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())
178
+ buf = b""
179
+ while b"\r\n\r\n" not in buf:
180
+ c = s.recv(4096)
181
+ if not c: raise ConnectionError("closed")
182
+ buf += c
183
+ if b"200" not in buf.split(b"\r\n")[0]: s.close(); raise ConnectionError("CONNECT failed")
184
+ return s
185
+
186
+ class S5H(socketserver.StreamRequestHandler):
187
+ proxy = None
188
+ def handle(self):
189
+ try: self._run()
190
+ except: pass
191
+ finally:
192
+ try: self.connection.close()
193
+ except: pass
194
+ def _run(self):
195
+ c = self.connection
196
+ vn = self._rx(c, 2)
197
+ if vn[0] != 5: return
198
+ ms = self._rx(c, vn[1])
199
+ if 0 in ms: c.sendall(b"\x05\x00")
200
+ elif 2 in ms:
201
+ c.sendall(b"\x05\x02"); a = self._rx(c, 2); self._rx(c, a[1])
202
+ pl = self._rx(c, 1)[0]; self._rx(c, pl); c.sendall(b"\x01\x00")
203
+ else: c.sendall(b"\x05\xFF"); return
204
+ h = self._rx(c, 4)
205
+ if h[1] != 1: c.sendall(b"\x05"+bytes([7,0,1,0,0,0,0,0,0,0])); return
206
+ at = h[3]
207
+ if at == 1: th = socket.inet_ntoa(self._rx(c, 4))
208
+ elif at == 3: th = self._rx(c, self._rx(c, 1)[0]).decode()
209
+ elif at == 4: th = socket.inet_ntop(socket.AF_INET6, self._rx(c, 16))
210
+ else: c.sendall(b"\x05"+bytes([8,0,1,0,0,0,0,0,0,0])); return
211
+ tp = struct.unpack("!H", self._rx(c, 2))[0]
212
+ log.info(f" SOCKS5 -> {th}:{tp}")
213
+ try:
214
+ if not self.proxy: raise Exception("no proxy")
215
+ remote = self.proxy.connect(th, tp)
216
+ except Exception as e:
217
+ log.error(f" Tunnel: {e}")
218
+ c.sendall(b"\x05"+bytes([5,0,1,0,0,0,0,0,0,0])); return
219
+ c.sendall(b"\x05"+bytes([0,0,1,0,0,0,0,0,0,0]))
220
+ c.settimeout(300); remote.settimeout(300)
221
+ def _pump(src, dst):
222
+ try:
223
+ while True:
224
+ data = src.recv(8192)
225
+ if not data: break
226
+ dst.sendall(data)
227
+ except: pass
228
+ finally:
229
+ try: dst.shutdown(socket.SHUT_WR)
230
+ except: pass
231
+ t1 = threading.Thread(target=_pump, args=(c, remote), daemon=True)
232
+ t2 = threading.Thread(target=_pump, args=(remote, c), daemon=True)
233
+ t1.start(); t2.start(); t1.join(timeout=300); t2.join(timeout=300)
234
+ @staticmethod
235
+ def _rx(s, n):
236
+ b = bytearray()
237
+ while len(b) < n:
238
+ c = s.recv(n - len(b))
239
+ if not c: raise ConnectionError("closed")
240
+ b.extend(c)
241
+ return bytes(b)
242
+
243
+ class S5Server(socketserver.ThreadingMixIn, socketserver.TCPServer):
244
+ allow_reuse_address = True; daemon_threads = True
245
+
246
+ def real_ip():
247
+ try: return _r.get("https://api.ipify.org?format=json", timeout=10).json().get("ip", "")
248
+ except: return ""
249
+
250
+ def vpn_test(p):
251
+ try:
252
+ s = p.connect("api.ipify.org", 80)
253
+ s.sendall(b"GET /?format=json HTTP/1.1\r\nHost: api.ipify.org\r\nConnection: close\r\n\r\n")
254
+ buf = b""
255
+ while True:
256
+ c = s.recv(4096)
257
+ if not c: break
258
+ buf += c
259
+ s.close()
260
+ body = buf.decode(errors="ignore").split("\r\n\r\n", 1)
261
+ if len(body) > 1: return json.loads(body[1]).get("ip", "")
262
+ except: return ""
263
+
264
+ def start_socks5(proxy):
265
+ S5H.proxy = proxy
266
+ srv = S5Server(("127.0.0.1", SOCKS5_PORT), S5H)
267
+ threading.Thread(target=srv.serve_forever, daemon=True).start()
268
+ log.info(f"SOCKS5 on :{SOCKS5_PORT}")
269
+ return srv
270
+
271
+ socks5_srv = None
272
+
273
+ def vpn_loop():
274
+ global socks5_srv
275
+ backoff = 5
276
+ while True:
277
+ with S.lock: S.connecting = True; S.connected = False; S.message = "Connecting..."
278
+ log.info("=== VPN: connecting ===")
279
+ try:
280
+ cl = TBClient()
281
+ if not cl.auth(): raise Exception("Auth failed")
282
+ proxy = None; wurl = None
283
+ for sv in cl.servers:
284
+ u, pr = sv["url"], sv["protocol"]
285
+ log.info(f" Try {u} ({pr})")
286
+ for PC in ([TLSProxy, TCPProxy] if not NO_TLS else [TCPProxy]):
287
+ p = PC(u, cl.vpn_tok)
288
+ ip = vpn_test(p)
289
+ if ip: proxy = p; wurl = u; log.info(f" OK {PC.__name__} ip={ip}"); break
290
+ if proxy: break
291
+ if not wurl: raise Exception("No working server")
292
+ rip = real_ip(); vip = vpn_test(proxy)
293
+ with S.lock:
294
+ S.connected = S.last_check_ok = True; S.connecting = False
295
+ S.proxy = proxy; S.server_url = wurl; S.vpn_token = cl.vpn_tok
296
+ S.region_name = cl.region_name; S.country_iso = cl.country_iso
297
+ S.real_ip = rip; S.vpn_ip = vip; S.connect_errors = 0
298
+ S.message = f"Connected: {S.region_name} ({S.country_iso})"
299
+ if socks5_srv:
300
+ try: socks5_srv.shutdown()
301
+ except: pass
302
+ socks5_srv = start_socks5(proxy)
303
+ log.info(f"=== VPN UP: {S.region_name} ip={vip} ===")
304
+ while True:
305
+ time.sleep(60)
306
+ ip = vpn_test(proxy)
307
+ now = datetime.now(timezone.utc).isoformat()
308
+ with S.lock: S.last_check = now; S.vpn_ip = ip or S.vpn_ip; S.last_check_ok = bool(ip)
309
+ if not ip: log.warning("Health fail"); break
310
+ except Exception as e:
311
+ log.error(f"VPN error: {e}")
312
+ with S.lock: S.connected = False; S.connecting = False; S.connect_errors += 1; S.message = f"Error: {e}"
313
+ if socks5_srv:
314
+ try: socks5_srv.shutdown()
315
+ except: pass
316
+ log.info(f"Retry in {backoff}s"); time.sleep(backoff); backoff = min(backoff * 2, 300)
317
+
318
+ @app.route("/health")
319
+ def health():
320
+ with S.lock: conn, msg = S.connected, S.message
321
+ if request.args.get("live") == "1" and conn:
322
+ ip = vpn_test(S.proxy); now = datetime.now(timezone.utc).isoformat()
323
+ with S.lock: S.last_check = now; S.last_check_ok = bool(ip); S.vpn_ip = ip or S.vpn_ip
324
+ if ip: return jsonify(status="ok", connected=True, vpn_ip=ip, region=S.region_name, country=S.country_iso), 200
325
+ return jsonify(status="degraded", error="tunnel test failed"), 503
326
+ if conn:
327
+ return jsonify(status="ok", connected=True, vpn_ip=S.vpn_ip, real_ip=S.real_ip,
328
+ region=S.region_name, country=S.country_iso, server=S.server_url,
329
+ last_check=S.last_check, last_ok=S.last_check_ok,
330
+ uptime=int((datetime.now(timezone.utc)-S.started_at).total_seconds()),
331
+ requests=S.total_requests, message=msg), 200
332
+ return jsonify(status="unhealthy", connected=False, message=msg, errors=S.connect_errors), 503
333
+
334
+ @app.route("/ip")
335
+ def ip():
336
+ if not S.connected: return jsonify(error="VPN not connected"), 503
337
+ ip = vpn_test(S.proxy)
338
+ 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)
339
+ if ip: return jsonify(vpn_ip=ip, real_ip=S.real_ip, working=ip != S.real_ip, region=S.region_name), 200
340
+ return jsonify(error="tunnel test failed"), 503
341
+
342
+ @app.route("/fetch")
343
+ def fetch():
344
+ url = request.args.get("url")
345
+ if not url: return jsonify(error="missing ?url="), 400
346
+ if urlparse(url).scheme not in ("http", "https"): return jsonify(error="http/https only"), 400
347
+ S.total_requests += 1
348
+ try:
349
+ px = {"http": f"socks5h://127.0.0.1:{SOCKS5_PORT}", "https": f"socks5h://127.0.0.1:{SOCKS5_PORT}"}
350
+ r = _r.get(url, proxies=px, timeout=int(request.args.get("timeout", 30)))
351
+ return Response(r.content, mimetype="text/plain", headers={"X-VPN-IP": S.vpn_ip or "", "X-Region": S.region_name})
352
+ except Exception as e: return jsonify(error=str(e)), 502
353
+
354
+ @app.route("/status")
355
+ def status():
356
+ with S.lock:
357
+ return jsonify(connected=S.connected, connecting=S.connecting, message=S.message,
358
+ server=S.server_url, vpn_ip=S.vpn_ip, real_ip=S.real_ip,
359
+ region=S.region_name, country=S.country_iso, socks5=SOCKS5_PORT,
360
+ uptime=int((datetime.now(timezone.utc)-S.started_at).total_seconds()),
361
+ last_check=S.last_check, last_ok=S.last_check_ok,
362
+ errors=S.connect_errors, requests=S.total_requests)
363
+
364
+ @app.route("/")
365
+ def index():
366
+ return jsonify(service="TunnelBear VPN Proxy",
367
+ endpoints={"/health": "health check (?live=1)", "/ip": "VPN exit IP",
368
+ "/fetch?url=X": "fetch through VPN", "/status": "full status"})
369
+
370
+ if __name__ == "__main__":
371
+ log.info(f"Starting — country={TB_COUNTRY or 'auto'} tls={not NO_TLS} port={PORT}")
372
+ threading.Thread(target=vpn_loop, daemon=True).start()
373
+ app.run(host="0.0.0.0", port=PORT, threaded=True)
374
+ PYEOF
375
+
376
+ EXPOSE 7860
377
+ CMD ["gunicorn", "--bind", "0.0.0.0:7860", "--workers", "1", "--threads", "4", "--timeout", "120", "main:app"]