diff --git a/projects/vpn/README.md b/projects/vpn/README.md index 3c1c2fb..1a8369d 100644 --- a/projects/vpn/README.md +++ b/projects/vpn/README.md @@ -9,10 +9,13 @@ und eigenes Browser-Profil — mehrere VPNs laufen parallel ohne Konflikt. - **wuerth_vpn_tray.py** — Würth, Cisco AnyConnect (`--protocol=anyconnect`), User+Passwort. Fertig und in Betrieb. -- **heron_vpn_tray.py** — Heron, GlobalProtect (`--protocol=gp`). Gerüst fertig; - der SAML-POST-/Duo-Login ist auf macOS nicht sauber rootlos lösbar - (openconnect `--external-browser` bedient nur SAML-REDIRECT; gp-saml-gui - braucht WebKit2GTK). Unter Linux via gp-saml-gui umsetzbar. +- **heron_vpn_tray.py** — Heron, GlobalProtect (`--protocol=gp`), Duo-SAML. + Läuft rootlos auf macOS: Der SAML-Login (`heron_gp_saml.py`) erfolgt in einem + nativen WKWebView (pyobjc) mit GlobalProtect-User-Agent, greift den + `prelogin-cookie` aus den HTTP-Antwort-Headern ab (was QtWebEngine nicht kann) + und startet damit openconnect inkl. HIP-Report (`--csd-wrapper`) über ocproxy. + Der SAML-Teil ist ein eigener CLT-Python-Prozess (braucht pyobjc), die Tray-App + ruft ihn und reicht den Cookie an openconnect. ## Voraussetzungen diff --git a/projects/vpn/build_heron_app.sh b/projects/vpn/build_heron_app.sh index 927381d..a320893 100644 --- a/projects/vpn/build_heron_app.sh +++ b/projects/vpn/build_heron_app.sh @@ -14,25 +14,28 @@ cd "$APPS" # --- 0. Icon erzeugen (H-Badge -> icon_heron.icns) --- TMP="$(mktemp -d)" -QT_QPA_PLATFORM=offscreen "$PY" - "$TMP/h.png" <<'PYEOF' +QT_QPA_PLATFORM=offscreen "$PY" - "$APPS/heron_mask.png" "$TMP/h.png" <<'PYEOF' import sys -from PyQt6.QtGui import QImage, QPainter, QColor, QFont, QGuiApplication +from PyQt6.QtGui import QImage, QPainter, QColor, qAlpha, qRgba from PyQt6.QtCore import Qt -_app = QGuiApplication([]) # noetig fuer Font-Zugriff +# Reiher-Maske gruen einfaerben und mittig auf quadratische Flaeche setzen. +mask = QImage(sys.argv[1]).convertToFormat(QImage.Format.Format_ARGB32) +tgt = QColor("#2e7d32") +for y in range(mask.height()): + for x in range(mask.width()): + a = qAlpha(mask.pixel(x, y)) + if a: + mask.setPixel(x, y, qRgba(tgt.red(), tgt.green(), tgt.blue(), a)) S = 1024 -img = QImage(S, S, QImage.Format.Format_ARGB32) -img.fill(Qt.GlobalColor.transparent) -p = QPainter(img) -p.setRenderHint(QPainter.RenderHint.Antialiasing) -p.setBrush(QColor("#2e7d32")) -p.setPen(Qt.PenStyle.NoPen) -m = int(S * 0.09) -p.drawRoundedRect(m, m, S - 2 * m, S - 2 * m, S * 0.18, S * 0.18) -p.setPen(QColor("#ffffff")) -f = QFont(); f.setBold(True); f.setPixelSize(int(S * 0.6)); p.setFont(f) -p.drawText(img.rect(), Qt.AlignmentFlag.AlignCenter, "H") +inner = int(S * 0.80) +scaled = mask.scaled(inner, inner, Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation) +canvas = QImage(S, S, QImage.Format.Format_ARGB32) +canvas.fill(Qt.GlobalColor.transparent) +p = QPainter(canvas) +p.drawImage((S - scaled.width()) // 2, (S - scaled.height()) // 2, scaled) p.end() -img.save(sys.argv[1]) +canvas.save(sys.argv[2]) PYEOF ISET="$TMP/icon.iconset"; mkdir -p "$ISET" for s in 16 32 128 256 512; do diff --git a/projects/vpn/heron_gp_saml.py b/projects/vpn/heron_gp_saml.py new file mode 100644 index 0000000..5663b84 --- /dev/null +++ b/projects/vpn/heron_gp_saml.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +""" +PoC: GlobalProtect SAML-POST (Duo) auf macOS rootlos. + +Holt den GP-Prelogin, fuehrt das Duo-SAML in einem eingebetteten WKWebView durch, +greift den `prelogin-cookie`/`saml-username` aus den HTTP-Antwort-Headern ab +(was QtWebEngine nicht kann, das native WKWebView ueber den Navigation-Response- +Delegate aber schon) und startet damit openconnect rootlos ueber ocproxy. + +Start: /Library/Developer/CommandLineTools/usr/bin/python3 ~/apps/heron_gp_saml.py +""" +from __future__ import annotations + +import base64 +import json +import os +import re +import shutil +import subprocess +import sys +import urllib.request + +import objc +from Cocoa import ( + NSApplication, NSObject, NSWindow, NSURL, NSMakeRect, + NSBackingStoreBuffered, +) +from Foundation import NSHTTPURLResponse, NSHTTPCookie +from WebKit import WKWebView, WKWebViewConfiguration + +SERVER = "ssl.heron.at" +PORT = 11090 +GP_UA = "PAN GlobalProtect" # GP liefert den prelogin-cookie nur dem Client-UA +NSWindowStyleMaskTitled = 1 +NSWindowStyleMaskClosable = 2 +NSWindowStyleMaskResizable = 8 +WKNavigationResponsePolicyAllow = 1 + +captured: dict[str, str] = {} + + +def _tool(n: str) -> str: + return shutil.which(n) or ( + f"/opt/homebrew/bin/{n}" if os.path.exists(f"/opt/homebrew/bin/{n}") else n + ) + + +def _hipreport() -> str | None: + import glob + for p in ["/opt/homebrew/opt/openconnect/libexec/openconnect/hipreport.sh", + *glob.glob("/opt/homebrew/Cellar/openconnect/*/libexec/openconnect/hipreport.sh"), + "/usr/local/opt/openconnect/libexec/openconnect/hipreport.sh"]: + if os.path.exists(p): + return p + return None + + +def prelogin() -> tuple[str, str]: + """Liefert (saml_html, sessid).""" + # Gateway-Prelogin direkt (nicht das Portal) -> liefert den Gateway-Cookie. + url = (f"https://{SERVER}/ssl-vpn/prelogin.esp" + "?tmp=tmp&clientVer=4100&clientos=Mac") + req = urllib.request.Request(url, method="POST", data=b"", + headers={"User-Agent": GP_UA}) + with urllib.request.urlopen(req, timeout=20) as r: + xml = r.read().decode("utf-8", "replace") + sessid = "" + for c in r.headers.get_all("Set-Cookie") or []: + m = re.search(r"SESSID=([^;]+)", c) + if m: + sessid = m.group(1) + m = re.search(r"(.*?)", xml, re.S) + if not m: + raise SystemExit("Kein im Prelogin.") + return base64.b64decode(m.group(1)).decode("utf-8", "replace"), sessid + + +class Delegate(NSObject): + def webView_decidePolicyForNavigationAction_decisionHandler_( + self, webView, navAction, handler): + u = str(navAction.request().URL().absoluteString()) + print("NAV ->", u[:120], file=sys.stderr) + if u.startswith("globalprotectcallback:"): + payload = u[len("globalprotectcallback:"):] + try: + dec = base64.b64decode(payload).decode("utf-8", "replace") + except Exception: + dec = payload + print("CALLBACK-Payload:", dec[:500], file=sys.stderr) + for key in ("prelogin-cookie", "saml-username", + "portal-userauthcookie"): + m = re.search(rf"{key}[=:>]+\s*([^<&\"']+)", dec) + if m: + captured[key] = m.group(1).strip() + handler(0) # Cancel + if "prelogin-cookie" in captured: + NSApplication.sharedApplication().stop_(None) + return + handler(1) # Allow + + def webView_decidePolicyForNavigationResponse_decisionHandler_( + self, webView, navResponse, handler): + resp = navResponse.response() + if resp.isKindOfClass_(NSHTTPURLResponse): + for k, v in resp.allHeaderFields().items(): + lk = str(k).lower() + if lk in ("prelogin-cookie", "saml-username", + "portal-userauthcookie", "saml-auth-status"): + captured[lk] = str(v) + handler(WKNavigationResponsePolicyAllow) + if "prelogin-cookie" in captured and "saml-username" in captured: + NSApplication.sharedApplication().stop_(None) + + def webView_didFinishNavigation_(self, webView, nav): + # Nur die SAML-Weiterleitungsform (id=myform) anstossen, sonst wuerde + # auch das Duo-Login-Formular dauernd neu abgesendet (Flackern). + webView.evaluateJavaScript_completionHandler_( + "var f=document.getElementById('myform'); if(f){f.submit();}", None) + + +def run_saml(html: str, sessid: str): + app = NSApplication.sharedApplication() + app.setActivationPolicy_(0) # Regular -> Fenster bekommt sauber den Fokus + rect = NSMakeRect(0, 0, 540, 720) + style = (NSWindowStyleMaskTitled | NSWindowStyleMaskClosable + | NSWindowStyleMaskResizable) + win = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_( + rect, style, NSBackingStoreBuffered, False) + win.setTitle_("Heron SSO-Login") + + cfg = WKWebViewConfiguration.alloc().init() + wv = WKWebView.alloc().initWithFrame_configuration_(rect, cfg) + wv.setCustomUserAgent_(GP_UA) + delegate = Delegate.alloc().init() + wv.setNavigationDelegate_(delegate) + win.setContentView_(wv) + win.makeKeyAndOrderFront_(None) + + base = NSURL.URLWithString_(f"https://{SERVER}/") + + def load(_=None): + wv.loadHTMLString_baseURL_(html, base) + + if sessid: + store = cfg.websiteDataStore().httpCookieStore() + cookie = NSHTTPCookie.cookieWithProperties_({ + "Name": "SESSID", "Value": sessid, "Domain": SERVER, + "Path": "/", "Secure": "TRUE", + }) + store.setCookie_completionHandler_(cookie, load) + else: + load() + + app.activateIgnoringOtherApps_(True) + app.run() + + +def main(): + auth_only = "--auth-only" in sys.argv + html, sessid = prelogin() + run_saml(html, sessid) + + if "prelogin-cookie" not in captured: + print(f"Kein prelogin-cookie erhalten. Erfasst: {list(captured)}", + file=sys.stderr) + raise SystemExit(2) + + if auth_only: + # Nur den Cookie als JSON auf stdout -> die Tray-App liest ihn. + print(json.dumps({ + "prelogin-cookie": captured["prelogin-cookie"], + "saml-username": captured.get("saml-username", ""), + })) + return + + print("Erfasst:", {k: v[:20] + "…" for k, v in captured.items()}, + file=sys.stderr) + cmd = [ + _tool("openconnect"), "--protocol=gp", + "--user", captured.get("saml-username", ""), + "--usergroup=gateway:prelogin-cookie", "--passwd-on-stdin", + "--script", f"{_tool('ocproxy')} -D {PORT}", "--script-tun", + ] + hip = _hipreport() + if hip: + cmd += ["--csd-wrapper", hip] # HIP-Report, sonst trennt das Gateway + cmd.append(SERVER) + print("Starte:", " ".join(cmd), file=sys.stderr) + p = subprocess.Popen(cmd, stdin=subprocess.PIPE) + p.stdin.write((captured["prelogin-cookie"] + "\n").encode()) + p.stdin.flush() + p.wait() + + +if __name__ == "__main__": + main() diff --git a/projects/vpn/heron_mask.png b/projects/vpn/heron_mask.png new file mode 100644 index 0000000..4e11070 Binary files /dev/null and b/projects/vpn/heron_mask.png differ diff --git a/projects/vpn/heron_vpn_tray.py b/projects/vpn/heron_vpn_tray.py index ef352b8..611fc9c 100644 --- a/projects/vpn/heron_vpn_tray.py +++ b/projects/vpn/heron_vpn_tray.py @@ -8,14 +8,18 @@ VPN schickt und alles andere direkt. Start/Stop ueber das Tray-Icon. Eigene Ports/Profil/Bundle -> laeuft parallel zur Wuerth-App. -Abhaengigkeiten: - openconnect + ocproxy auf dem PATH (macOS: brew install openconnect ocproxy) +Auth: GlobalProtect-SAML (Duo). Der SAML-Login laeuft als eigener Cocoa/pyobjc- +Prozess (heron_gp_saml.py --auth-only, via CLT-Python) und liefert nur den +prelogin-cookie; diese App startet damit openconnect (+HIP-Report) ueber ocproxy. -ZU FUELLEN: VPN_USER, INTERNAL_GLOB, START_URL, BOOKMARKS. +Abhaengigkeiten: + openconnect + ocproxy (macOS: brew install openconnect ocproxy), + CLT-Python mit pyobjc (pyobjc-framework-WebKit) fuer heron_gp_saml.py. """ from __future__ import annotations +import glob as _glob import json import os import platform @@ -26,30 +30,32 @@ import subprocess import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer -from PyQt6.QtCore import QTimer, Qt -from PyQt6.QtGui import QAction, QColor, QIcon, QPainter, QPixmap +from PyQt6.QtCore import QProcess, QProcessEnvironment, QTimer, Qt +from PyQt6.QtGui import ( + QAction, QColor, QIcon, QImage, QPainter, QPixmap, qAlpha, qRgba, +) from PyQt6.QtWidgets import ( - QApplication, QSystemTrayIcon, QMenu, QInputDialog, QLineEdit, QMessageBox, + QApplication, QSystemTrayIcon, QMenu, QMessageBox, ) # ===== CONFIG ================================================================= APP_NAME = "Heron VPN" PROTOCOL = "gp" # GlobalProtect (Palo Alto) SERVER = "ssl.heron.at" -AUTH = "saml" # "saml" = SSO via externem Browser, "password" -VPN_USER = "marcus.hinz@robotunits.com" # nur fuer AUTH="password" noetig -PORT = 11090 # SOCKS5 (ocproxy) – andere Ports als Wuerth +PORT = 11090 # SOCKS5 (ocproxy) – andere Ports als Wuerth PACPORT = 11091 # lokaler PAC-HTTP-Server -INTERNAL_GLOB = "*.heron.at" # <-- interne Hosts anpassen (ueber VPN) +# Hosts, die ueber das VPN laufen (Domain-Globs und/oder IP-Globs). +INTERNAL_GLOBS = ["*.heron.at", "172.16.*"] PROFILE = os.path.expanduser("~/.heron-edge") -START_URL = "https://robotunits.com" # <-- gewuenschte Startseite +START_URL = "https://robotunits.com" LOGFILE = os.path.expanduser("~/.heron-vpn.log") PAC_NAME = "heron.pac" +CLT_PYTHON = "/Library/Developer/CommandLineTools/usr/bin/python3" # hat pyobjc -# Lesezeichen, die ins frische Profil gesaet werden (Name, URL). <-- anpassen +# Lesezeichen, die ins frische Profil gesaet werden (Name, URL). BOOKMARKS: list[tuple[str, str]] = [ - ("Heron", "https://ssl.heron.at"), - + ("Storybook", "http://172.16.80.30:30060/"), + ("Heron", "https://heron.at"), ] # ============================================================================= @@ -123,20 +129,31 @@ def _tool(name: str) -> str: return name -def _external_browser() -> str: - """Programm, das openconnect fuer das SAML-Login mit der SSO-URL aufruft.""" - s = _system() - if s == "Darwin": - return "/usr/bin/open" # oeffnet Standardbrowser mit der URL - if s == "Windows": - return "cmd" # TODO: 'cmd /c start' verifizieren - return shutil.which("xdg-open") or "xdg-open" +def _saml_script() -> str: + """Pfad zu heron_gp_saml.py (im Bundle unter Resources, sonst neben uns).""" + base = os.environ.get( + "RESOURCEPATH", os.path.dirname(os.path.abspath(__file__))) + return os.path.join(base, "heron_gp_saml.py") + + +def _hipreport() -> str | None: + for p in ["/opt/homebrew/opt/openconnect/libexec/openconnect/hipreport.sh", + *_glob.glob("/opt/homebrew/Cellar/openconnect/*/libexec/openconnect/hipreport.sh"), + "/usr/local/opt/openconnect/libexec/openconnect/hipreport.sh"]: + if os.path.exists(p): + return p + return None + + +def _resolver_rules() -> str: + return " , ".join(f"MAP {g} ~NOTFOUND" for g in INTERNAL_GLOBS) # ---------- PAC-Server (in-process) ------------------------------------------ +_PAC_COND = " || ".join(f'shExpMatch(host, "{g}")' for g in INTERNAL_GLOBS) PAC_BODY = ( "function FindProxyForURL(url, host) {\n" - f' if (shExpMatch(host, "{INTERNAL_GLOB}")) {{\n' + f" if ({_PAC_COND}) {{\n" f' return "SOCKS5 127.0.0.1:{PORT}";\n' " }\n" ' return "DIRECT";\n' @@ -163,28 +180,46 @@ def start_pac_server() -> ThreadingHTTPServer: return srv -# ---------- Icon (generiertes "H"-Badge in Zustandsfarbe) -------------------- +# ---------- Icon (Reiher-Silhouette in Zustandsfarbe) ------------------------ _ICON_CACHE: dict[str, QIcon] = {} +_MASK: QImage | None = None -def badge_icon(color: str) -> QIcon: +def _heron_mask() -> QImage: + global _MASK + if _MASK is None: + base = os.environ.get( + "RESOURCEPATH", os.path.dirname(os.path.abspath(__file__))) + img = QImage(os.path.join(base, "heron_mask.png")) + _MASK = (img.scaled(44, 44, Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation) + .convertToFormat(QImage.Format.Format_ARGB32) + if not img.isNull() else QImage()) + return _MASK + + +def heron_icon(color: str) -> QIcon: if color in _ICON_CACHE: return _ICON_CACHE[color] - pm = QPixmap(44, 44) - pm.fill(Qt.GlobalColor.transparent) - p = QPainter(pm) - p.setRenderHint(QPainter.RenderHint.Antialiasing) - p.setBrush(QColor(color)) - p.setPen(Qt.PenStyle.NoPen) - p.drawRoundedRect(4, 4, 36, 36, 9, 9) - p.setPen(QColor("#ffffff")) - f = p.font() - f.setBold(True) - f.setPixelSize(26) - p.setFont(f) - p.drawText(pm.rect(), Qt.AlignmentFlag.AlignCenter, "H") - p.end() - icon = QIcon(pm) + m = _heron_mask() + if m.isNull(): # Fallback: einfacher Punkt + pm = QPixmap(22, 22) + pm.fill(Qt.GlobalColor.transparent) + p = QPainter(pm) + p.setRenderHint(QPainter.RenderHint.Antialiasing) + p.setBrush(QColor(color)) + p.setPen(Qt.PenStyle.NoPen) + p.drawEllipse(4, 4, 14, 14) + p.end() + return QIcon(pm) + tgt = QColor(color) + o = QImage(m) + for y in range(o.height()): + for x in range(o.width()): + a = qAlpha(o.pixel(x, y)) + if a: + o.setPixel(x, y, qRgba(tgt.red(), tgt.green(), tgt.blue(), a)) + icon = QIcon(QPixmap.fromImage(o)) _ICON_CACHE[color] = icon return icon @@ -195,13 +230,13 @@ class VpnTray: self.app = app self.oc: subprocess.Popen | None = None self.browser: subprocess.Popen | None = None - self.vpn_pw: str | None = None + self.auth_proc: QProcess | None = None self.state = "disconnected" self.pac = start_pac_server() self.tray = QSystemTrayIcon() - self.tray.setIcon(badge_icon("#9a9a9a")) + self.tray.setIcon(heron_icon("#9a9a9a")) self.tray.setToolTip(f"{APP_NAME} – getrennt") self.menu = QMenu() @@ -240,7 +275,7 @@ class VpnTray: "connected": ("#2e7d32", "Verbunden"), "error": ("#cc3333", "Fehler"), }[state] - self.tray.setIcon(badge_icon(color)) + self.tray.setIcon(heron_icon(color)) self.tray.setToolTip(f"{APP_NAME} – {label}") self.act_status.setText(label) self.act_connect.setEnabled(state in ("disconnected", "error")) @@ -248,48 +283,79 @@ class VpnTray: self.act_open.setEnabled(state == "connected") def connect(self): - base = [ - _tool("openconnect"), - f"--protocol={PROTOCOL}", - "--script", f"{_tool('ocproxy')} -D {PORT}", - "--script-tun", + if self.auth_proc is not None: + return + self._set_state("connecting") + # SAML-Login als eigener pyobjc/WKWebView-Prozess -> liefert Cookie als JSON. + self.auth_proc = QProcess() + # Bereinigte Umgebung: sonst erbt das CLT-Python die PYTHONHOME/PATH des + # py2app-Bundles und findet sein pyobjc nicht (Subprozess stirbt sofort). + env = QProcessEnvironment.systemEnvironment() + for v in ("PYTHONHOME", "PYTHONPATH", "PYTHONEXECUTABLE", + "PYTHONNOUSERSITE", "PYTHONDONTWRITEBYTECODE"): + env.remove(v) + self.auth_proc.setProcessEnvironment(env) + self.auth_proc.finished.connect(self._on_auth_done) + self.auth_proc.start(CLT_PYTHON, [_saml_script(), "--auth-only"]) + if not self.auth_proc.waitForStarted(3000): + self.auth_proc = None + self._set_state("error") + QMessageBox.critical(None, APP_NAME, + "SAML-Login (CLT-Python/pyobjc) nicht startbar.") + + def _on_auth_done(self, code, status): + out = bytes(self.auth_proc.readAllStandardOutput()).decode("utf-8", "replace") + self.auth_proc = None + auth = None + for line in out.strip().splitlines(): + line = line.strip() + if line.startswith("{"): + try: + auth = json.loads(line) + except Exception: + pass + if not auth or not auth.get("prelogin-cookie"): + self._set_state("error") + return + self._start_openconnect(auth.get("saml-username", ""), + auth["prelogin-cookie"]) + + def _start_openconnect(self, user: str, cookie: str): + # Reste eines frueheren Laufs wegraeumen, sonst Portkonflikt auf SOCKS. + if _system() in ("Darwin", "Linux"): + subprocess.run(["pkill", "-f", f"ocproxy -D {PORT}"], + stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + cmd = [ + _tool("openconnect"), f"--protocol={PROTOCOL}", + "--user", user, "--usergroup=gateway:prelogin-cookie", + "--passwd-on-stdin", + "--script", f"{_tool('ocproxy')} -D {PORT}", "--script-tun", ] - send_pw = None - - if AUTH == "saml": - # SSO: openconnect oeffnet den Standardbrowser fuer das Login. - cmd = base + ["--external-browser", _external_browser(), - f"https://{SERVER}"] - else: - if not self.vpn_pw: - pw, ok = QInputDialog.getText( - None, APP_NAME, f"VPN-Passwort fuer {VPN_USER}:", - QLineEdit.EchoMode.Password, - ) - if not (ok and pw): - return - self.vpn_pw = pw - send_pw = self.vpn_pw - cmd = base + ["--user", VPN_USER, "--passwd-on-stdin", - f"https://{SERVER}"] - + hip = _hipreport() + if hip: + cmd += ["--csd-wrapper", hip] # HIP-Report, sonst trennt das Gateway + cmd.append(SERVER) try: self._log = open(LOGFILE, "wb") self.oc = subprocess.Popen( - cmd, stdin=subprocess.PIPE if send_pw else None, + cmd, stdin=subprocess.PIPE, stdout=self._log, stderr=subprocess.STDOUT, ) - if send_pw: - self.oc.stdin.write((send_pw + "\n").encode()) - self.oc.stdin.flush() + self.oc.stdin.write((cookie + "\n").encode()) + self.oc.stdin.flush() except FileNotFoundError: self._set_state("error") QMessageBox.critical(None, APP_NAME, "openconnect nicht gefunden.") return - - self._set_state("connecting") + # bleibt "connecting"; _tick erkennt den offenen SOCKS-Port def disconnect(self): + if self.auth_proc is not None: + try: + self.auth_proc.kill() + except Exception: + pass + self.auth_proc = None self._kill_browser() if self.oc and self.oc.poll() is None: try: @@ -318,7 +384,7 @@ class VpnTray: argv = browser_argv() + [ f"--user-data-dir={PROFILE}", f"--proxy-pac-url=http://127.0.0.1:{PACPORT}/{PAC_NAME}", - f"--host-resolver-rules=MAP {INTERNAL_GLOB} ~NOTFOUND", + f"--host-resolver-rules={_resolver_rules()}", "--no-first-run", "--no-default-browser-check", "--disable-features=msSmartScreenProtection", "--test-type", diff --git a/projects/vpn/setup_heron.py b/projects/vpn/setup_heron.py index 2c954f3..6b8d82b 100644 --- a/projects/vpn/setup_heron.py +++ b/projects/vpn/setup_heron.py @@ -6,6 +6,7 @@ Bauen: cd ~/apps && bash build_heron_app.sh from setuptools import setup APP = ["heron_vpn_tray.py"] +DATA_FILES = ["heron_gp_saml.py", "heron_mask.png"] # Auth-Prozess + Icon-Maske OPTIONS = { "iconfile": "icon_heron.icns", "packages": ["PyQt6"], @@ -23,6 +24,7 @@ OPTIONS = { setup( app=APP, name="HeronVPN", + data_files=DATA_FILES, options={"py2app": OPTIONS}, setup_requires=["py2app"], )