stupidpythonbrowser 1.0.4__py3-none-any.whl → 1.1.0__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.
- pybrowser/browser.py +169 -64
- pybrowser/doh_server.py +61 -22
- pybrowser/legacy.py +1024 -0
- pybrowser/settings.py +9 -3
- pybrowser/terminal.py +16 -11
- {stupidpythonbrowser-1.0.4.dist-info → stupidpythonbrowser-1.1.0.dist-info}/METADATA +1 -1
- stupidpythonbrowser-1.1.0.dist-info/RECORD +15 -0
- {stupidpythonbrowser-1.0.4.dist-info → stupidpythonbrowser-1.1.0.dist-info}/entry_points.txt +1 -0
- stupidpythonbrowser-1.0.4.dist-info/RECORD +0 -14
- {stupidpythonbrowser-1.0.4.dist-info → stupidpythonbrowser-1.1.0.dist-info}/WHEEL +0 -0
- {stupidpythonbrowser-1.0.4.dist-info → stupidpythonbrowser-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {stupidpythonbrowser-1.0.4.dist-info → stupidpythonbrowser-1.1.0.dist-info}/top_level.txt +0 -0
pybrowser/browser.py
CHANGED
|
@@ -39,7 +39,7 @@ from pybrowser.settings import (
|
|
|
39
39
|
sanitize_for_json
|
|
40
40
|
)
|
|
41
41
|
from pybrowser.doh_server import (
|
|
42
|
-
DNS_CACHE, DNS_LOCK, STEVENBLACK_DOMAINS,
|
|
42
|
+
DNS_CACHE, DNS_LOCK, STEVENBLACK_DOMAINS, ADBLOCK_LOCK,
|
|
43
43
|
WK_RULE_LIST, is_domain_blocked, search_blocked_domains, load_dns_cache,
|
|
44
44
|
start_proxy_server, create_nw_proxy_config, load_adblock_filters_from_disk,
|
|
45
45
|
compile_webkit_adblock_rules, update_adblock_filters_from_web,
|
|
@@ -210,18 +210,27 @@ try:
|
|
|
210
210
|
SecKeychainFindGenericPassword = sec.SecKeychainFindGenericPassword
|
|
211
211
|
SecKeychainFindGenericPassword.argtypes = [c_void_p, c_uint32, c_char_p, c_uint32, c_char_p, POINTER(c_uint32), POINTER(c_void_p), POINTER(c_void_p)]
|
|
212
212
|
SecKeychainItemFreeContent = sec.SecKeychainItemFreeContent
|
|
213
|
+
SecKeychainItemFreeContent.argtypes = [c_void_p, c_void_p]
|
|
213
214
|
SecKeychainItemDelete = sec.SecKeychainItemDelete
|
|
214
215
|
SecKeychainItemDelete.argtypes = [c_void_p]
|
|
216
|
+
|
|
217
|
+
cf = ctypes.cdll.LoadLibrary("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation")
|
|
218
|
+
CFRelease = cf.CFRelease
|
|
219
|
+
CFRelease.argtypes = [c_void_p]
|
|
215
220
|
except Exception:
|
|
216
|
-
|
|
221
|
+
CFRelease = None
|
|
217
222
|
|
|
218
223
|
def save_to_keychain(host, username, password):
|
|
219
224
|
try:
|
|
220
225
|
srv, acc, pwd = host.encode('utf-8'), username.encode('utf-8'), password.encode('utf-8')
|
|
221
226
|
length, data_ptr, item_ref = c_uint32(), c_void_p(), c_void_p()
|
|
222
227
|
if SecKeychainFindGenericPassword(None, len(srv), srv, len(acc), acc, byref(length), byref(data_ptr), byref(item_ref)) == 0:
|
|
223
|
-
if data_ptr.value:
|
|
224
|
-
|
|
228
|
+
if data_ptr.value:
|
|
229
|
+
SecKeychainItemFreeContent(None, data_ptr.value)
|
|
230
|
+
if item_ref.value:
|
|
231
|
+
SecKeychainItemDelete(item_ref.value)
|
|
232
|
+
elif item_ref.value and CFRelease:
|
|
233
|
+
CFRelease(item_ref.value)
|
|
225
234
|
SecKeychainAddGenericPassword(None, len(srv), srv, len(acc), acc, len(pwd), pwd, None)
|
|
226
235
|
except Exception:
|
|
227
236
|
pass
|
|
@@ -232,12 +241,14 @@ def get_from_keychain(host, username):
|
|
|
232
241
|
length, data_ptr, item_ref = c_uint32(), c_void_p(), c_void_p()
|
|
233
242
|
if SecKeychainFindGenericPassword(None, len(srv), srv, len(acc), acc, byref(length), byref(data_ptr), byref(item_ref)) == 0:
|
|
234
243
|
pwd = string_at(data_ptr.value, length.value).decode('utf-8')
|
|
235
|
-
|
|
244
|
+
if data_ptr.value:
|
|
245
|
+
SecKeychainItemFreeContent(None, data_ptr.value)
|
|
246
|
+
if item_ref.value and CFRelease:
|
|
247
|
+
CFRelease(item_ref.value)
|
|
236
248
|
return pwd
|
|
237
249
|
except Exception:
|
|
238
250
|
pass
|
|
239
251
|
return None
|
|
240
|
-
|
|
241
252
|
# ============================================================================
|
|
242
253
|
# USERSCRIPTS & EXTENSIONS (CRX)
|
|
243
254
|
# ============================================================================
|
|
@@ -361,7 +372,7 @@ def init_history_db():
|
|
|
361
372
|
print(f"[History DB Error] {e}")
|
|
362
373
|
|
|
363
374
|
def record_history(url, title):
|
|
364
|
-
if not url or url.startswith("browser://"):
|
|
375
|
+
if not url or url.startswith("browser://") or url.startswith("about:"):
|
|
365
376
|
return
|
|
366
377
|
try:
|
|
367
378
|
with sqlite3.connect(HISTORY_FILE) as conn:
|
|
@@ -867,6 +878,7 @@ class ChromeTabView(Cocoa.NSView):
|
|
|
867
878
|
if target_idx != self.index:
|
|
868
879
|
from_idx = self.index
|
|
869
880
|
self.controller.move_tab(from_idx, target_idx)
|
|
881
|
+
self.index = target_idx
|
|
870
882
|
self.drag_start = cur_pt
|
|
871
883
|
break
|
|
872
884
|
|
|
@@ -1016,22 +1028,26 @@ class DownloadDelegate(Cocoa.NSObject):
|
|
|
1016
1028
|
handler(Foundation.NSURL.fileURLWithPath_(dest_path))
|
|
1017
1029
|
|
|
1018
1030
|
def downloadDidFinish_(self, download):
|
|
1019
|
-
|
|
1031
|
+
ctrl = self.controller
|
|
1032
|
+
if ctrl and hasattr(download, "progress") and download.progress():
|
|
1020
1033
|
try:
|
|
1021
|
-
download.progress().removeObserver_forKeyPath_(
|
|
1034
|
+
download.progress().removeObserver_forKeyPath_(ctrl, "fractionCompleted")
|
|
1022
1035
|
except Exception:
|
|
1023
1036
|
pass
|
|
1024
1037
|
path = self.dest_paths.pop(download, None)
|
|
1025
|
-
|
|
1038
|
+
if ctrl:
|
|
1039
|
+
ctrl.finish_download(download, "Completed", dl_path=path)
|
|
1026
1040
|
|
|
1027
1041
|
def download_didFailWithError_resumeData_(self, download, error, resumeData):
|
|
1028
|
-
|
|
1042
|
+
ctrl = self.controller
|
|
1043
|
+
if ctrl and hasattr(download, "progress") and download.progress():
|
|
1029
1044
|
try:
|
|
1030
|
-
download.progress().removeObserver_forKeyPath_(
|
|
1045
|
+
download.progress().removeObserver_forKeyPath_(ctrl, "fractionCompleted")
|
|
1031
1046
|
except Exception:
|
|
1032
1047
|
pass
|
|
1033
1048
|
path = self.dest_paths.pop(download, None)
|
|
1034
|
-
|
|
1049
|
+
if ctrl:
|
|
1050
|
+
ctrl.finish_download(download, "Failed", dl_path=path)
|
|
1035
1051
|
|
|
1036
1052
|
# ============================================================================
|
|
1037
1053
|
# SCHEME HANDLER
|
|
@@ -1151,11 +1167,11 @@ term.onData(d => {{
|
|
|
1151
1167
|
window.webkit.messageHandlers.terminalInput.postMessage(d);
|
|
1152
1168
|
}}
|
|
1153
1169
|
}});
|
|
1154
|
-
|
|
1170
|
+
const termDecoder = new TextDecoder();
|
|
1155
1171
|
window.writeToTerm = b => {{
|
|
1156
1172
|
const s = atob(b), u = new Uint8Array(s.length);
|
|
1157
1173
|
for (let i = 0; i < s.length; i++) u[i] = s.charCodeAt(i);
|
|
1158
|
-
term.write(
|
|
1174
|
+
term.write(termDecoder.decode(u, {{ stream: true }}));
|
|
1159
1175
|
}};
|
|
1160
1176
|
</script></body></html>"""
|
|
1161
1177
|
|
|
@@ -1171,34 +1187,55 @@ window.writeToTerm = b => {{
|
|
|
1171
1187
|
for b in bms:
|
|
1172
1188
|
if isinstance(b, dict) and b.get("url"):
|
|
1173
1189
|
u = b["url"]
|
|
1174
|
-
# Bug 16 Fix: Guard title against None
|
|
1175
1190
|
t = b.get("title") or u or ""
|
|
1176
1191
|
safe_u = json.dumps(u)
|
|
1177
1192
|
esc_u = html.escape(u)
|
|
1178
1193
|
esc_t = html.escape(t)
|
|
1179
1194
|
host = urllib.parse.urlparse(u).netloc
|
|
1180
1195
|
fav_tag = f"<img src='https://www.google.com/s2/favicons?domain={urllib.parse.quote(host)}&sz=32' width='16' height='16' style='vertical-align:middle;margin-right:8px;border-radius:3px;' onerror=\"this.style.display='none'\">" if host else ""
|
|
1181
|
-
|
|
1182
|
-
items.append(f"<li style='display:flex;align-items:center;padding:6px 0;'><span style='flex:1;display:flex;align-items:center;'>{fav_tag}<a href='{esc_u}' style='color:#1a73e8;text-decoration:none;'>{esc_t}</a></span> <button onclick='{del_call}' style='margin-left:12px;padding:3px 8px;'>Delete</button></li>")
|
|
1196
|
+
items.append(f"<li style='display:flex;align-items:center;padding:6px 0;'><span style='flex:1;display:flex;align-items:center;'>{fav_tag}<a href='{esc_u}' style='color:#1a73e8;text-decoration:none;'>{esc_t}</a></span> <button onclick='deleteBm({safe_u})' style='margin-left:12px;padding:3px 8px;'>Delete</button></li>")
|
|
1183
1197
|
rows = "".join(items)
|
|
1184
|
-
return f"<!DOCTYPE html><html><
|
|
1198
|
+
return f"""<!DOCTYPE html><html><head><meta charset='utf-8'><script>
|
|
1199
|
+
function deleteBm(url) {{
|
|
1200
|
+
if (window.webkit?.messageHandlers?.bridge) {{
|
|
1201
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_bookmark', url: url}});
|
|
1202
|
+
}}
|
|
1203
|
+
setTimeout(() => location.reload(), 100);
|
|
1204
|
+
}}
|
|
1205
|
+
function addBm() {{
|
|
1206
|
+
const t = document.getElementById('t').value;
|
|
1207
|
+
const u = document.getElementById('u').value;
|
|
1208
|
+
if (u && window.webkit?.messageHandlers?.bridge) {{
|
|
1209
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'add_bookmark', title: t, url: u}});
|
|
1210
|
+
}}
|
|
1211
|
+
setTimeout(() => location.reload(), 100);
|
|
1212
|
+
}}
|
|
1213
|
+
</script></head><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Bookmarks</h2><div style='margin-bottom:15px;'><input id='t' placeholder='Title' style='padding:4px 8px;margin-right:8px;'><input id='u' placeholder='https://...' style='padding:4px 8px;margin-right:8px;width:260px;'><button onclick='addBm()' style='padding:4px 12px;'>Add</button></div><ul style='list-style:none;padding:0;'>{rows or '<li>No bookmarks</li>'}</ul></body></html>"""
|
|
1185
1214
|
|
|
1186
1215
|
def get_history_html(self):
|
|
1187
1216
|
records = query_history()
|
|
1188
1217
|
items = []
|
|
1189
1218
|
for r in records:
|
|
1190
|
-
del_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: {r[0]}}}); this.closest('tr').remove();"
|
|
1191
|
-
# Bug 16 Fix: Guard title against None and avoid backslashes inside f-string expressions
|
|
1192
1219
|
title_text = r[2] or r[1] or ""
|
|
1193
1220
|
url_str = r[1] or ""
|
|
1194
1221
|
esc_url = html.escape(url_str)
|
|
1195
1222
|
esc_title = html.escape(title_text)
|
|
1196
|
-
items.append(f"<tr><td>{r[3]}</td><td><a href='{esc_url}'>{esc_title}</a></td><td><button onclick='{
|
|
1223
|
+
items.append(f"<tr><td>{r[3]}</td><td><a href='{esc_url}'>{esc_title}</a></td><td><button onclick='deleteHist({r[0]}, this)'>✕</button></td></tr>")
|
|
1197
1224
|
rows = "".join(items)
|
|
1198
|
-
return f"<!DOCTYPE html><html><
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1225
|
+
return f"""<!DOCTYPE html><html><head><meta charset='utf-8'><script>
|
|
1226
|
+
function deleteHist(id, btn) {{
|
|
1227
|
+
if (window.webkit?.messageHandlers?.bridge) {{
|
|
1228
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_history_item', id: id}});
|
|
1229
|
+
}}
|
|
1230
|
+
btn.closest('tr')?.remove();
|
|
1231
|
+
}}
|
|
1232
|
+
function clearAllHist() {{
|
|
1233
|
+
if (window.webkit?.messageHandlers?.bridge) {{
|
|
1234
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'clear_browsing_data', targets: ['history']}});
|
|
1235
|
+
}}
|
|
1236
|
+
setTimeout(() => location.reload(), 100);
|
|
1237
|
+
}}
|
|
1238
|
+
</script></head><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>History</h2><button onclick='clearAllHist()'>Clear All</button><table style='width:100%;margin-top:15px;'>{rows or '<tr><td>No history</td></tr>'}</table></body></html>"""
|
|
1202
1239
|
|
|
1203
1240
|
def get_extensions_html(self):
|
|
1204
1241
|
scripts = get_installed_userscripts()
|
|
@@ -1207,17 +1244,35 @@ window.writeToTerm = b => {{
|
|
|
1207
1244
|
f = s["file"]
|
|
1208
1245
|
safe_f = json.dumps(f)
|
|
1209
1246
|
checked = "checked" if s["enabled"] else ""
|
|
1210
|
-
toggle_call = f"if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: {safe_f}, enabled: this.checked}})"
|
|
1211
|
-
del_call = f"if(confirm('Delete?')){{if(window.webkit?.messageHandlers?.bridge) window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: {safe_f}}}); setTimeout(() => location.reload(), 100);}}"
|
|
1212
1247
|
items.append(
|
|
1213
1248
|
f"<div style='border:1px solid #ccc;padding:10px;margin-bottom:8px;'>"
|
|
1214
1249
|
f"<b>{html.escape(s['name'])}</b> ({html.escape(f)}) "
|
|
1215
|
-
f"<label><input type='checkbox' {checked} onchange='{
|
|
1216
|
-
f"<button onclick
|
|
1250
|
+
f"<label><input type='checkbox' {checked} onchange='toggleScript({safe_f}, this.checked)'> Enabled</label> "
|
|
1251
|
+
f"<button onclick='deleteScript({safe_f})'>Delete</button>"
|
|
1217
1252
|
f"</div>"
|
|
1218
1253
|
)
|
|
1219
1254
|
rows = "".join(items)
|
|
1220
|
-
return f"<!DOCTYPE html><html><
|
|
1255
|
+
return f"""<!DOCTYPE html><html><head><meta charset='utf-8'><script>
|
|
1256
|
+
function toggleScript(file, enabled) {{
|
|
1257
|
+
if (window.webkit?.messageHandlers?.bridge) {{
|
|
1258
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'toggle_script', file: file, enabled: enabled}});
|
|
1259
|
+
}}
|
|
1260
|
+
}}
|
|
1261
|
+
function deleteScript(file) {{
|
|
1262
|
+
if (confirm('Delete this userscript?')) {{
|
|
1263
|
+
if (window.webkit?.messageHandlers?.bridge) {{
|
|
1264
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'delete_script', file: file}});
|
|
1265
|
+
}}
|
|
1266
|
+
setTimeout(() => location.reload(), 100);
|
|
1267
|
+
}}
|
|
1268
|
+
}}
|
|
1269
|
+
function installCrxPrompt() {{
|
|
1270
|
+
const id = prompt('CRX Extension ID or URL:');
|
|
1271
|
+
if (id && window.webkit?.messageHandlers?.bridge) {{
|
|
1272
|
+
window.webkit.messageHandlers.bridge.postMessage({{cmd: 'install_crx', id: id}});
|
|
1273
|
+
}}
|
|
1274
|
+
}}
|
|
1275
|
+
</script></head><body style='font-family:-apple-system,sans-serif;padding:30px;'><h2>Extensions & Userscripts</h2><button onclick='installCrxPrompt()'>Install CRX</button><div style='margin-top:15px;'>{rows or 'No userscripts installed.'}</div></body></html>"""
|
|
1221
1276
|
|
|
1222
1277
|
# ============================================================================
|
|
1223
1278
|
# WEBKIT SUBCLASS & IMAGE CONTEXT MENU
|
|
@@ -1421,9 +1476,11 @@ class OmniboxPanelCoordinator:
|
|
|
1421
1476
|
for h in query_history(query_str)[:4]:
|
|
1422
1477
|
results.append({"type": "history", "text": h[2] or h[1] or "", "url": h[1]})
|
|
1423
1478
|
|
|
1479
|
+
# Show local suggestions immediately without waiting for remote network I/O
|
|
1480
|
+
self.show(results)
|
|
1481
|
+
|
|
1424
1482
|
def fetch_remote():
|
|
1425
1483
|
try:
|
|
1426
|
-
# Secured endpoint via HTTPS to avoid keystroke sniffing
|
|
1427
1484
|
url = f"https://suggestqueries.google.com/complete/search?client=chrome&q={urllib.parse.quote(query_str)}"
|
|
1428
1485
|
req = urllib.request.Request(url, headers={"User-Agent": UA_PRESETS["chrome_mac"]})
|
|
1429
1486
|
with urllib.request.urlopen(req, timeout=1.0) as resp:
|
|
@@ -1432,8 +1489,7 @@ class OmniboxPanelCoordinator:
|
|
|
1432
1489
|
if req_id == self.current_req_id:
|
|
1433
1490
|
dispatch_to_main(lambda: self.show(results + remote))
|
|
1434
1491
|
except Exception:
|
|
1435
|
-
|
|
1436
|
-
dispatch_to_main(lambda: self.show(results))
|
|
1492
|
+
pass
|
|
1437
1493
|
|
|
1438
1494
|
threading.Thread(target=fetch_remote, daemon=True).start()
|
|
1439
1495
|
|
|
@@ -1477,8 +1533,9 @@ class OmniboxPanelCoordinator:
|
|
|
1477
1533
|
|
|
1478
1534
|
def hide(self):
|
|
1479
1535
|
if self.suggestion_window and self.suggestion_window.isVisible():
|
|
1480
|
-
|
|
1481
|
-
|
|
1536
|
+
parent = self.suggestion_window.parentWindow()
|
|
1537
|
+
if parent:
|
|
1538
|
+
parent.removeChildWindow_(self.suggestion_window)
|
|
1482
1539
|
self.suggestion_window.orderOut_(None)
|
|
1483
1540
|
|
|
1484
1541
|
def select_suggestion(self, sugg):
|
|
@@ -1876,9 +1933,16 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
1876
1933
|
def save_session(self):
|
|
1877
1934
|
if self.is_incognito:
|
|
1878
1935
|
return
|
|
1936
|
+
valid_tabs = [
|
|
1937
|
+
{"url": t.get("url"), "group_id": t.get("group_id"), "pinned": t.get("pinned", False)}
|
|
1938
|
+
for t in self.tabs
|
|
1939
|
+
if t.get("url") and not t.get("url").startswith("browser://") and not t.get("url").startswith("about:")
|
|
1940
|
+
]
|
|
1941
|
+
if not valid_tabs and os.path.exists(SESSION_FILE):
|
|
1942
|
+
return
|
|
1879
1943
|
data = {
|
|
1880
|
-
"active_index": self.active_tab_index,
|
|
1881
|
-
"tabs":
|
|
1944
|
+
"active_index": max(0, min(self.active_tab_index, len(valid_tabs) - 1)) if valid_tabs else 0,
|
|
1945
|
+
"tabs": valid_tabs,
|
|
1882
1946
|
"groups": [asdict(g) for g in self.groups]
|
|
1883
1947
|
}
|
|
1884
1948
|
atomic_write_json(SESSION_FILE, data)
|
|
@@ -2169,7 +2233,22 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2169
2233
|
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(IN_WINDOW_FULLSCREEN_JS, WebKit.WKUserScriptInjectionTimeAtDocumentEnd, False))
|
|
2170
2234
|
|
|
2171
2235
|
if SETTINGS.settings.get("cosmetic_adblock_enabled", True):
|
|
2172
|
-
cosmetic_js = "(function() {
|
|
2236
|
+
cosmetic_js = """(function() {
|
|
2237
|
+
function inject() {
|
|
2238
|
+
const target = document.head || document.documentElement;
|
|
2239
|
+
if (target && !document.getElementById('__pybrowser_cosmetic_adblock')) {
|
|
2240
|
+
const st = document.createElement('style');
|
|
2241
|
+
st.id = '__pybrowser_cosmetic_adblock';
|
|
2242
|
+
st.textContent = `""" + COSMETIC_ADBLOCK_CSS + """`;
|
|
2243
|
+
target.appendChild(st);
|
|
2244
|
+
return true;
|
|
2245
|
+
}
|
|
2246
|
+
return false;
|
|
2247
|
+
}
|
|
2248
|
+
if (!inject()) {
|
|
2249
|
+
document.addEventListener('DOMContentLoaded', inject);
|
|
2250
|
+
}
|
|
2251
|
+
})();"""
|
|
2173
2252
|
uc.addUserScript_(WebKit.WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly_(cosmetic_js, WebKit.WKUserScriptInjectionTimeAtDocumentStart, False))
|
|
2174
2253
|
|
|
2175
2254
|
if SETTINGS.settings.get("yt_adblock_enabled", True):
|
|
@@ -2899,25 +2978,31 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
2899
2978
|
if cid:
|
|
2900
2979
|
threading.Thread(target=lambda: install_crx(cid), daemon=True).start()
|
|
2901
2980
|
elif cmd == "prompt_save_password":
|
|
2902
|
-
|
|
2981
|
+
calling_wv = message.webView()
|
|
2982
|
+
verified_host = calling_wv.URL().host() if (calling_wv and calling_wv.URL()) else None
|
|
2983
|
+
if verified_host:
|
|
2984
|
+
self.show_password_banner(verified_host, body.get("username", ""), body.get("password", ""))
|
|
2903
2985
|
elif cmd == "check_autofill":
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2986
|
+
calling_wv = message.webView()
|
|
2987
|
+
if not calling_wv or not calling_wv.URL():
|
|
2988
|
+
return
|
|
2989
|
+
verified_host = calling_wv.URL().host()
|
|
2990
|
+
if not verified_host:
|
|
2991
|
+
return
|
|
2992
|
+
saved_user = SETTINGS.settings.get("saved_logins", {}).get(verified_host)
|
|
2907
2993
|
if saved_user:
|
|
2908
|
-
pwd = get_from_keychain(
|
|
2994
|
+
pwd = get_from_keychain(verified_host, saved_user)
|
|
2909
2995
|
if pwd:
|
|
2910
|
-
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
calling_wv.evaluateJavaScript_completionHandler_(js, None)
|
|
2996
|
+
pwd_json = json.dumps(pwd)
|
|
2997
|
+
user_json = json.dumps(saved_user)
|
|
2998
|
+
js = f"""(function() {{
|
|
2999
|
+
let pw = document.querySelector('input[type=password]');
|
|
3000
|
+
let user = document.querySelector('input[type=text], input[type=email]');
|
|
3001
|
+
if (pw) pw.value = {pwd_json};
|
|
3002
|
+
if (user) user.value = {user_json};
|
|
3003
|
+
}})();"""
|
|
3004
|
+
calling_wv.evaluateJavaScript_completionHandler_(js, None)
|
|
3005
|
+
|
|
2921
3006
|
elif cmd == "save_setting":
|
|
2922
3007
|
raw_key, raw_val = body.get("key"), body.get("val")
|
|
2923
3008
|
if raw_key is None:
|
|
@@ -3110,9 +3195,11 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3110
3195
|
elif pending_action == "copy_address": self.perform_copy_image_address(img_url)
|
|
3111
3196
|
return None
|
|
3112
3197
|
|
|
3113
|
-
url = act.request().URL().absoluteString() if act.request().URL() else
|
|
3114
|
-
|
|
3115
|
-
|
|
3198
|
+
url = act.request().URL().absoluteString() if act.request().URL() else ""
|
|
3199
|
+
new_tab_idx = len(self.tabs)
|
|
3200
|
+
self.add_tab(url or "about:blank")
|
|
3201
|
+
if 0 <= new_tab_idx < len(self.tabs):
|
|
3202
|
+
return self.tabs[new_tab_idx].get("webview")
|
|
3116
3203
|
return None
|
|
3117
3204
|
|
|
3118
3205
|
def webView_decidePolicyForNavigationAction_decisionHandler_(self, wv, action, handler):
|
|
@@ -3247,13 +3334,23 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3247
3334
|
self.omnibox_panel.hide()
|
|
3248
3335
|
|
|
3249
3336
|
def windowWillClose_(self, notif):
|
|
3250
|
-
# Bug 4 Fix: Invalidate repeating NSTimer to prevent memory retain cycle
|
|
3251
3337
|
if hasattr(self, 'sleep_timer') and self.sleep_timer:
|
|
3252
3338
|
self.sleep_timer.invalidate()
|
|
3253
3339
|
self.sleep_timer = None
|
|
3254
3340
|
|
|
3255
|
-
if hasattr(self, 'omnibox_panel') and self.omnibox_panel
|
|
3256
|
-
self.omnibox_panel.
|
|
3341
|
+
if hasattr(self, 'omnibox_panel') and self.omnibox_panel:
|
|
3342
|
+
if self.omnibox_panel.is_visible():
|
|
3343
|
+
self.omnibox_panel.hide()
|
|
3344
|
+
self.omnibox_panel.wc = None
|
|
3345
|
+
|
|
3346
|
+
# Unregister KVO observers from active downloads to prevent deallocation crash
|
|
3347
|
+
for d in self.downloads:
|
|
3348
|
+
dl_obj = d.get("obj")
|
|
3349
|
+
if dl_obj and hasattr(dl_obj, "progress") and dl_obj.progress():
|
|
3350
|
+
try:
|
|
3351
|
+
dl_obj.progress().removeObserver_forKeyPath_(self, "fractionCompleted")
|
|
3352
|
+
except Exception:
|
|
3353
|
+
pass
|
|
3257
3354
|
|
|
3258
3355
|
self.save_session()
|
|
3259
3356
|
for t in self.tabs:
|
|
@@ -3265,13 +3362,21 @@ class BrowserWindowController(Cocoa.NSWindowController):
|
|
|
3265
3362
|
wv.removeObserver_forKeyPath_(self, k)
|
|
3266
3363
|
except Exception:
|
|
3267
3364
|
pass
|
|
3268
|
-
# Bug 20 Fix: Clean up userContentController message handlers
|
|
3269
3365
|
uc = wv.configuration().userContentController()
|
|
3270
3366
|
for name in SCRIPT_HANDLER_NAMES:
|
|
3271
3367
|
try:
|
|
3272
3368
|
uc.removeScriptMessageHandlerForName_(name)
|
|
3273
3369
|
except Exception:
|
|
3274
3370
|
pass
|
|
3371
|
+
|
|
3372
|
+
# Break circular references
|
|
3373
|
+
if hasattr(self, 'download_delegate'):
|
|
3374
|
+
self.download_delegate.controller = None
|
|
3375
|
+
if hasattr(self, 'find_bar'):
|
|
3376
|
+
self.find_bar.controller = None
|
|
3377
|
+
if hasattr(self, 'bookmarks_bar'):
|
|
3378
|
+
self.bookmarks_bar.controller = None
|
|
3379
|
+
|
|
3275
3380
|
app_delegate = Cocoa.NSApp.delegate()
|
|
3276
3381
|
if app_delegate and hasattr(app_delegate, "controllers") and self in app_delegate.controllers:
|
|
3277
3382
|
app_delegate.controllers.remove(self)
|
|
@@ -3299,7 +3404,7 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3299
3404
|
Cocoa.NSApp.activateIgnoringOtherApps_(True)
|
|
3300
3405
|
|
|
3301
3406
|
def click_monitor(event):
|
|
3302
|
-
for c in self.controllers:
|
|
3407
|
+
for c in list(self.controllers):
|
|
3303
3408
|
if c.omnibox_panel.is_visible():
|
|
3304
3409
|
if c.omnibox_panel.suggestion_window and event.window() == c.omnibox_panel.suggestion_window:
|
|
3305
3410
|
return event
|
|
@@ -3326,7 +3431,7 @@ class AppDelegate(Cocoa.NSObject):
|
|
|
3326
3431
|
if win and isinstance(win.delegate(), BrowserWindowController):
|
|
3327
3432
|
wc = win.delegate()
|
|
3328
3433
|
elif hasattr(self, 'controllers') and self.controllers:
|
|
3329
|
-
for c in self.controllers:
|
|
3434
|
+
for c in list(self.controllers):
|
|
3330
3435
|
if c.window() == win:
|
|
3331
3436
|
wc = c
|
|
3332
3437
|
break
|
pybrowser/doh_server.py
CHANGED
|
@@ -4,6 +4,8 @@ Provides encrypted local DNS proxying and WebKit content rule compilation.
|
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
6
|
import os
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
7
9
|
import json
|
|
8
10
|
import time
|
|
9
11
|
import threading
|
|
@@ -104,12 +106,23 @@ def load_dns_cache():
|
|
|
104
106
|
|
|
105
107
|
def flush_dns_cache_to_disk():
|
|
106
108
|
global DNS_CACHE_DIRTY
|
|
109
|
+
snapshot = None
|
|
107
110
|
with DNS_LOCK:
|
|
108
111
|
if DNS_CACHE_DIRTY:
|
|
112
|
+
snapshot = dict(DNS_CACHE)
|
|
113
|
+
DNS_CACHE_DIRTY = False
|
|
114
|
+
|
|
115
|
+
if snapshot is not None:
|
|
116
|
+
try:
|
|
117
|
+
dir_name = os.path.dirname(DNS_CACHE_FILE) or "."
|
|
118
|
+
fd, temp_path = tempfile.mkstemp(dir=dir_name)
|
|
119
|
+
with os.fdopen(fd, 'w', encoding='utf-8') as f:
|
|
120
|
+
json.dump(snapshot, f, indent=2)
|
|
121
|
+
os.replace(temp_path, DNS_CACHE_FILE)
|
|
122
|
+
except Exception:
|
|
109
123
|
try:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
DNS_CACHE_DIRTY = False
|
|
124
|
+
if 'temp_path' in locals() and os.path.exists(temp_path):
|
|
125
|
+
os.remove(temp_path)
|
|
113
126
|
except Exception:
|
|
114
127
|
pass
|
|
115
128
|
|
|
@@ -130,7 +143,10 @@ def doh_resolve(hostname):
|
|
|
130
143
|
now = time.time()
|
|
131
144
|
clean_host = hostname.strip("[]").strip()
|
|
132
145
|
|
|
133
|
-
|
|
146
|
+
# Localhost and IP literals bypass external DoH
|
|
147
|
+
if clean_host in ("localhost", "127.0.0.1", "::1") or clean_host.endswith(".local"):
|
|
148
|
+
return "127.0.0.1" if clean_host in ("localhost", "127.0.0.1") else clean_host
|
|
149
|
+
|
|
134
150
|
try:
|
|
135
151
|
ipaddress.ip_address(clean_host)
|
|
136
152
|
return clean_host
|
|
@@ -207,30 +223,49 @@ def doh_resolve(hostname):
|
|
|
207
223
|
return clean_host
|
|
208
224
|
|
|
209
225
|
class DoHProxyHandler(http.server.BaseHTTPRequestHandler):
|
|
226
|
+
rbufsize = 0
|
|
227
|
+
wbufsize = 0
|
|
228
|
+
|
|
210
229
|
def log_message(self, format, *args):
|
|
211
230
|
pass
|
|
212
231
|
|
|
213
232
|
def do_CONNECT(self):
|
|
233
|
+
self.close_connection = True
|
|
214
234
|
try:
|
|
215
235
|
host, port = (self.path.rsplit(":", 1) if ":" in self.path else (self.path, 443))
|
|
216
236
|
port = int(port)
|
|
217
237
|
host = host.strip("[]")
|
|
218
238
|
except Exception:
|
|
219
|
-
|
|
239
|
+
try:
|
|
240
|
+
self.send_error(400, "Bad Request")
|
|
241
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
242
|
+
pass
|
|
220
243
|
return
|
|
221
244
|
|
|
222
245
|
if SETTINGS.settings.get("adblock_enabled", True) and is_domain_blocked(host):
|
|
223
|
-
|
|
246
|
+
try:
|
|
247
|
+
self.send_error(403, "Blocked by PyBrowser uBlock Engine")
|
|
248
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
249
|
+
pass
|
|
224
250
|
return
|
|
225
251
|
|
|
226
252
|
ip = doh_resolve(host)
|
|
253
|
+
upstream = None
|
|
227
254
|
try:
|
|
228
255
|
upstream = socket.create_connection((ip, port), timeout=6)
|
|
229
256
|
self.send_response(200, "Connection Established")
|
|
230
257
|
self.end_headers()
|
|
231
258
|
self._tunnel(self.connection, upstream)
|
|
232
259
|
except Exception:
|
|
233
|
-
|
|
260
|
+
if upstream:
|
|
261
|
+
try:
|
|
262
|
+
upstream.close()
|
|
263
|
+
except Exception:
|
|
264
|
+
pass
|
|
265
|
+
try:
|
|
266
|
+
self.send_error(502, "Bad Gateway")
|
|
267
|
+
except (BrokenPipeError, ConnectionResetError, OSError):
|
|
268
|
+
pass
|
|
234
269
|
|
|
235
270
|
def do_GET(self): self._proxy_plain_http()
|
|
236
271
|
def do_POST(self): self._proxy_plain_http()
|
|
@@ -299,25 +334,29 @@ class DoHProxyHandler(http.server.BaseHTTPRequestHandler):
|
|
|
299
334
|
except Exception:
|
|
300
335
|
pass
|
|
301
336
|
|
|
337
|
+
class ReusableTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
338
|
+
allow_reuse_address = True
|
|
339
|
+
daemon_threads = True
|
|
340
|
+
|
|
341
|
+
def server_bind(self):
|
|
342
|
+
if hasattr(socket, "SO_REUSEPORT"):
|
|
343
|
+
try:
|
|
344
|
+
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
345
|
+
except Exception:
|
|
346
|
+
pass
|
|
347
|
+
super().server_bind()
|
|
348
|
+
|
|
349
|
+
def handle_error(self, request, client_address):
|
|
350
|
+
exc_type = sys.exc_info()[0]
|
|
351
|
+
if exc_type in (BrokenPipeError, ConnectionResetError, ConnectionAbortedError):
|
|
352
|
+
return
|
|
353
|
+
super().handle_error(request, client_address)
|
|
354
|
+
|
|
302
355
|
def start_proxy_server():
|
|
303
|
-
# Bug 23 Fix: Prevent multiple TCP server instances & duplicate worker threads
|
|
304
356
|
global _PROXY_STARTED
|
|
305
357
|
with _PROXY_START_LOCK:
|
|
306
358
|
if _PROXY_STARTED:
|
|
307
359
|
return
|
|
308
|
-
|
|
309
|
-
class ReusableTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
|
|
310
|
-
allow_reuse_address = True
|
|
311
|
-
daemon_threads = True
|
|
312
|
-
|
|
313
|
-
def server_bind(self):
|
|
314
|
-
if hasattr(socket, "SO_REUSEPORT"):
|
|
315
|
-
try:
|
|
316
|
-
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
|
|
317
|
-
except Exception:
|
|
318
|
-
pass
|
|
319
|
-
super().server_bind()
|
|
320
|
-
|
|
321
360
|
try:
|
|
322
361
|
server = ReusableTCPServer(("127.0.0.1", LOCAL_PROXY_PORT), DoHProxyHandler)
|
|
323
362
|
threading.Thread(target=server.serve_forever, daemon=True).start()
|
|
@@ -445,4 +484,4 @@ def update_adblock_filters_from_web():
|
|
|
445
484
|
return len(STEVENBLACK_DOMAINS)
|
|
446
485
|
except Exception as e:
|
|
447
486
|
print(f"[Adblock Web Update Error] {e}")
|
|
448
|
-
return len(STEVENBLACK_DOMAINS)
|
|
487
|
+
return len(STEVENBLACK_DOMAINS)
|