updated with the new version of gp-saml-gui (from upstream)
This commit is contained in:
parent
0b6ace2da3
commit
5d9f17d8ee
@ -1,9 +1,15 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import warnings
|
||||||
try:
|
try:
|
||||||
import gi
|
import gi
|
||||||
|
|
||||||
gi.require_version('Gtk', '3.0')
|
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
|
from gi.repository import Gtk, WebKit2, GLib
|
||||||
except ImportError:
|
except ImportError:
|
||||||
try:
|
try:
|
||||||
@ -11,10 +17,11 @@ except ImportError:
|
|||||||
gi.require_version('Gtk', '3.0')
|
gi.require_version('Gtk', '3.0')
|
||||||
gi.require_version('WebKit2', '4.0')
|
gi.require_version('WebKit2', '4.0')
|
||||||
from pgi.repository import Gtk, WebKit2, GLib
|
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:
|
except ImportError:
|
||||||
gi = None
|
gi = None
|
||||||
if gi is 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 argparse
|
||||||
import urllib3
|
import urllib3
|
||||||
@ -24,11 +31,11 @@ import ssl
|
|||||||
import tempfile
|
import tempfile
|
||||||
|
|
||||||
from operator import setitem
|
from operator import setitem
|
||||||
from os import path, dup2, execvp
|
from os import path, dup2, execvp, environ
|
||||||
from shlex import quote
|
from shlex import quote
|
||||||
from sys import stderr, platform
|
from sys import stderr, platform
|
||||||
from binascii import a2b_base64, b2a_base64
|
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
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
|
||||||
@ -45,30 +52,35 @@ COOKIE_FIELDS = ('prelogin-cookie', 'portal-userauthcookie')
|
|||||||
|
|
||||||
|
|
||||||
class SAMLLoginView:
|
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)
|
Gtk.init(None)
|
||||||
window = Gtk.Window()
|
self.window = window = Gtk.Window()
|
||||||
|
|
||||||
# API reference: https://lazka.github.io/pgi-docs/#WebKit2-4.0
|
# API reference: https://lazka.github.io/pgi-docs/#WebKit2-4.0
|
||||||
|
|
||||||
self.closed = False
|
self.closed = False
|
||||||
self.success = False
|
self.success = False
|
||||||
self.saml_result = {}
|
self.saml_result = {}
|
||||||
self.verbose = verbose
|
self.verbose = args.verbose
|
||||||
|
|
||||||
self.ctx = WebKit2.WebContext.get_default()
|
self.ctx = WebKit2.WebContext.get_default()
|
||||||
if not verify:
|
if not args.verify:
|
||||||
self.ctx.set_tls_errors_policy(WebKit2.TLSErrorsPolicy.IGNORE)
|
self.ctx.set_tls_errors_policy(WebKit2.TLSErrorsPolicy.IGNORE)
|
||||||
self.cookies = self.ctx.get_cookie_manager()
|
self.cookies = self.ctx.get_cookie_manager()
|
||||||
if cookies:
|
if args.cookies:
|
||||||
self.cookies.set_accept_policy(WebKit2.CookieAcceptPolicy.ALWAYS)
|
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()
|
self.wview = WebKit2.WebView()
|
||||||
|
|
||||||
if user_agent is None:
|
if args.no_proxy:
|
||||||
user_agent = 'PAN GlobalProtect'
|
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 = self.wview.get_settings()
|
||||||
settings.set_user_agent(user_agent)
|
settings.set_user_agent(args.user_agent)
|
||||||
self.wview.set_settings(settings)
|
self.wview.set_settings(settings)
|
||||||
|
|
||||||
window.resize(500, 500)
|
window.resize(500, 500)
|
||||||
@ -101,7 +113,8 @@ class SAMLLoginView:
|
|||||||
h = rs.get_http_headers() if rs else None
|
h = rs.get_http_headers() if rs else None
|
||||||
if h:
|
if h:
|
||||||
ct, cl = h.get_content_type(), h.get_content_length()
|
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 '')
|
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)
|
print('[RECEIVE] %sresource %s %s' % (content_details if h else '', m, uri), file=stderr)
|
||||||
|
|
||||||
@ -124,10 +137,17 @@ class SAMLLoginView:
|
|||||||
uri = mr.get_uri()
|
uri = mr.get_uri()
|
||||||
rs = mr.get_response()
|
rs = mr.get_response()
|
||||||
h = rs.get_http_headers() if rs else None
|
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:
|
if self.verbose:
|
||||||
print('[PAGE ] Finished loading page %s' % uri, file=stderr)
|
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
|
# convert to normal dict
|
||||||
d = {}
|
d = {}
|
||||||
@ -212,10 +232,25 @@ class TLSAdapter(requests.adapters.HTTPAdapter):
|
|||||||
We have extracted the relevant value from <openssl/ssl.h>.
|
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):
|
def init_poolmanager(self, connections, maxsize, block=False):
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||||
ssl_context.set_ciphers('DEFAULT:@SECLEVEL=1')
|
ssl_context.set_ciphers('DEFAULT:@SECLEVEL=1')
|
||||||
ssl_context.options |= 1<<2 # OP_LEGACY_SERVER_CONNECT
|
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(
|
self.poolmanager = urllib3.PoolManager(
|
||||||
num_pools=connections,
|
num_pools=connections,
|
||||||
maxsize=maxsize,
|
maxsize=maxsize,
|
||||||
@ -236,6 +271,7 @@ def parse_args(args = None):
|
|||||||
x.add_argument('-K', '--no-cookies', dest='cookies', action='store_const', const=None,
|
x.add_argument('-K', '--no-cookies', dest='cookies', action='store_const', const=None,
|
||||||
help="Don't use or store cookies at all")
|
help="Don't use or store cookies at all")
|
||||||
x = p.add_mutually_exclusive_group()
|
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',
|
x.add_argument('-g','--gateway', dest='interface', action='store_const', const='gateway', default='portal',
|
||||||
help='SAML auth to gateway')
|
help='SAML auth to gateway')
|
||||||
x.add_argument('-p','--portal', dest='interface', action='store_const', const='portal',
|
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('-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('-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('-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('-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)")
|
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=[],
|
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')
|
help='Allow use of insecure renegotiation or ancient 3DES and RC4 ciphers')
|
||||||
p.add_argument('--user-agent', '--useragent', default='PAN GlobalProtect',
|
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)')
|
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")
|
p.add_argument('openconnect_extra', nargs='*', help="Extra arguments to include in output OpenConnect command-line")
|
||||||
args = p.parse_args(args)
|
args = p.parse_args(args)
|
||||||
|
|
||||||
@ -284,7 +322,7 @@ def main(args = None):
|
|||||||
|
|
||||||
s = requests.Session()
|
s = requests.Session()
|
||||||
if args.insecure:
|
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.headers['User-Agent'] = 'PAN GlobalProtect' if args.user_agent is None else args.user_agent
|
||||||
s.cert = args.cert
|
s.cert = args.cert
|
||||||
|
|
||||||
@ -354,7 +392,7 @@ def main(args = None):
|
|||||||
# spawn WebKit view to do SAML interactive login
|
# spawn WebKit view to do SAML interactive login
|
||||||
if args.verbose:
|
if args.verbose:
|
||||||
print("Got SAML %s, opening browser..." % sam, file=stderr)
|
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()
|
Gtk.main()
|
||||||
if slv.closed:
|
if slv.closed:
|
||||||
print("Login window closed by user.", file=stderr)
|
print("Login window closed by user.", file=stderr)
|
||||||
@ -364,7 +402,10 @@ def main(args = None):
|
|||||||
|
|
||||||
# extract response and convert to OpenConnect command-line
|
# extract response and convert to OpenConnect command-line
|
||||||
un = slv.saml_result.get('saml-username')
|
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')):
|
for cn, ifh in (('prelogin-cookie','gateway'), ('portal-userauthcookie','portal')):
|
||||||
cv = slv.saml_result.get(cn)
|
cv = slv.saml_result.get(cn)
|
||||||
@ -393,6 +434,8 @@ def main(args = None):
|
|||||||
if key:
|
if key:
|
||||||
openconnect_args.insert(1, "--sslkey="+key)
|
openconnect_args.insert(1, "--sslkey="+key)
|
||||||
openconnect_args.insert(1, "--certificate="+cert)
|
openconnect_args.insert(1, "--certificate="+cert)
|
||||||
|
if args.no_proxy:
|
||||||
|
openconnect_args.insert(1, "--no-proxy")
|
||||||
|
|
||||||
openconnect_command = ''' echo {} |\n sudo openconnect {}'''.format(
|
openconnect_command = ''' echo {} |\n sudo openconnect {}'''.format(
|
||||||
quote(cv), " ".join(map(quote, openconnect_args)))
|
quote(cv), " ".join(map(quote, openconnect_args)))
|
||||||
@ -400,9 +443,14 @@ def main(args = None):
|
|||||||
if args.verbose:
|
if args.verbose:
|
||||||
# Warn about ambiguities
|
# Warn about ambiguities
|
||||||
if server != args.server and not args.uri:
|
if server != args.server and not args.uri:
|
||||||
print('''IMPORTANT: During the SAML auth, you were redirected from {0} to {1}. This probably '''
|
if args.ignore_redirects:
|
||||||
'''means you should specify {1} as the server for final connection, but we're not 100% '''
|
print('''IMPORTANT: During the SAML auth, you were redirected from {0} to {1}. This probably '''
|
||||||
'''sure about this. You should probably try both.\n'''.format(args.server, server), file=stderr)
|
'''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:
|
if ifh != args.interface and not args.uri:
|
||||||
print('''IMPORTANT: We started with SAML auth to the {} interface, but received a cookie '''
|
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),
|
'''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
|
# redirect stdin from this file, before it is closed by the context manager
|
||||||
# (it will remain accessible via the open file descriptor)
|
# (it will remain accessible via the open file descriptor)
|
||||||
dup2(tf.fileno(), 0)
|
dup2(tf.fileno(), 0)
|
||||||
|
cmd = ["openconnect"] + openconnect_args
|
||||||
if args.exec == 'pkexec':
|
if args.exec == 'pkexec':
|
||||||
cmd = ["pkexec", "--user", "root", "openconnect"] + openconnect_args
|
cmd = ["pkexec", "--user", "root"] + cmd
|
||||||
elif args.exec == 'sudo':
|
elif args.exec == 'sudo':
|
||||||
cmd = ["sudo", "openconnect"] + openconnect_args
|
cmd = ["sudo"] + cmd
|
||||||
execvp(cmd[0], cmd)
|
execvp(cmd[0], cmd)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|||||||
@ -1,13 +1,7 @@
|
|||||||
#!/usr/bin/python3
|
#!/usr/bin/python3
|
||||||
|
|
||||||
from __future__ import print_function
|
from sys import stderr, platform
|
||||||
from sys import stderr, version_info, platform
|
from urllib.parse import urlparse, urlencode
|
||||||
if (version_info >= (3, 0)):
|
|
||||||
from urllib.parse import urlparse, urlencode
|
|
||||||
raw_input = input
|
|
||||||
else:
|
|
||||||
from urlparse import urlparse
|
|
||||||
from urllib import urlencode
|
|
||||||
import requests
|
import requests
|
||||||
import argparse
|
import argparse
|
||||||
import getpass
|
import getpass
|
||||||
@ -68,7 +62,7 @@ if prelogin:
|
|||||||
else:
|
else:
|
||||||
# same request params work for /global-protect/getconfig.esp as for /ssl-vpn/login.esp
|
# same request params work for /global-protect/getconfig.esp as for /ssl-vpn/login.esp
|
||||||
if args.user == None:
|
if args.user == None:
|
||||||
args.user = raw_input('Username: ')
|
args.user = input('Username: ')
|
||||||
if args.password == None:
|
if args.password == None:
|
||||||
args.password = getpass.getpass('Password: ')
|
args.password = getpass.getpass('Password: ')
|
||||||
data=dict(user=args.user, passwd=args.password,
|
data=dict(user=args.user, passwd=args.password,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user