stupidpythonbrowser 4.7.2__py3-none-any.whl

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.
@@ -0,0 +1,423 @@
1
+ """
2
+ PyBrowser DNS-over-HTTPS (DoH) Proxy Engine & StevenBlack Adblock Service.
3
+ Provides encrypted local DNS proxying and WebKit content rule compilation.
4
+ """
5
+
6
+ import os
7
+ import json
8
+ import time
9
+ import threading
10
+ import urllib.parse
11
+ import urllib.request
12
+ import socket
13
+ import select
14
+ import socketserver
15
+ import http.server
16
+ import ssl
17
+ import ctypes
18
+ import re
19
+ import ipaddress
20
+
21
+ import Cocoa
22
+ import WebKit
23
+ import objc
24
+
25
+ from pybrowser.settings import (
26
+ SETTINGS, ADBLOCK_CACHE_FILE, DNS_CACHE_FILE, FILTER_LIST_URL,
27
+ LOCAL_PROXY_PORT, BASE_ADBLOCK_DOMAINS
28
+ )
29
+ from pybrowser.terminal import dispatch_to_main
30
+
31
+ STEVENBLACK_DOMAINS = set()
32
+ ACTIVE_BLOCK_DOMAINS = set()
33
+ ADBLOCK_LOCK = threading.Lock()
34
+ WK_RULE_LIST = None
35
+ DNS_CACHE = {}
36
+ DNS_LOCK = threading.Lock()
37
+ DNS_CACHE_DIRTY = False
38
+
39
+ def rebuild_active_domains():
40
+ global ACTIVE_BLOCK_DOMAINS
41
+ with ADBLOCK_LOCK:
42
+ s = set()
43
+ if SETTINGS.settings.get("adblock_base_enabled", True):
44
+ s.update(BASE_ADBLOCK_DOMAINS)
45
+ if SETTINGS.settings.get("adblock_stevenblack_enabled", True):
46
+ s.update(STEVENBLACK_DOMAINS)
47
+ s.update(SETTINGS.get_custom_adblock_domains())
48
+ ACTIVE_BLOCK_DOMAINS = s
49
+
50
+ def is_domain_blocked(host):
51
+ if not host:
52
+ return False
53
+ host = host.strip().lower()
54
+ if host in ACTIVE_BLOCK_DOMAINS:
55
+ return True
56
+ parts = host.split(".")
57
+ for i in range(1, len(parts) - 1):
58
+ if ".".join(parts[i:]) in ACTIVE_BLOCK_DOMAINS:
59
+ return True
60
+ return False
61
+
62
+ def search_blocked_domains(query, limit=100):
63
+ query = (query or "").strip().lower()
64
+ results = []
65
+ custom = SETTINGS.get_custom_adblock_domains()
66
+ with ADBLOCK_LOCK:
67
+ sb = list(STEVENBLACK_DOMAINS)
68
+ base = list(BASE_ADBLOCK_DOMAINS)
69
+
70
+ for d in sorted(custom):
71
+ if not query or query in d:
72
+ results.append({"domain": d, "source": "Custom List"})
73
+ if len(results) >= limit:
74
+ return results
75
+
76
+ for d in sorted(base):
77
+ if not query or query in d:
78
+ results.append({"domain": d, "source": "Built-in High Impact"})
79
+ if len(results) >= limit:
80
+ return results
81
+
82
+ for d in sb:
83
+ if not query or query in d:
84
+ results.append({"domain": d, "source": "StevenBlack Unified"})
85
+ if len(results) >= limit:
86
+ return results
87
+
88
+ return results
89
+
90
+ def load_dns_cache():
91
+ global DNS_CACHE
92
+ if os.path.exists(DNS_CACHE_FILE):
93
+ try:
94
+ with open(DNS_CACHE_FILE, 'r', encoding='utf-8') as f:
95
+ DNS_CACHE = json.load(f)
96
+ except Exception:
97
+ DNS_CACHE = {}
98
+
99
+ def flush_dns_cache_to_disk():
100
+ global DNS_CACHE_DIRTY
101
+ with DNS_LOCK:
102
+ if DNS_CACHE_DIRTY:
103
+ try:
104
+ with open(DNS_CACHE_FILE, 'w', encoding='utf-8') as f:
105
+ json.dump(DNS_CACHE, f, indent=2)
106
+ DNS_CACHE_DIRTY = False
107
+ except Exception:
108
+ pass
109
+
110
+ def doh_resolve(hostname):
111
+ global DNS_CACHE_DIRTY
112
+ now = time.time()
113
+ clean_host = hostname.strip("[]").strip()
114
+
115
+ # If already an IPv4 or IPv6 address literal, bypass DNS lookup
116
+ try:
117
+ ipaddress.ip_address(clean_host)
118
+ return clean_host
119
+ except ValueError:
120
+ pass
121
+
122
+ with DNS_LOCK:
123
+ if clean_host in DNS_CACHE:
124
+ record = DNS_CACHE[clean_host]
125
+ if isinstance(record, dict) and record.get("expires", 0) > now:
126
+ return record["ip"]
127
+
128
+ provider = SETTINGS.settings.get("doh_provider", "cloudflare")
129
+ doh_configs = {
130
+ "cloudflare": ("1.1.1.1", "cloudflare-dns.com", "/dns-query"),
131
+ "cloudflare_security": ("1.1.1.2", "security.cloudflare-dns.com", "/dns-query"),
132
+ "google": ("8.8.8.8", "dns.google", "/resolve"),
133
+ "quad9": ("9.9.9.9", "dns.quad9.net", "/dns-query"),
134
+ "adguard": ("94.140.14.14", "dns.adguard-dns.com", "/dns-query"),
135
+ }
136
+
137
+ if provider == "custom":
138
+ custom_url = SETTINGS.settings.get("doh_custom_url", "")
139
+ if custom_url.startswith("https://"):
140
+ try:
141
+ parsed = urllib.parse.urlparse(custom_url)
142
+ host_hdr = parsed.netloc
143
+ path_base = parsed.path or "/dns-query"
144
+ ip_target = host_hdr.split(":")[0]
145
+ except Exception:
146
+ ip_target, host_hdr, path_base = doh_configs["cloudflare"]
147
+ else:
148
+ ip_target, host_hdr, path_base = doh_configs["cloudflare"]
149
+ else:
150
+ ip_target, host_hdr, path_base = doh_configs.get(provider, doh_configs["cloudflare"])
151
+
152
+ for _ in range(2):
153
+ try:
154
+ ctx = ssl.create_default_context()
155
+ sock = socket.create_connection((ip_target, 443), timeout=3.5)
156
+ ssock = ctx.wrap_socket(sock, server_hostname=host_hdr)
157
+ sep = "&" if "?" in path_base else "?"
158
+ query_path = f"{path_base}{sep}name={urllib.parse.quote(clean_host)}&type=A"
159
+ req = (
160
+ f"GET {query_path} HTTP/1.1\r\n"
161
+ f"Host: {host_hdr}\r\n"
162
+ f"Accept: application/dns-json\r\n"
163
+ f"User-Agent: PyBrowser-DoH/4.7\r\n"
164
+ f"Connection: close\r\n\r\n"
165
+ )
166
+ ssock.sendall(req.encode('utf-8'))
167
+ raw_resp = b""
168
+ while True:
169
+ chunk = ssock.recv(4096)
170
+ if not chunk:
171
+ break
172
+ raw_resp += chunk
173
+ ssock.close()
174
+
175
+ start = raw_resp.find(b'{')
176
+ end = raw_resp.rfind(b'}')
177
+ if start != -1 and end != -1 and end > start:
178
+ data = json.loads(raw_resp[start:end+1].decode("utf-8", errors="ignore"))
179
+ for answer in data.get("Answer", []):
180
+ if answer.get("type") == 1:
181
+ ip = answer.get("data")
182
+ with DNS_LOCK:
183
+ DNS_CACHE[clean_host] = {"ip": ip, "expires": now + 3600}
184
+ DNS_CACHE_DIRTY = True
185
+ return ip
186
+ except Exception:
187
+ time.sleep(0.05)
188
+
189
+ return clean_host
190
+
191
+ class DoHProxyHandler(http.server.BaseHTTPRequestHandler):
192
+ def log_message(self, format, *args):
193
+ pass
194
+
195
+ def do_CONNECT(self):
196
+ try:
197
+ host, port = (self.path.rsplit(":", 1) if ":" in self.path else (self.path, 443))
198
+ port = int(port)
199
+ host = host.strip("[]")
200
+ except Exception:
201
+ self.send_error(400, "Bad Request")
202
+ return
203
+
204
+ if SETTINGS.settings.get("adblock_enabled", True) and is_domain_blocked(host):
205
+ self.send_error(403, "Blocked by PyBrowser uBlock Engine")
206
+ return
207
+
208
+ ip = doh_resolve(host)
209
+ try:
210
+ upstream = socket.create_connection((ip, port), timeout=6)
211
+ self.send_response(200, "Connection Established")
212
+ self.end_headers()
213
+ self._tunnel(self.connection, upstream)
214
+ except Exception:
215
+ self.send_error(502, "Bad Gateway")
216
+
217
+ def do_GET(self): self._proxy_plain_http()
218
+ def do_POST(self): self._proxy_plain_http()
219
+ def do_HEAD(self): self._proxy_plain_http()
220
+ def do_PUT(self): self._proxy_plain_http()
221
+ def do_DELETE(self): self._proxy_plain_http()
222
+ def do_PATCH(self): self._proxy_plain_http()
223
+ def do_OPTIONS(self): self._proxy_plain_http()
224
+
225
+ def _proxy_plain_http(self):
226
+ try:
227
+ parsed = urllib.parse.urlparse(self.path)
228
+ host = parsed.hostname or self.headers.get("Host", "").split(":")[0]
229
+ port = parsed.port or 80
230
+ if SETTINGS.settings.get("adblock_enabled", True) and is_domain_blocked(host):
231
+ self.send_error(403, "Blocked by PyBrowser")
232
+ return
233
+
234
+ ip = doh_resolve(host)
235
+ upstream = socket.create_connection((ip, port), timeout=6)
236
+ req_path = parsed.path or "/"
237
+ if parsed.query:
238
+ req_path += f"?{parsed.query}"
239
+
240
+ raw_req = f"{self.command} {req_path} HTTP/1.1\r\n"
241
+ for k, v in self.headers.items():
242
+ if k.lower() not in ("proxy-connection", "connection"):
243
+ raw_req += f"{k}: {v}\r\n"
244
+ raw_req += "Connection: close\r\n\r\n"
245
+ upstream.sendall(raw_req.encode("utf-8"))
246
+
247
+ content_len = int(self.headers.get("Content-Length", 0))
248
+ if content_len > 0:
249
+ body = self.rfile.read(content_len)
250
+ upstream.sendall(body)
251
+
252
+ self._tunnel(self.connection, upstream)
253
+ except Exception:
254
+ try:
255
+ self.send_error(502, "Bad Gateway")
256
+ except Exception:
257
+ pass
258
+
259
+ def _tunnel(self, client, upstream):
260
+ sockets = [client, upstream]
261
+ try:
262
+ while True:
263
+ r, _, _ = select.select(sockets, [], [], 120)
264
+ if not r:
265
+ break
266
+ for sock in r:
267
+ other = upstream if sock is client else client
268
+ data = sock.recv(8192)
269
+ if not data:
270
+ return
271
+ other.sendall(data)
272
+ except Exception:
273
+ pass
274
+ finally:
275
+ try:
276
+ client.close()
277
+ except Exception:
278
+ pass
279
+ try:
280
+ upstream.close()
281
+ except Exception:
282
+ pass
283
+
284
+ def start_proxy_server():
285
+ class ReusableTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
286
+ allow_reuse_address = True
287
+ daemon_threads = True
288
+
289
+ def server_bind(self):
290
+ if hasattr(socket, "SO_REUSEPORT"):
291
+ try:
292
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
293
+ except Exception:
294
+ pass
295
+ super().server_bind()
296
+
297
+ try:
298
+ server = ReusableTCPServer(("127.0.0.1", LOCAL_PROXY_PORT), DoHProxyHandler)
299
+ threading.Thread(target=server.serve_forever, daemon=True).start()
300
+ threading.Thread(target=dns_flush_worker, daemon=True).start()
301
+ except Exception as e:
302
+ print(f"[Proxy Warning] Could not start local proxy server: {e}")
303
+
304
+ def dns_flush_worker():
305
+ while True:
306
+ time.sleep(30)
307
+ flush_dns_cache_to_disk()
308
+
309
+ def create_nw_proxy_config(host="127.0.0.1", port=LOCAL_PROXY_PORT):
310
+ """Safely instantiates an NWProxyConfig instance via Network.framework."""
311
+ try:
312
+ try:
313
+ import Network
314
+ except ImportError:
315
+ pass
316
+
317
+ libnet = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/Network.framework/Network")
318
+ libnet.nw_endpoint_create_host.restype = ctypes.c_void_p
319
+ libnet.nw_endpoint_create_host.argtypes = [ctypes.c_char_p, ctypes.c_char_p]
320
+ libnet.nw_proxy_config_create_http_connect.restype = ctypes.c_void_p
321
+ libnet.nw_proxy_config_create_http_connect.argtypes = [ctypes.c_void_p, ctypes.c_void_p]
322
+
323
+ ep = libnet.nw_endpoint_create_host(host.encode("utf-8"), str(port).encode("utf-8"))
324
+ if not ep:
325
+ return None
326
+ cfg = libnet.nw_proxy_config_create_http_connect(ep, None)
327
+ if not cfg:
328
+ return None
329
+ return objc.objc_object(c_void_p=cfg)
330
+ except Exception as e:
331
+ print(f"[DoH Proxy Config Error] Failed to bind proxy to WebKit: {e}")
332
+ return None
333
+
334
+ def load_adblock_filters_from_disk():
335
+ global STEVENBLACK_DOMAINS
336
+ domains = set()
337
+ if os.path.exists(ADBLOCK_CACHE_FILE):
338
+ try:
339
+ with open(ADBLOCK_CACHE_FILE, "r", encoding="utf-8", errors="ignore") as f:
340
+ for line in f:
341
+ line = line.strip()
342
+ if line.startswith("0.0.0.0 ") or line.startswith("127.0.0.1 "):
343
+ parts = line.split()
344
+ if len(parts) >= 2:
345
+ d = parts[1].strip().lower()
346
+ if d and d not in ("localhost", "0.0.0.0", "broadcasthost", "local"):
347
+ domains.add(d)
348
+ except Exception as e:
349
+ print(f"[Adblock Disk Error] {e}")
350
+ with ADBLOCK_LOCK:
351
+ STEVENBLACK_DOMAINS = domains
352
+ rebuild_active_domains()
353
+
354
+ def compile_webkit_adblock_rules():
355
+ """Compiles active blocklist domains into WebKit WKContentRuleList with instant cache lookup."""
356
+ def on_cached_lookup(rule_list, error):
357
+ global WK_RULE_LIST
358
+ if rule_list:
359
+ WK_RULE_LIST = rule_list
360
+ dispatch_to_main(apply_rules_to_all_tabs)
361
+ # Proceed with background compilation if not cached or updating
362
+ threading.Thread(target=compile_task, daemon=True).start()
363
+
364
+ def compile_task():
365
+ rebuild_active_domains()
366
+ with ADBLOCK_LOCK:
367
+ domains = list(ACTIVE_BLOCK_DOMAINS)[:48000]
368
+
369
+ rules = []
370
+ for d in domains:
371
+ if not d:
372
+ continue
373
+ rules.append({
374
+ "trigger": {"url-filter": f"^https?://([^/]+\\.)?{re.escape(d)}"},
375
+ "action": {"type": "block"}
376
+ })
377
+
378
+ json_str = json.dumps(rules) if rules else "[]"
379
+
380
+ def handler(rule_list, error):
381
+ global WK_RULE_LIST
382
+ if rule_list:
383
+ WK_RULE_LIST = rule_list
384
+ dispatch_to_main(apply_rules_to_all_tabs)
385
+ elif error:
386
+ print(f"[WebKit Rule Compile Error] {error}")
387
+
388
+ dispatch_to_main(lambda: WebKit.WKContentRuleListStore.defaultStore().compileContentRuleListForIdentifier_encodedContentRuleList_completionHandler_(
389
+ "PyBrowserAdblock", json_str, handler
390
+ ))
391
+
392
+ dispatch_to_main(lambda: WebKit.WKContentRuleListStore.defaultStore().lookUpContentRuleListForIdentifier_completionHandler_(
393
+ "PyBrowserAdblock", on_cached_lookup
394
+ ))
395
+
396
+ def apply_rules_to_all_tabs():
397
+ app_delegate = Cocoa.NSApp.delegate()
398
+ if not app_delegate or not hasattr(app_delegate, 'controllers'):
399
+ return
400
+ for wc in getattr(app_delegate, "controllers", []):
401
+ for tab in getattr(wc, "tabs", []):
402
+ wv = tab.get("webview")
403
+ if wv:
404
+ uc = wv.configuration().userContentController()
405
+ uc.removeAllContentRuleLists()
406
+ if WK_RULE_LIST and SETTINGS.settings.get("adblock_enabled", True):
407
+ uc.addContentRuleList_(WK_RULE_LIST)
408
+
409
+ def update_adblock_filters_from_web():
410
+ try:
411
+ req = urllib.request.Request(FILTER_LIST_URL, headers={"User-Agent": "PyBrowser/4.7 Adblock"})
412
+ with urllib.request.urlopen(req, timeout=15) as resp:
413
+ content = resp.read().decode("utf-8", errors="ignore")
414
+ with open(ADBLOCK_CACHE_FILE, "w", encoding="utf-8") as f:
415
+ f.write(content)
416
+ load_adblock_filters_from_disk()
417
+ SETTINGS.settings["adblock_last_updated"] = time.strftime("%Y-%m-%d %H:%M")
418
+ SETTINGS.save()
419
+ compile_webkit_adblock_rules()
420
+ return len(STEVENBLACK_DOMAINS)
421
+ except Exception as e:
422
+ print(f"[Adblock Web Update Error] {e}")
423
+ return len(STEVENBLACK_DOMAINS)
pybrowser/incognito.py ADDED
@@ -0,0 +1,36 @@
1
+ """
2
+ PyBrowser Incognito & Private Browsing Module.
3
+ Manages non-persistent WebKit data stores and incognito session isolation.
4
+ """
5
+
6
+ import WebKit
7
+ from pybrowser.settings import LOCAL_PROXY_PORT, SETTINGS
8
+ from pybrowser.doh_server import create_nw_proxy_config
9
+
10
+ def create_incognito_data_store():
11
+ """Returns an isolated, non-persistent WKWebsiteDataStore with DoH proxy bound."""
12
+ ds = WebKit.WKWebsiteDataStore.nonPersistentDataStore()
13
+ pcfg = create_nw_proxy_config("127.0.0.1", LOCAL_PROXY_PORT)
14
+ if pcfg and hasattr(ds, "setProxyConfigurations_"):
15
+ try:
16
+ ds.setProxyConfigurations_([pcfg])
17
+ except Exception:
18
+ pass
19
+ return ds
20
+
21
+ def apply_incognito_preferences(config, shared_data_store=None):
22
+ """Configures a WKWebViewConfiguration for zero-persistence incognito browsing."""
23
+ data_store = shared_data_store or create_incognito_data_store()
24
+ config.setWebsiteDataStore_(data_store)
25
+ try:
26
+ config.preferences().setValue_forKey_(True, "developerExtrasEnabled")
27
+ config.preferences().setValue_forKey_(True, "fullScreenEnabled")
28
+ if hasattr(config.preferences(), "setJavaScriptCanOpenWindowsAutomatically_"):
29
+ config.preferences().setJavaScriptCanOpenWindowsAutomatically_(not SETTINGS.settings.get("block_popups", True))
30
+ if hasattr(config.preferences(), "setJavaScriptEnabled_"):
31
+ config.preferences().setJavaScriptEnabled_(SETTINGS.settings.get("js_enabled", True))
32
+ if hasattr(config.preferences(), "setElementFullscreenEnabled_"):
33
+ config.preferences().setElementFullscreenEnabled_(True)
34
+ except Exception:
35
+ pass
36
+ return config
pybrowser/main.py ADDED
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ PyBrowser: Native macOS Chromium/Material 3 Web Browser (PyObjC & WebKit).
4
+ Self-healing Application Launcher and Entry Point.
5
+ """
6
+
7
+ import sys
8
+ import os
9
+ import types
10
+ import signal
11
+ import traceback
12
+
13
+ # ============================================================================
14
+ # SELF-HEALING SYS.PATH RESOLUTION
15
+ # ============================================================================
16
+ current_dir = os.path.abspath(os.path.dirname(__file__))
17
+ parent_dir = os.path.abspath(os.path.join(current_dir, ".."))
18
+
19
+ # Case 1: Standard layout (main.py is in PyBrowser/, and pybrowser/ is a subfolder)
20
+ if os.path.isdir(os.path.join(current_dir, "pybrowser")):
21
+ if current_dir not in sys.path:
22
+ sys.path.insert(0, current_dir)
23
+
24
+ # Case 2: main.py is sitting inside the pybrowser/ folder itself
25
+ elif os.path.basename(current_dir).lower() == "pybrowser" or os.path.isfile(os.path.join(parent_dir, "pybrowser", "browser.py")):
26
+ if parent_dir not in sys.path:
27
+ sys.path.insert(0, parent_dir)
28
+
29
+ # Case 3: Flat layout (all files are in the same folder with no pybrowser/ subfolder)
30
+ elif os.path.isfile(os.path.join(current_dir, "browser.py")):
31
+ if "pybrowser" not in sys.modules:
32
+ pkg = types.ModuleType("pybrowser")
33
+ pkg.__path__ = [current_dir]
34
+ sys.modules["pybrowser"] = pkg
35
+ if current_dir not in sys.path:
36
+ sys.path.insert(0, current_dir)
37
+
38
+ import Cocoa
39
+ from pybrowser.browser import AppDelegate
40
+
41
+ def sigint_handler(sig, frame):
42
+ """Handle Unix SIGINT (Ctrl+C) to terminate Cocoa cleanly."""
43
+ if Cocoa.NSApp:
44
+ Cocoa.NSApp.terminate_(None)
45
+ else:
46
+ sys.exit(0)
47
+
48
+ def main():
49
+ """Initializes the NSApplication run loop and AppDelegate."""
50
+ signal.signal(signal.SIGINT, sigint_handler)
51
+ try:
52
+ app = Cocoa.NSApplication.sharedApplication()
53
+ app.setActivationPolicy_(Cocoa.NSApplicationActivationPolicyRegular)
54
+
55
+ delegate = AppDelegate.alloc().init()
56
+ app.setDelegate_(delegate)
57
+ app.activateIgnoringOtherApps_(True)
58
+
59
+ # Periodic timer allowing Python run loop to process Unix signals
60
+ Cocoa.NSTimer.scheduledTimerWithTimeInterval_repeats_block_(0.5, True, lambda t: None)
61
+ app.run()
62
+ except Exception:
63
+ traceback.print_exc()
64
+
65
+ if __name__ == "__main__":
66
+ main()