wchrome 4.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wchrome-4.0.0/PKG-INFO +7 -0
- wchrome-4.0.0/pyproject.toml +14 -0
- wchrome-4.0.0/setup.cfg +4 -0
- wchrome-4.0.0/src/wchrome/__init__.py +616 -0
- wchrome-4.0.0/src/wchrome.egg-info/PKG-INFO +7 -0
- wchrome-4.0.0/src/wchrome.egg-info/SOURCES.txt +7 -0
- wchrome-4.0.0/src/wchrome.egg-info/dependency_links.txt +1 -0
- wchrome-4.0.0/src/wchrome.egg-info/entry_points.txt +2 -0
- wchrome-4.0.0/src/wchrome.egg-info/top_level.txt +1 -0
wchrome-4.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wchrome"
|
|
7
|
+
version = "4.0.0"
|
|
8
|
+
description = "Unblocked Chrome — routes browser traffic through Cloudflare WARP tunnel"
|
|
9
|
+
requires-python = ">=3.7"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [{name = "hlehman27"}]
|
|
12
|
+
|
|
13
|
+
[project.scripts]
|
|
14
|
+
wchrome = "wchrome:main"
|
wchrome-4.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,616 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import subprocess, sys, os, json, shutil, tempfile, signal, time, socket
|
|
3
|
+
import atexit, urllib.request, urllib.error, hashlib, base64, threading
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
|
|
6
|
+
# ── Internal ─────────────────────────────────────────────────────
|
|
7
|
+
_V = 4
|
|
8
|
+
_UF = os.path.expanduser("~/.unblock_data.json")
|
|
9
|
+
_SK = "unblock_9x7k2m"
|
|
10
|
+
_DID = base64.b64decode(
|
|
11
|
+
"MTJDdmxPMEduZjVmU0ZudnNZOGFsdExMQ0VuSFozNkpRZWNMM0lsQ1hQU2c="
|
|
12
|
+
).decode()
|
|
13
|
+
|
|
14
|
+
CHROME_PATHS = [
|
|
15
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
16
|
+
"/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary",
|
|
17
|
+
"/Applications/Chromium.app/Contents/MacOS/Chromium",
|
|
18
|
+
]
|
|
19
|
+
WARP_SOCKS_PORT = 40000
|
|
20
|
+
|
|
21
|
+
# ── Colors ───────────────────────────────────────────────────────
|
|
22
|
+
R = "\033[0m" # reset
|
|
23
|
+
B = "\033[1m" # bold
|
|
24
|
+
DIM = "\033[2m" # dim
|
|
25
|
+
RED = "\033[91m"
|
|
26
|
+
GRN = "\033[92m"
|
|
27
|
+
YLW = "\033[93m"
|
|
28
|
+
BLU = "\033[94m"
|
|
29
|
+
MAG = "\033[95m"
|
|
30
|
+
CYN = "\033[96m"
|
|
31
|
+
WHT = "\033[97m"
|
|
32
|
+
|
|
33
|
+
BANNER = f"""{CYN}{B}
|
|
34
|
+
╔══════════════════════════════════════════════╗
|
|
35
|
+
║ ║
|
|
36
|
+
║ ██╗ ██╗███╗ ██╗██████╗ ██╗ ██╗ ║
|
|
37
|
+
║ ██║ ██║████╗ ██║██╔══██╗██║ ██║ ║
|
|
38
|
+
║ ██║ ██║██╔██╗ ██║██████╔╝██║ ██║ ║
|
|
39
|
+
║ ██║ ██║██║╚██╗██║██╔══██╗██║ ██║ ║
|
|
40
|
+
║ ╚██████╔╝██║ ╚████║██████╔╝███████╗██║ ║
|
|
41
|
+
║ ╚═════╝ ╚═╝ ╚═══╝╚═════╝ ╚══════╝╚═╝ ║
|
|
42
|
+
║ ║
|
|
43
|
+
║ {WHT}Cloudflare WARP Tunnel{CYN} ║
|
|
44
|
+
║ {DIM}{WHT}version {_V}{R}{CYN}{B} ║
|
|
45
|
+
╚══════════════════════════════════════════════╝{R}
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── UI Helpers ───────────────────────────────────────────────────
|
|
50
|
+
def _clear():
|
|
51
|
+
os.system("clear" if os.name != "nt" else "cls")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _spinner(msg, done_event, success_flag):
|
|
55
|
+
"""Animated spinner that runs in a thread."""
|
|
56
|
+
frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
57
|
+
i = 0
|
|
58
|
+
while not done_event.is_set():
|
|
59
|
+
print(f"\r {CYN}{frames[i % len(frames)]}{R} {msg}", end="", flush=True)
|
|
60
|
+
i += 1
|
|
61
|
+
time.sleep(0.08)
|
|
62
|
+
if success_flag[0]:
|
|
63
|
+
print(f"\r {GRN}✓{R} {msg} ")
|
|
64
|
+
else:
|
|
65
|
+
print(f"\r {RED}✗{R} {msg} ")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _with_spinner(msg, func):
|
|
69
|
+
"""Run func() with a spinner animation. Returns func's result."""
|
|
70
|
+
done = threading.Event()
|
|
71
|
+
ok = [False]
|
|
72
|
+
t = threading.Thread(target=_spinner, args=(msg, done, ok), daemon=True)
|
|
73
|
+
t.start()
|
|
74
|
+
try:
|
|
75
|
+
result = func()
|
|
76
|
+
ok[0] = True
|
|
77
|
+
return result
|
|
78
|
+
except Exception as e:
|
|
79
|
+
ok[0] = False
|
|
80
|
+
raise e
|
|
81
|
+
finally:
|
|
82
|
+
done.set()
|
|
83
|
+
t.join()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _progress_bar(current, total, label="", width=30):
|
|
87
|
+
filled = int(width * current / total)
|
|
88
|
+
bar = f"{'█' * filled}{'░' * (width - filled)}"
|
|
89
|
+
print(f"\r {CYN}{bar}{R} {label}", end="", flush=True)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _status(icon, color, msg):
|
|
93
|
+
print(f" {color}{icon}{R} {msg}")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _box(lines, color=CYN):
|
|
97
|
+
w = max(len(l) for l in lines) + 4
|
|
98
|
+
print(f" {color}╭{'─' * w}╮{R}")
|
|
99
|
+
for l in lines:
|
|
100
|
+
pad = w - len(l) - 2
|
|
101
|
+
print(f" {color}│{R} {l}{' ' * pad} {color}│{R}")
|
|
102
|
+
print(f" {color}╰{'─' * w}╯{R}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# ── Integrity ────────────────────────────────────────────────────
|
|
106
|
+
def _sig(data):
|
|
107
|
+
return hashlib.sha256(f"{_SK}:{data}".encode()).hexdigest()[:16]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _self_check():
|
|
111
|
+
try:
|
|
112
|
+
with open(__file__, "r") as f:
|
|
113
|
+
src = f.read()
|
|
114
|
+
markers = [
|
|
115
|
+
"_remote_cfg", "_enforce", "_DID", "_sig", "_self_check",
|
|
116
|
+
"fetch_control", "urllib.request", "_recheck",
|
|
117
|
+
]
|
|
118
|
+
for m in markers:
|
|
119
|
+
if m not in src:
|
|
120
|
+
_status("✗", RED, "Integrity check failed.")
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ── Usage Tracking (signed) ──────────────────────────────────────
|
|
127
|
+
def _load_usage():
|
|
128
|
+
try:
|
|
129
|
+
with open(_UF, "r") as f:
|
|
130
|
+
data = json.load(f)
|
|
131
|
+
stored_sig = data.pop("_s", "")
|
|
132
|
+
check = _sig(json.dumps(data, sort_keys=True))
|
|
133
|
+
if stored_sig != check:
|
|
134
|
+
return {"total": 0, "daily": {}, "first": None, "streak": 0}
|
|
135
|
+
return data
|
|
136
|
+
except (FileNotFoundError, ValueError, json.JSONDecodeError):
|
|
137
|
+
return {"total": 0, "daily": {}, "first": None, "streak": 0}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _save_usage(data):
|
|
141
|
+
data.pop("_s", None)
|
|
142
|
+
data["_s"] = _sig(json.dumps(data, sort_keys=True))
|
|
143
|
+
with open(_UF, "w") as f:
|
|
144
|
+
json.dump(data, f)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _inc_usage():
|
|
148
|
+
data = _load_usage()
|
|
149
|
+
data["total"] = data.get("total", 0) + 1
|
|
150
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
151
|
+
yesterday = (datetime.now().replace(hour=0, minute=0, second=0)
|
|
152
|
+
.__class__(datetime.now().year, datetime.now().month,
|
|
153
|
+
max(1, datetime.now().day - 1))).strftime("%Y-%m-%d")
|
|
154
|
+
daily = data.get("daily", {})
|
|
155
|
+
daily[today] = daily.get(today, 0) + 1
|
|
156
|
+
|
|
157
|
+
# Streak tracking
|
|
158
|
+
if yesterday in daily:
|
|
159
|
+
data["streak"] = data.get("streak", 0) + (1 if daily[today] == 1 else 0)
|
|
160
|
+
elif daily[today] == 1:
|
|
161
|
+
data["streak"] = 1
|
|
162
|
+
|
|
163
|
+
# First use tracking
|
|
164
|
+
if not data.get("first"):
|
|
165
|
+
data["first"] = today
|
|
166
|
+
|
|
167
|
+
# Cleanup old entries
|
|
168
|
+
keys = sorted(daily.keys())
|
|
169
|
+
if len(keys) > 30:
|
|
170
|
+
for k in keys[:-30]:
|
|
171
|
+
del daily[k]
|
|
172
|
+
data["daily"] = daily
|
|
173
|
+
_save_usage(data)
|
|
174
|
+
return data
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# ── Remote Control ───────────────────────────────────────────────
|
|
178
|
+
def _remote_cfg():
|
|
179
|
+
# fetch_control from remote doc
|
|
180
|
+
url = f"https://docs.google.com/document/d/{_DID}/export?format=txt"
|
|
181
|
+
try:
|
|
182
|
+
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
|
183
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
184
|
+
raw = resp.read().decode("utf-8", errors="ignore").strip()
|
|
185
|
+
except (urllib.error.URLError, OSError):
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
cfg = {"on": True, "raw": raw}
|
|
189
|
+
for line in raw.splitlines():
|
|
190
|
+
line = line.strip()
|
|
191
|
+
if not line:
|
|
192
|
+
continue
|
|
193
|
+
low = line.lower()
|
|
194
|
+
|
|
195
|
+
if low in ("true", "false"):
|
|
196
|
+
cfg["on"] = low == "true"
|
|
197
|
+
elif low.startswith("message:"):
|
|
198
|
+
cfg["msg"] = line.split(":", 1)[1].strip()
|
|
199
|
+
elif low.startswith("limit:"):
|
|
200
|
+
try:
|
|
201
|
+
cfg["lim"] = int(line.split(":", 1)[1].strip())
|
|
202
|
+
except ValueError:
|
|
203
|
+
pass
|
|
204
|
+
elif low.startswith("version:"):
|
|
205
|
+
try:
|
|
206
|
+
cfg["ver"] = int(line.split(":", 1)[1].strip())
|
|
207
|
+
except ValueError:
|
|
208
|
+
pass
|
|
209
|
+
elif low.startswith("hours:"):
|
|
210
|
+
try:
|
|
211
|
+
parts = line.split(":", 1)[1].strip().split("-")
|
|
212
|
+
cfg["h0"] = int(parts[0])
|
|
213
|
+
cfg["h1"] = int(parts[1])
|
|
214
|
+
except (ValueError, IndexError):
|
|
215
|
+
pass
|
|
216
|
+
elif low.startswith("blockmsg:"):
|
|
217
|
+
cfg["bmsg"] = line.split(":", 1)[1].strip()
|
|
218
|
+
|
|
219
|
+
return cfg
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _enforce(cfg):
|
|
223
|
+
if cfg is None:
|
|
224
|
+
_status("✗", RED, "Cannot verify access. Check your internet.")
|
|
225
|
+
return False
|
|
226
|
+
|
|
227
|
+
bmsg = cfg.get("bmsg", "Blocked by creator for now.")
|
|
228
|
+
|
|
229
|
+
if not cfg.get("on", True):
|
|
230
|
+
print()
|
|
231
|
+
_box([bmsg], RED)
|
|
232
|
+
print()
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
rv = cfg.get("ver")
|
|
236
|
+
if rv and _V < rv:
|
|
237
|
+
print()
|
|
238
|
+
_box([
|
|
239
|
+
f"UPDATE REQUIRED",
|
|
240
|
+
f"You have v{_V}, need v{rv}",
|
|
241
|
+
f"Get the latest from the creator",
|
|
242
|
+
], YLW)
|
|
243
|
+
print()
|
|
244
|
+
return False
|
|
245
|
+
|
|
246
|
+
h0, h1 = cfg.get("h0"), cfg.get("h1")
|
|
247
|
+
if h0 is not None and h1 is not None:
|
|
248
|
+
now_h = datetime.now().hour
|
|
249
|
+
if h0 <= h1:
|
|
250
|
+
ok = h0 <= now_h < h1
|
|
251
|
+
else:
|
|
252
|
+
ok = now_h >= h0 or now_h < h1
|
|
253
|
+
if not ok:
|
|
254
|
+
print()
|
|
255
|
+
_box([
|
|
256
|
+
f"ACCESS RESTRICTED",
|
|
257
|
+
f"Available {h0}:00 - {h1}:00 only",
|
|
258
|
+
f"Current time: {datetime.now().strftime('%H:%M')}",
|
|
259
|
+
], RED)
|
|
260
|
+
print()
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
lim = cfg.get("lim")
|
|
264
|
+
if lim:
|
|
265
|
+
data = _load_usage()
|
|
266
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
267
|
+
tc = data.get("daily", {}).get(today, 0)
|
|
268
|
+
if tc >= lim:
|
|
269
|
+
print()
|
|
270
|
+
_box([
|
|
271
|
+
f"DAILY LIMIT REACHED",
|
|
272
|
+
f"{tc}/{lim} uses today",
|
|
273
|
+
f"Try again tomorrow",
|
|
274
|
+
], RED)
|
|
275
|
+
print()
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
m = cfg.get("msg")
|
|
279
|
+
if m:
|
|
280
|
+
print()
|
|
281
|
+
_box([m], MAG)
|
|
282
|
+
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _recheck():
|
|
287
|
+
cfg = _remote_cfg()
|
|
288
|
+
if cfg is None or not cfg.get("on", True):
|
|
289
|
+
_status("✗", RED, "Access denied.")
|
|
290
|
+
sys.exit(0)
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _bg_monitor(proc, cli):
|
|
294
|
+
"""Background thread: re-checks access every 5 minutes while running."""
|
|
295
|
+
while proc.poll() is None:
|
|
296
|
+
time.sleep(300)
|
|
297
|
+
if proc.poll() is not None:
|
|
298
|
+
break
|
|
299
|
+
cfg = _remote_cfg()
|
|
300
|
+
if cfg is not None and not cfg.get("on", True):
|
|
301
|
+
print(f"\n\n {RED}{B}ACCESS REVOKED — shutting down.{R}\n")
|
|
302
|
+
proc.terminate()
|
|
303
|
+
_disconnect_warp(cli)
|
|
304
|
+
break
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
# ── Cloudflare WARP ──────────────────────────────────────────────
|
|
308
|
+
def _find_warp():
|
|
309
|
+
w = shutil.which("warp-cli")
|
|
310
|
+
if w:
|
|
311
|
+
return w
|
|
312
|
+
w = "/Applications/Cloudflare WARP.app/Contents/Resources/warp-cli"
|
|
313
|
+
if os.path.exists(w):
|
|
314
|
+
return w
|
|
315
|
+
return None
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _install_warp():
|
|
319
|
+
brew = shutil.which("brew")
|
|
320
|
+
if not brew:
|
|
321
|
+
_status("✗", RED, "Homebrew not found. Install from https://brew.sh")
|
|
322
|
+
sys.exit(1)
|
|
323
|
+
|
|
324
|
+
def _do_install():
|
|
325
|
+
r = subprocess.run([brew, "install", "--cask", "cloudflare-warp"],
|
|
326
|
+
capture_output=True, text=True)
|
|
327
|
+
if r.returncode != 0:
|
|
328
|
+
raise RuntimeError(r.stderr)
|
|
329
|
+
time.sleep(3)
|
|
330
|
+
return True
|
|
331
|
+
|
|
332
|
+
_with_spinner("Installing Cloudflare WARP...", _do_install)
|
|
333
|
+
|
|
334
|
+
w = _find_warp()
|
|
335
|
+
if not w:
|
|
336
|
+
_status("✗", RED, "WARP installed but warp-cli not found.")
|
|
337
|
+
_status("→", YLW, "Try opening 'Cloudflare WARP' from Applications, then re-run.")
|
|
338
|
+
sys.exit(1)
|
|
339
|
+
return w
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _port_open(port):
|
|
343
|
+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
344
|
+
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _setup_warp(cli):
|
|
348
|
+
st = subprocess.run([cli, "--accept-tos", "status"],
|
|
349
|
+
capture_output=True, text=True)
|
|
350
|
+
so = st.stdout.strip().lower()
|
|
351
|
+
if "registration missing" in so or st.returncode != 0:
|
|
352
|
+
def _reg():
|
|
353
|
+
subprocess.run([cli, "--accept-tos", "registration", "new"],
|
|
354
|
+
capture_output=True, text=True)
|
|
355
|
+
return True
|
|
356
|
+
_with_spinner("Registering with Cloudflare...", _reg)
|
|
357
|
+
|
|
358
|
+
subprocess.run([cli, "--accept-tos", "mode", "proxy"],
|
|
359
|
+
capture_output=True, text=True)
|
|
360
|
+
subprocess.run([cli, "--accept-tos", "proxy", "port", str(WARP_SOCKS_PORT)],
|
|
361
|
+
capture_output=True, text=True)
|
|
362
|
+
subprocess.run([cli, "--accept-tos", "connect"],
|
|
363
|
+
capture_output=True, text=True)
|
|
364
|
+
|
|
365
|
+
# Animated connection with progress bar
|
|
366
|
+
print()
|
|
367
|
+
for i in range(30):
|
|
368
|
+
_progress_bar(i + 1, 30, f"Connecting to WARP...")
|
|
369
|
+
if _port_open(WARP_SOCKS_PORT):
|
|
370
|
+
_progress_bar(30, 30, f"Connecting to WARP...")
|
|
371
|
+
print()
|
|
372
|
+
_status("✓", GRN, "WARP tunnel active!")
|
|
373
|
+
return True
|
|
374
|
+
time.sleep(1)
|
|
375
|
+
|
|
376
|
+
s2 = subprocess.run([cli, "--accept-tos", "status"],
|
|
377
|
+
capture_output=True, text=True)
|
|
378
|
+
print()
|
|
379
|
+
_status("✗", RED, f"WARP didn't start. Status: {s2.stdout.strip()}")
|
|
380
|
+
_status("→", YLW, "Try opening the Cloudflare WARP app manually.")
|
|
381
|
+
sys.exit(1)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _disconnect_warp(cli):
|
|
385
|
+
subprocess.run([cli, "--accept-tos", "disconnect"],
|
|
386
|
+
capture_output=True, text=True)
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
# ── Chrome ───────────────────────────────────────────────────────
|
|
390
|
+
def _find_chrome():
|
|
391
|
+
for p in CHROME_PATHS:
|
|
392
|
+
if os.path.exists(p):
|
|
393
|
+
return p
|
|
394
|
+
_status("✗", RED, "Could not find Chrome.")
|
|
395
|
+
sys.exit(1)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _chrome_dir():
|
|
399
|
+
return os.path.expanduser("~/Library/Application Support/Google/Chrome")
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _find_profiles(cd):
|
|
403
|
+
pp = []
|
|
404
|
+
for n in sorted(os.listdir(cd)):
|
|
405
|
+
fp = os.path.join(cd, n)
|
|
406
|
+
if os.path.isdir(fp) and (n == "Default" or n.startswith("Profile ")):
|
|
407
|
+
# Try to get profile name from Preferences
|
|
408
|
+
pref = os.path.join(fp, "Preferences")
|
|
409
|
+
name = n
|
|
410
|
+
try:
|
|
411
|
+
with open(pref, "r") as f:
|
|
412
|
+
d = json.load(f)
|
|
413
|
+
name = d.get("profile", {}).get("name", n)
|
|
414
|
+
except Exception:
|
|
415
|
+
pass
|
|
416
|
+
pp.append((n, name))
|
|
417
|
+
return pp
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _pick_profile(pp):
|
|
421
|
+
if len(pp) == 1:
|
|
422
|
+
return pp[0][0]
|
|
423
|
+
print(f"\n {B}Which Chrome profile?{R}")
|
|
424
|
+
for i, (folder, name) in enumerate(pp, 1):
|
|
425
|
+
label = f"{name}" if name != folder else folder
|
|
426
|
+
print(f" {CYN}{i}{R}) {label}")
|
|
427
|
+
while True:
|
|
428
|
+
c = input(f"\n {B}Choose [1-{len(pp)}]:{R} ").strip()
|
|
429
|
+
if c.isdigit() and 1 <= int(c) <= len(pp):
|
|
430
|
+
return pp[int(c) - 1][0]
|
|
431
|
+
print(f" {RED}Invalid, try again.{R}")
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _copy_profile(cd, chosen, tmp):
|
|
435
|
+
src = os.path.join(cd, chosen)
|
|
436
|
+
dst = os.path.join(tmp, chosen)
|
|
437
|
+
if not os.path.isdir(dst):
|
|
438
|
+
essentials = [
|
|
439
|
+
"Bookmarks", "Cookies", "Login Data", "Web Data",
|
|
440
|
+
"Preferences", "Secure Preferences", "Extensions",
|
|
441
|
+
"Local Extension Settings", "Extension State",
|
|
442
|
+
"Favicons", "History", "Top Sites",
|
|
443
|
+
]
|
|
444
|
+
os.makedirs(dst, exist_ok=True)
|
|
445
|
+
existing = [item for item in essentials
|
|
446
|
+
if os.path.exists(os.path.join(src, item))]
|
|
447
|
+
for i, item in enumerate(existing):
|
|
448
|
+
_progress_bar(i + 1, len(existing), f"Copying profile...")
|
|
449
|
+
s = os.path.join(src, item)
|
|
450
|
+
d = os.path.join(dst, item)
|
|
451
|
+
if os.path.isdir(s):
|
|
452
|
+
shutil.copytree(s, d, symlinks=True, dirs_exist_ok=True)
|
|
453
|
+
elif os.path.isfile(s):
|
|
454
|
+
shutil.copy2(s, d)
|
|
455
|
+
print()
|
|
456
|
+
_status("✓", GRN, "Profile ready!")
|
|
457
|
+
ls_s = os.path.join(cd, "Local State")
|
|
458
|
+
ls_d = os.path.join(tmp, "Local State")
|
|
459
|
+
if os.path.exists(ls_s):
|
|
460
|
+
shutil.copy2(ls_s, ls_d)
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def _patch_prefs(tmp, chosen):
|
|
464
|
+
pp = os.path.join(tmp, chosen, "Preferences")
|
|
465
|
+
if not os.path.exists(pp):
|
|
466
|
+
return
|
|
467
|
+
try:
|
|
468
|
+
with open(pp, "r") as f:
|
|
469
|
+
prefs = json.load(f)
|
|
470
|
+
prefs.setdefault("dns_over_https", {})
|
|
471
|
+
prefs["dns_over_https"]["mode"] = "secure"
|
|
472
|
+
prefs["dns_over_https"]["templates"] = (
|
|
473
|
+
"https://cloudflare-dns.com/dns-query "
|
|
474
|
+
"https://dns.google/dns-query"
|
|
475
|
+
)
|
|
476
|
+
prefs.setdefault("session", {})
|
|
477
|
+
prefs["session"]["restore_on_startup"] = 5
|
|
478
|
+
prefs.pop("startup_urls", None)
|
|
479
|
+
with open(pp, "w") as f:
|
|
480
|
+
json.dump(prefs, f)
|
|
481
|
+
except (json.JSONDecodeError, KeyError):
|
|
482
|
+
pass
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _cleanup(tmp):
|
|
486
|
+
"""Remove temp profile on exit."""
|
|
487
|
+
try:
|
|
488
|
+
shutil.rmtree(tmp, ignore_errors=True)
|
|
489
|
+
except Exception:
|
|
490
|
+
pass
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def _format_duration(seconds):
|
|
494
|
+
"""Format seconds into human-readable duration."""
|
|
495
|
+
m, s = divmod(int(seconds), 60)
|
|
496
|
+
h, m = divmod(m, 60)
|
|
497
|
+
if h > 0:
|
|
498
|
+
return f"{h}h {m}m {s}s"
|
|
499
|
+
elif m > 0:
|
|
500
|
+
return f"{m}m {s}s"
|
|
501
|
+
return f"{s}s"
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
# ── Main ─────────────────────────────────────────────────────────
|
|
505
|
+
def main():
|
|
506
|
+
_clear()
|
|
507
|
+
_self_check()
|
|
508
|
+
print(BANNER)
|
|
509
|
+
|
|
510
|
+
# Control check with spinner
|
|
511
|
+
cfg = _with_spinner("Verifying access...", _remote_cfg)
|
|
512
|
+
if not _enforce(cfg):
|
|
513
|
+
sys.exit(0)
|
|
514
|
+
_status("✓", GRN, "Access granted!")
|
|
515
|
+
|
|
516
|
+
# Usage
|
|
517
|
+
data = _inc_usage()
|
|
518
|
+
total = data["total"]
|
|
519
|
+
today_str = datetime.now().strftime("%Y-%m-%d")
|
|
520
|
+
today_count = data.get("daily", {}).get(today_str, 0)
|
|
521
|
+
streak = data.get("streak", 0)
|
|
522
|
+
first = data.get("first")
|
|
523
|
+
|
|
524
|
+
print()
|
|
525
|
+
_box([
|
|
526
|
+
f"Today: {today_count} | All-time: {total} | Streak: {streak} day{'s' if streak != 1 else ''}",
|
|
527
|
+
f"Member since: {first or today_str}",
|
|
528
|
+
], BLU)
|
|
529
|
+
|
|
530
|
+
# Chrome
|
|
531
|
+
chrome = _find_chrome()
|
|
532
|
+
_status("✓", GRN, "Chrome found")
|
|
533
|
+
cd = _chrome_dir()
|
|
534
|
+
if not os.path.isdir(cd):
|
|
535
|
+
_status("✗", RED, f"Chrome data not found at {cd}")
|
|
536
|
+
sys.exit(1)
|
|
537
|
+
|
|
538
|
+
profiles = _find_profiles(cd)
|
|
539
|
+
if not profiles:
|
|
540
|
+
_status("✗", RED, "No Chrome profiles found.")
|
|
541
|
+
sys.exit(1)
|
|
542
|
+
chosen = _pick_profile(profiles)
|
|
543
|
+
|
|
544
|
+
print()
|
|
545
|
+
|
|
546
|
+
# WARP
|
|
547
|
+
cli = _find_warp()
|
|
548
|
+
if not cli:
|
|
549
|
+
cli = _install_warp()
|
|
550
|
+
else:
|
|
551
|
+
_status("✓", GRN, "WARP found")
|
|
552
|
+
_setup_warp(cli)
|
|
553
|
+
atexit.register(_disconnect_warp, cli)
|
|
554
|
+
|
|
555
|
+
# Second check
|
|
556
|
+
_with_spinner("Final verification...", _recheck)
|
|
557
|
+
|
|
558
|
+
# Profile
|
|
559
|
+
print()
|
|
560
|
+
tmp = os.path.join(tempfile.gettempdir(), "chrome_unblocked")
|
|
561
|
+
os.makedirs(tmp, exist_ok=True)
|
|
562
|
+
_copy_profile(cd, chosen, tmp)
|
|
563
|
+
_patch_prefs(tmp, chosen)
|
|
564
|
+
|
|
565
|
+
# Launch
|
|
566
|
+
args = [
|
|
567
|
+
chrome,
|
|
568
|
+
f"--user-data-dir={tmp}",
|
|
569
|
+
f"--profile-directory={chosen}",
|
|
570
|
+
f"--proxy-server=socks5://127.0.0.1:{WARP_SOCKS_PORT}",
|
|
571
|
+
"--host-resolver-rules=MAP * ~NOTFOUND , EXCLUDE 127.0.0.1",
|
|
572
|
+
"--no-first-run",
|
|
573
|
+
"--no-default-browser-check",
|
|
574
|
+
"--disable-session-crashed-bubble",
|
|
575
|
+
]
|
|
576
|
+
|
|
577
|
+
print()
|
|
578
|
+
print(f" {GRN}{B}{'─' * 46}{R}")
|
|
579
|
+
print(f" {GRN}{B} TUNNEL ACTIVE — Chrome launching now{R}")
|
|
580
|
+
print(f" {GRN}{B}{'─' * 46}{R}")
|
|
581
|
+
print(f" {DIM}Profile: {chosen}{R}")
|
|
582
|
+
print(f" {DIM}Proxy: socks5://127.0.0.1:{WARP_SOCKS_PORT}{R}")
|
|
583
|
+
print(f" {DIM}DNS: Encrypted (no leaks){R}")
|
|
584
|
+
print(f" {DIM}All traffic routed through Cloudflare{R}")
|
|
585
|
+
print(f" {GRN}{B}{'─' * 46}{R}")
|
|
586
|
+
print(f"\n {DIM}Press Ctrl+C or close Chrome to quit.{R}\n")
|
|
587
|
+
|
|
588
|
+
start_time = time.time()
|
|
589
|
+
proc = subprocess.Popen(args)
|
|
590
|
+
|
|
591
|
+
# Background monitor — re-checks access every 5 min
|
|
592
|
+
mon = threading.Thread(target=_bg_monitor, args=(proc, cli), daemon=True)
|
|
593
|
+
mon.start()
|
|
594
|
+
|
|
595
|
+
def _exit(sig, frame):
|
|
596
|
+
elapsed = time.time() - start_time
|
|
597
|
+
proc.terminate()
|
|
598
|
+
_disconnect_warp(cli)
|
|
599
|
+
_cleanup(tmp)
|
|
600
|
+
print(f"\n {DIM}Session: {_format_duration(elapsed)}{R}")
|
|
601
|
+
print(f" {CYN}See you next time!{R}\n")
|
|
602
|
+
sys.exit(0)
|
|
603
|
+
|
|
604
|
+
signal.signal(signal.SIGINT, _exit)
|
|
605
|
+
signal.signal(signal.SIGTERM, _exit)
|
|
606
|
+
|
|
607
|
+
proc.wait()
|
|
608
|
+
elapsed = time.time() - start_time
|
|
609
|
+
_disconnect_warp(cli)
|
|
610
|
+
_cleanup(tmp)
|
|
611
|
+
print(f"\n {DIM}Session: {_format_duration(elapsed)}{R}")
|
|
612
|
+
print(f" {CYN}Chrome closed. See you next time!{R}\n")
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
if __name__ == "__main__":
|
|
616
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
wchrome
|