From 052a6ff02953bd48c4e2e51d97d14cd2ad8dc89a Mon Sep 17 00:00:00 2001 From: Ezri Brimhall Date: Tue, 24 Jun 2025 09:16:25 -0600 Subject: [PATCH] Added support for opening in a defined Firefox container --- src/vpn_manager_globalprotect/agent.py | 16 ++++++++---- src/vpn_manager_globalprotect/backend.py | 31 ++++++++++++++++++++---- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/src/vpn_manager_globalprotect/agent.py b/src/vpn_manager_globalprotect/agent.py index 108f75e..2051d6e 100644 --- a/src/vpn_manager_globalprotect/agent.py +++ b/src/vpn_manager_globalprotect/agent.py @@ -13,6 +13,7 @@ from .auth_flows import AuthFlows from .listener_targets import ListenerTargets from .agent_tools import parse_prelogin import webbrowser +import urllib.parse from asyncio import Future, wait_for, TimeoutError, get_running_loop @@ -20,7 +21,7 @@ class GlobalProtectSamlAuthenticator: """Encapsulates SAML authentication flow.""" def __init__(self): - self._future: Future = None + self._future: Future | None = None async def saml_authenticate_in_browser( self, auth_flow: str, options: Variant @@ -31,16 +32,21 @@ class GlobalProtectSamlAuthenticator: f"This auth handler is for {AuthFlows.GLOBALPROTECT_SAML_DEFAULT_BROWSER} and does not support {auth_flow}" ) - # Options should contain a single string that is the URL to visit. - if options.signature != "s": + # Options should contain a single string that is the URL to visit, or a struct of two strings. + if options.signature not in {"s", "(ss)"}: raise InvalidArguments( - f"Option signature for SAML auth must be string (s), got {options.signature}" + f"Option signature for SAML auth must be string ('s') or struct of 2 strings ('(ss)'), got {options.signature}" ) if self._future is not None: raise AuthenticationInProgress() - webbrowser.open(options.value) + if options.signature == "(ss)": + target = f"ext+container:name={urllib.parse.quote_plus(options.value[0])}&url={urllib.parse.quote_plus(options.value[1])}" + else: + target = options.value + + webbrowser.open(target) self._future = get_running_loop().create_future() try: return await wait_for(self._future, timeout=300) diff --git a/src/vpn_manager_globalprotect/backend.py b/src/vpn_manager_globalprotect/backend.py index d6ad2ee..218f941 100644 --- a/src/vpn_manager_globalprotect/backend.py +++ b/src/vpn_manager_globalprotect/backend.py @@ -14,7 +14,6 @@ from asyncio.subprocess import Process, PIPE, DEVNULL from dbus_fast import Variant from sys import platform import requests -from vpn_manager.common import errors from vpn_manager.utils import unwrap_variant from .insecure_tls import TLSAdapter import logging @@ -43,6 +42,11 @@ class Options(TypedDict, total=False): spoof_clientos: str use_default_browser: bool + # If set, will attempt to open the in-browser request using + # a Firefox "Open external links in a container" plugin URI, + # using the container name set here. + firefox_browser_container: str + class Auth(TypedDict): """Auth type definition for GlobalProtect VPNs.""" @@ -169,10 +173,27 @@ class GlobalProtectConnection( else AuthFlows.GLOBALPROTECT_SAML_INTEGRATED ) - auth_result = await self._manager.request_credentials( - auth_flow, - Variant("s", base64.b64decode(preauth.get("saml-request")).decode("utf-8")), - ) + if ( + auth_flow == AuthFlows.GLOBALPROTECT_SAML_DEFAULT_BROWSER + and "firefox_browser_container" in options + ): + auth_result = await self._manager.request_credentials( + auth_flow, + Variant( + "(ss)", + ( + base64.b64decode(preauth.get("saml-request")).decode("utf-8"), + options["firefox_browser_container"], + ), + ), + ) + else: + auth_result = await self._manager.request_credentials( + auth_flow, + Variant( + "s", base64.b64decode(preauth.get("saml-request")).decode("utf-8") + ), + ) if auth_result.signature != "a{ss}": # Make sure the agent is well-behaved and is returning a valid response for this request.