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"],
)