#!/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:
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
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
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)
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)
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 = {}
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,
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',
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=[],
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)
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
# 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)
# 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)
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)))
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),
# 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: