17
0

updated with the new version of gp-saml-gui (from upstream)

This commit is contained in:
Benjamin Mako Hill 2025-03-09 23:10:06 -07:00
parent 0b6ace2da3
commit 5d9f17d8ee
2 changed files with 75 additions and 32 deletions

View File

@ -1,9 +1,15 @@
#!/usr/bin/env python3
import warnings
try:
import gi
gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.0')
try:
gi.require_version('WebKit2', '4.1')
except ValueError: # I wish this were ImportError
gi.require_version('WebKit2', '4.0')
warnings.warn("Using WebKit2Gtk 4.0 (obsolete); please upgrade to WebKit2Gtk 4.1")
from gi.repository import Gtk, WebKit2, GLib
except ImportError:
try:
@ -11,10 +17,11 @@ except ImportError:
gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.0')
from pgi.repository import Gtk, WebKit2, GLib
warnings.warn("Using PGI and WebKit2Gtk 4.0 (both obsolete); please upgrade to PyGObject and WebKit2Gtk 4.1")
except ImportError:
gi = None
if gi is None:
raise ImportError("Either gi (PyGObject) or pgi module is required.")
raise ImportError("Either gi (PyGObject) or pgi (obsolete) module is required.")
import argparse
import urllib3
@ -24,11 +31,11 @@ import ssl
import tempfile
from operator import setitem
from os import path, dup2, execvp
from os import path, dup2, execvp, environ
from shlex import quote
from sys import stderr, platform
from binascii import a2b_base64, b2a_base64
from urllib.parse import urlparse, urlencode
from urllib.parse import urlparse, urlencode, urlunsplit
from html.parser import HTMLParser
@ -45,30 +52,35 @@ COOKIE_FIELDS = ('prelogin-cookie', 'portal-userauthcookie')
class SAMLLoginView:
def __init__(self, uri, html=None, verbose=False, cookies=None, verify=True, user_agent=None):
def __init__(self, uri, html, args):
Gtk.init(None)
window = Gtk.Window()
self.window = window = Gtk.Window()
# API reference: https://lazka.github.io/pgi-docs/#WebKit2-4.0
self.closed = False
self.success = False
self.saml_result = {}
self.verbose = verbose
self.verbose = args.verbose
self.ctx = WebKit2.WebContext.get_default()
if not verify:
if not args.verify:
self.ctx.set_tls_errors_policy(WebKit2.TLSErrorsPolicy.IGNORE)
self.cookies = self.ctx.get_cookie_manager()
if cookies:
if args.cookies:
self.cookies.set_accept_policy(WebKit2.CookieAcceptPolicy.ALWAYS)
self.cookies.set_persistent_storage(cookies, WebKit2.CookiePersistentStorage.TEXT)
self.cookies.set_persistent_storage(args.cookies, WebKit2.CookiePersistentStorage.TEXT)
self.wview = WebKit2.WebView()
if user_agent is None:
user_agent = 'PAN GlobalProtect'
if args.no_proxy:
data_manager = self.ctx.get_website_data_manager()
data_manager.set_network_proxy_settings(WebKit2.NetworkProxyMode.NO_PROXY, None)
if args.user_agent is None:
args.user_agent = 'PAN GlobalProtect'
settings = self.wview.get_settings()
settings.set_user_agent(user_agent)
settings.set_user_agent(args.user_agent)
self.wview.set_settings(settings)
window.resize(500, 500)
@ -101,7 +113,8 @@ class SAMLLoginView:
h = rs.get_http_headers() if rs else None
if h:
ct, cl = h.get_content_type(), h.get_content_length()
content_type, charset = ct[0], ct.params.get('charset')
content_type = ct[0]
charset = ct.params.get('charset') if ct.params else None
content_details = '%d bytes of %s%s for ' % (cl, content_type, ('; charset='+charset) if charset else '')
print('[RECEIVE] %sresource %s %s' % (content_details if h else '', m, uri), file=stderr)
@ -124,10 +137,17 @@ class SAMLLoginView:
uri = mr.get_uri()
rs = mr.get_response()
h = rs.get_http_headers() if rs else None
ct = h.get_content_type()
ct = h.get_content_type() if h else None
if self.verbose:
print('[PAGE ] Finished loading page %s' % uri, file=stderr)
urip = urlparse(uri)
origin = '%s %s' % ('🔒' if urip.scheme == 'https' else '🔴', urip.netloc)
self.window.set_title("SAML Login (%s)" % origin)
# if no response or no headers (for e.g. about:blank), skip checking this
if not rs or not h:
return
# convert to normal dict
d = {}
@ -212,10 +232,25 @@ class TLSAdapter(requests.adapters.HTTPAdapter):
We have extracted the relevant value from <openssl/ssl.h>.
'''
def __init__(self, verify=True):
self.verify = verify
super().__init__()
def init_poolmanager(self, connections, maxsize, block=False):
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ssl_context.set_ciphers('DEFAULT:@SECLEVEL=1')
ssl_context.options |= 1<<2 # OP_LEGACY_SERVER_CONNECT
if not self.verify:
ssl_context.check_hostname = False
ssl_context.verify_mode = ssl.CERT_NONE
if hasattr(ssl_context, "keylog_filename"):
sslkeylogfile = environ.get("SSLKEYLOGFILE")
if sslkeylogfile:
ssl_context.keylog_filename = sslkeylogfile
self.poolmanager = urllib3.PoolManager(
num_pools=connections,
maxsize=maxsize,
@ -236,6 +271,7 @@ def parse_args(args = None):
x.add_argument('-K', '--no-cookies', dest='cookies', action='store_const', const=None,
help="Don't use or store cookies at all")
x = p.add_mutually_exclusive_group()
p.add_argument('-i', '--ignore-redirects', action='store_true', help='Use specified gateway hostname as server, ignoring redirects')
x.add_argument('-g','--gateway', dest='interface', action='store_const', const='gateway', default='portal',
help='SAML auth to gateway')
x.add_argument('-p','--portal', dest='interface', action='store_const', const='portal',
@ -251,6 +287,7 @@ def parse_args(args = None):
x.add_argument('-x','--external', action='store_true', help='Launch external browser (for debugging)')
x.add_argument('-P','--pkexec-openconnect', action='store_const', dest='exec', const='pkexec', help='Use PolicyKit to exec openconnect')
x.add_argument('-S','--sudo-openconnect', action='store_const', dest='exec', const='sudo', help='Use sudo to exec openconnect')
x.add_argument('-E','--exec-openconnect', action='store_const', dest='exec', const='exec', help='Execute openconnect directly (advanced users)')
g.add_argument('-u','--uri', action='store_true', help='Treat server as the complete URI of the SAML entry point, rather than GlobalProtect server')
g.add_argument('--clientos', choices=set(pf2clientos.values()), default=default_clientos, help="clientos value to send (default is %(default)s)")
p.add_argument('-f','--field', dest='extra', action='append', default=[],
@ -259,6 +296,7 @@ def parse_args(args = None):
help='Allow use of insecure renegotiation or ancient 3DES and RC4 ciphers')
p.add_argument('--user-agent', '--useragent', default='PAN GlobalProtect',
help='Use the provided string as the HTTP User-Agent header (default is %(default)r, as used by OpenConnect)')
p.add_argument('--no-proxy', action='store_true', help='Disable system proxy settings')
p.add_argument('openconnect_extra', nargs='*', help="Extra arguments to include in output OpenConnect command-line")
args = p.parse_args(args)
@ -284,7 +322,7 @@ def main(args = None):
s = requests.Session()
if args.insecure:
s.mount('https://', TLSAdapter())
s.mount('https://', TLSAdapter(verify=args.verify))
s.headers['User-Agent'] = 'PAN GlobalProtect' if args.user_agent is None else args.user_agent
s.cert = args.cert
@ -354,7 +392,7 @@ def main(args = None):
# spawn WebKit view to do SAML interactive login
if args.verbose:
print("Got SAML %s, opening browser..." % sam, file=stderr)
slv = SAMLLoginView(uri, html, verbose=args.verbose, cookies=args.cookies, verify=args.verify, user_agent=args.user_agent)
slv = SAMLLoginView(uri, html, args)
Gtk.main()
if slv.closed:
print("Login window closed by user.", file=stderr)
@ -364,7 +402,10 @@ def main(args = None):
# extract response and convert to OpenConnect command-line
un = slv.saml_result.get('saml-username')
server = slv.saml_result.get('server', args.server)
if args.ignore_redirects:
server = args.server
else:
server = slv.saml_result.get('server', args.server)
for cn, ifh in (('prelogin-cookie','gateway'), ('portal-userauthcookie','portal')):
cv = slv.saml_result.get(cn)
@ -393,6 +434,8 @@ def main(args = None):
if key:
openconnect_args.insert(1, "--sslkey="+key)
openconnect_args.insert(1, "--certificate="+cert)
if args.no_proxy:
openconnect_args.insert(1, "--no-proxy")
openconnect_command = ''' echo {} |\n sudo openconnect {}'''.format(
quote(cv), " ".join(map(quote, openconnect_args)))
@ -400,9 +443,14 @@ def main(args = None):
if args.verbose:
# Warn about ambiguities
if server != args.server and not args.uri:
print('''IMPORTANT: During the SAML auth, you were redirected from {0} to {1}. This probably '''
'''means you should specify {1} as the server for final connection, but we're not 100% '''
'''sure about this. You should probably try both.\n'''.format(args.server, server), file=stderr)
if args.ignore_redirects:
print('''IMPORTANT: During the SAML auth, you were redirected from {0} to {1}. This probably '''
'''means you should specify {1} as the server for final connection, but we're not 100% '''
'''sure about this. You should probably try both; if necessary, use the '''
'''--ignore-redirects option to specify desired behavior.\n'''.format(args.server, server), file=stderr)
else:
print('''IMPORTANT: During the SAML auth, you were redirected from {0} to {1}, however the '''
'''redirection was ignored because you specified --ignore-redirects.\n'''.format(args.server, server), file=stderr)
if ifh != args.interface and not args.uri:
print('''IMPORTANT: We started with SAML auth to the {} interface, but received a cookie '''
'''that's often associated with the {} interface. You should probably try both.\n'''.format(args.interface, ifh),
@ -423,10 +471,11 @@ def main(args = None):
# redirect stdin from this file, before it is closed by the context manager
# (it will remain accessible via the open file descriptor)
dup2(tf.fileno(), 0)
cmd = ["openconnect"] + openconnect_args
if args.exec == 'pkexec':
cmd = ["pkexec", "--user", "root", "openconnect"] + openconnect_args
cmd = ["pkexec", "--user", "root"] + cmd
elif args.exec == 'sudo':
cmd = ["sudo", "openconnect"] + openconnect_args
cmd = ["sudo"] + cmd
execvp(cmd[0], cmd)
else:

View File

@ -1,13 +1,7 @@
#!/usr/bin/python3
from __future__ import print_function
from sys import stderr, version_info, platform
if (version_info >= (3, 0)):
from urllib.parse import urlparse, urlencode
raw_input = input
else:
from urlparse import urlparse
from urllib import urlencode
from sys import stderr, platform
from urllib.parse import urlparse, urlencode
import requests
import argparse
import getpass
@ -68,7 +62,7 @@ if prelogin:
else:
# same request params work for /global-protect/getconfig.esp as for /ssl-vpn/login.esp
if args.user == None:
args.user = raw_input('Username: ')
args.user = input('Username: ')
if args.password == None:
args.password = getpass.getpass('Password: ')
data=dict(user=args.user, passwd=args.password,