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 ADDED
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: wchrome
3
+ Version: 4.0.0
4
+ Summary: Unblocked Chrome — routes browser traffic through Cloudflare WARP tunnel
5
+ Author: hlehman27
6
+ License: MIT
7
+ Requires-Python: >=3.7
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: wchrome
3
+ Version: 4.0.0
4
+ Summary: Unblocked Chrome — routes browser traffic through Cloudflare WARP tunnel
5
+ Author: hlehman27
6
+ License: MIT
7
+ Requires-Python: >=3.7
@@ -0,0 +1,7 @@
1
+ pyproject.toml
2
+ src/wchrome/__init__.py
3
+ src/wchrome.egg-info/PKG-INFO
4
+ src/wchrome.egg-info/SOURCES.txt
5
+ src/wchrome.egg-info/dependency_links.txt
6
+ src/wchrome.egg-info/entry_points.txt
7
+ src/wchrome.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wchrome = wchrome:main
@@ -0,0 +1 @@
1
+ wchrome