17
0

extensive cleanup, more logging

This commit is contained in:
Daniel Lenski 2020-01-25 16:45:35 -08:00
parent b1c36bf95e
commit ef2bfa6b56

View File

@ -6,18 +6,17 @@ import pprint
import urllib import urllib
import requests import requests
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import os
import ssl import ssl
from operator import setitem
from os import path
from shlex import quote from shlex import quote
from sys import stderr from sys import stderr
from binascii import a2b_base64, b2a_base64 from binascii import a2b_base64, b2a_base64
gi.require_version('Gtk', '3.0') gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
gi.require_version('WebKit2', '4.0') gi.require_version('WebKit2', '4.0')
from gi.repository import WebKit2 from gi.repository import Gtk, WebKit2, GLib
class SAMLLoginView: class SAMLLoginView:
def __init__(self, uri, html=None, verbose=False, cookies=None, verify=True): def __init__(self, uri, html=None, verbose=False, cookies=None, verify=True):
@ -25,6 +24,7 @@ class SAMLLoginView:
# 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.success = False self.success = False
self.saml_result = {} self.saml_result = {}
self.verbose = verbose self.verbose = verbose
@ -42,7 +42,7 @@ class SAMLLoginView:
window.add(self.wview) window.add(self.wview)
window.show_all() window.show_all()
window.set_title("SAML Login") window.set_title("SAML Login")
window.connect('delete-event', Gtk.main_quit) window.connect('delete-event', self.close)
self.wview.connect('load-changed', self.get_saml_headers) self.wview.connect('load-changed', self.get_saml_headers)
self.wview.connect('resource-load-started', self.log_resources) self.wview.connect('resource-load-started', self.log_resources)
@ -51,41 +51,83 @@ class SAMLLoginView:
else: else:
self.wview.load_uri(uri) self.wview.load_uri(uri)
def close(self, window, event):
self.closed = True
Gtk.main_quit()
def log_resources(self, webview, resource, request): def log_resources(self, webview, resource, request):
if self.verbose > 1: if self.verbose > 1:
print('%s for resource %s' % (request.get_http_method() or 'Request', resource.get_uri()), file=stderr) print('[REQUEST] %s for resource %s' % (request.get_http_method() or 'Request', resource.get_uri()), file=stderr)
if self.verbose > 2:
resource.connect('finished', self.log_resource_details, request)
def log_resource_details(self, resource, request):
m = request.get_http_method() or 'Request'
uri = resource.get_uri()
rs = resource.get_response()
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_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)
def log_resource_text(self, resource, result, content_type, charset=None, show_headers=None):
data = resource.get_data_finish(result)
content_details = '%d bytes of %s%s for ' % (len(data), content_type, ('; charset='+charset) if charset else '')
print('[DATA ] %sresource %s' % (content_details, resource.get_uri()), file=stderr)
if show_headers:
for h,v in show_headers.items():
print('%s: %s' % (h, v), file=stderr)
print(file=stderr)
if charset or content_type.startswith('text/'):
print(data.decode(charset or 'utf-8'), file=stderr)
def get_saml_headers(self, webview, event): def get_saml_headers(self, webview, event):
if event != WebKit2.LoadEvent.FINISHED: if event != WebKit2.LoadEvent.FINISHED:
return return
mr = webview.get_main_resource() mr = webview.get_main_resource()
if self.verbose:
print("Finished loading %s" % mr.get_uri(), file=stderr)
rs = mr.get_response() rs = mr.get_response()
h = rs.get_http_headers() h = rs.get_http_headers()
if h: if self.verbose:
l = [] print('[PAGE ] Finished loading page %s' % mr.get_uri(), file=stderr)
def listify(name, value, t=l): if not h:
if (name.startswith('saml-') or name in ('prelogin-cookie', 'portal-userauthcookie')): return
t.append((name, value))
h.foreach(listify) # convert to normal dict
d = dict(l) d = {}
if d and self.verbose: h.foreach(lambda k, v: setitem(d, k, v))
print("Got SAML result headers: %r" % d, file=stderr) # filter to interesting headers
d = self.saml_result fd = {name:v for name, v in d.items() if name.startswith('saml-') or name in ('location', 'prelogin-cookie', 'portal-userauthcookie')}
d.update(dict(l)) if fd and self.verbose:
if 'saml-username' in d and ('prelogin-cookie' in d or 'portal-userauthcookie' in d): print("[SAML ] Got SAML result headers: %r" % fd, file=stderr)
print("Got all required SAML headers, done.", file=stderr) if self.verbose > 1:
self.success = True # display everything we found
Gtk.main_quit() ct = h.get_content_type()
mr.get_data(None, self.log_resource_text, ct[0], ct.params.get('charset'), d)
# check if we're done
self.saml_result.update(fd)
GLib.timeout_add(1000, self.check_done)
def check_done(self):
d = self.saml_result
if 'saml-username' in d and ('prelogin-cookie' in d or 'portal-userauthcookie' in d):
if args.verbose:
print("[SAML ] Got all required SAML headers, done.", file=stderr)
self.success = True
Gtk.main_quit()
def parse_args(args = None): def parse_args(args = None):
p = argparse.ArgumentParser() p = argparse.ArgumentParser()
p.add_argument('server', help='GlobalProtect server (portal or gateway)') p.add_argument('server', help='GlobalProtect server (portal or gateway)')
p.add_argument('--no-verify', dest='verify', action='store_false', default=True, help='Ignore invalid server certificate') p.add_argument('--no-verify', dest='verify', action='store_false', default=True, help='Ignore invalid server certificate')
p.add_argument('-C', '--no-cookies', dest='cookies', action='store_const', const=None, x = p.add_mutually_exclusive_group()
default='~/.gp-saml-gui-cookies', help="Don't use cookies (stored in %(default)s)") x.add_argument('-C', '--cookies', default='~/.gp-saml-gui-cookies',
help='Use and store cookies in this file (instead of default %(default)s)')
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() x = p.add_mutually_exclusive_group()
x.add_argument('-p','--portal', dest='portal', action='store_true', help='SAML auth to portal') x.add_argument('-p','--portal', dest='portal', action='store_true', help='SAML auth to portal')
x.add_argument('-g','--gateway', dest='portal', action='store_false', help='SAML auth to gateway (default)') x.add_argument('-g','--gateway', dest='portal', action='store_false', help='SAML auth to gateway (default)')
@ -102,7 +144,7 @@ def parse_args(args = None):
args.extra = dict(x.split('=', 1) for x in args.extra) args.extra = dict(x.split('=', 1) for x in args.extra)
if args.cookies: if args.cookies:
args.cookies = os.path.expanduser(args.cookies) args.cookies = path.expanduser(args.cookies)
if args.cert and args.key: if args.cert and args.key:
args.cert, args.key = (args.cert, args.key), None args.cert, args.key = (args.cert, args.key), None
@ -127,7 +169,8 @@ if __name__ == "__main__":
sam, uri, html = 'URI', args.server, None sam, uri, html = 'URI', args.server, None
else: else:
endpoint = 'https://{}/{}/prelogin.esp'.format(args.server, ('global-protect' if args.portal else 'ssl-vpn')) endpoint = 'https://{}/{}/prelogin.esp'.format(args.server, ('global-protect' if args.portal else 'ssl-vpn'))
print("Looking for SAML auth tags in response to %s..." % endpoint, file=stderr) if args.verbose:
print("Looking for SAML auth tags in response to %s..." % endpoint, file=stderr)
try: try:
res = s.post(endpoint, verify=args.verify, data=args.extra) res = s.post(endpoint, verify=args.verify, data=args.extra)
except Exception as ex: except Exception as ex:
@ -172,8 +215,11 @@ if __name__ == "__main__":
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) slv = SAMLLoginView(uri, html, verbose=args.verbose, cookies=args.cookies, verify=args.verify)
Gtk.main() Gtk.main()
if slv.closed:
print("Login window closed by user.", file=stderr)
p.exit(1)
if not slv.success: if not slv.success:
p.error('''Login window closed without producing SAML cookie''') p.error('''Login window closed without producing SAML cookies.''')
# 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')
@ -187,10 +233,14 @@ if __name__ == "__main__":
fullpath = ('/global-protect/getconfig.esp' if args.portal else '/ssl-vpn/login.esp') fullpath = ('/global-protect/getconfig.esp' if args.portal else '/ssl-vpn/login.esp')
shortpath = ('portal' if args.portal else 'gateway') shortpath = ('portal' if args.portal else 'gateway')
if args.verbose: if args.verbose:
print('''\n\nSAML response converted to OpenConnect command line invocation:\n''', file=stderr) print('''\nSAML response converted to OpenConnect command line invocation:\n''', file=stderr)
print(''' echo {} |\n openconnect --protocol=gp --user={} --usergroup={}:{} --passwd-on-stdin {}\n'''.format( print(''' echo {} |\n openconnect --protocol=gp --user={} --usergroup={}:{} --passwd-on-stdin {}'''.format(
quote(cv), quote(un), quote(shortpath), quote(cn), quote(args.server)), file=stderr) quote(cv), quote(un), quote(shortpath), quote(cn), quote(args.server)), file=stderr)
print('''\nSAML response converted to test-globalprotect-login.py invocation:\n''', file=stderr)
print(''' test-globalprotect-login.py --user={} -p '' \\\n https://{}{} {}={}\n'''.format(
quote(un), quote(args.server), quote(fullpath), quote(cn), quote(cv)), file=stderr)
varvals = { varvals = {
'HOST': quote('https://%s/%s:%s' % (args.server, shortpath, cn)), 'HOST': quote('https://%s/%s:%s' % (args.server, shortpath, cn)),
'USER': quote(un), 'COOKIE': quote(cv), 'USER': quote(un), 'COOKIE': quote(cv),