]> code.communitydata.science - nu-vpn-proxy.git/blob - test-globalprotect-login.py
need to base64-decode URLs from SAML REDIRECT too
[nu-vpn-proxy.git] / test-globalprotect-login.py
1 #!/usr/bin/python3
2
3 from __future__ import print_function
4 from sys import stderr, version_info, platform
5 if (version_info >= (3, 0)):
6     from urllib.parse import urlparse, urlencode
7     raw_input = input
8 else:
9     from urlparse import urlparse
10     from urllib import urlencode
11 import requests
12 import argparse
13 import getpass
14 import os
15 import xml.etree.ElementTree as ET
16 import posixpath
17 from binascii import a2b_base64
18 from tempfile import NamedTemporaryFile
19 from shlex import quote
20
21 clientos_map = dict(linux='Linux', darwin='Mac', win32='Windows', cygwin='Windows')
22 default_clientos = clientos_map.get(platform, 'Windows')
23
24 p = argparse.ArgumentParser()
25 p.add_argument('-v','--verbose', default=0, action='count')
26 p.add_argument('endpoint', help='GlobalProtect server; can append /ssl-vpn/login.esp (default) or /global-protect/getconfig.esp or /{ssl-vpn,global-protect}/prelogin.esp')
27 p.add_argument('extra', nargs='*', help='Extra field to pass to include in the login query string (e.g. "portal-userauthcookie=deadbeef01234567")')
28 g = p.add_argument_group('Login credentials')
29 g.add_argument('-u','--user', help='Username (will prompt if unspecified)')
30 g.add_argument('-p','--password', help='Password (will prompt if unspecified)')
31 g.add_argument('-c','--cert', help='PEM file containing client certificate (and optionally private key)')
32 g.add_argument('--computer', default=os.uname()[1], help="Computer name (default is `hostname`)")
33 g.add_argument('--clientos', choices=set(clientos_map.values()), default=default_clientos, help="clientos value to send (default is %(default)s)")
34 g.add_argument('--key', help='PEM file containing client private key (if not included in same file as certificate)')
35 p.add_argument('-b','--browse', action='store_true', help='Automatically spawn browser for SAML')
36 p.add_argument('--no-verify', dest='verify', action='store_false', default=True, help='Ignore invalid server certificate')
37 args = p.parse_args()
38
39 extra = dict(x.split('=', 1) for x in args.extra)
40 endpoint = urlparse(('https://' if '//' not in args.endpoint else '') + args.endpoint, 'https:')
41 if not endpoint.path:
42     print("Endpoint path unspecified: defaulting to /ssl-vpn/login.esp", file=stderr)
43     endpoint = endpoint._replace(path = '/ssl-vpn/login.esp')
44 prelogin = (posixpath.split(endpoint.path)[-1] == 'prelogin.esp')
45
46 if args.cert and args.key:
47     cert = (args.cert, args.key)
48 elif args.cert:
49     cert = (args.cert, None)
50 elif args.key:
51     p.error('--key specified without --cert')
52 else:
53     cert = None
54
55 s = requests.Session()
56 s.headers['User-Agent'] = 'PAN GlobalProtect'
57 s.cert = cert
58
59 if prelogin:
60     data={
61         # sent by many clients but not known to have any effect
62         'tmp': 'tmp', 'clientVer': 4100, 'kerberos-support': 'yes', 'ipv6-support': 'yes',
63         # affects some clients' behavior (https://github.com/dlenski/gp-saml-gui/issues/6#issuecomment-599743060)
64         'clientos': args.clientos,
65         **extra
66     }
67 else:
68     # same request params work for /global-protect/getconfig.esp as for /ssl-vpn/login.esp
69     if args.user == None:
70         args.user = raw_input('Username: ')
71     if args.password == None:
72         args.password = getpass.getpass('Password: ')
73     data=dict(user=args.user, passwd=args.password,
74               # required
75               jnlpReady='jnlpReady', ok='Login', direct='yes',
76               # optional but might affect behavior
77               clientVer=4100, server=endpoint.netloc, prot='https:',
78               computer=args.computer,
79               **extra)
80 res = s.post(endpoint.geturl(), verify=args.verify, data=data)
81
82 if args.verbose:
83     print("Request body:\n", res.request.body, file=stderr)
84
85 res.raise_for_status()
86
87 # build openconnect "cookie" if the result is a <jnlp>
88
89 try:
90     xml = ET.fromstring(res.text)
91 except Exception:
92     xml = None
93
94 if xml is not None and xml.tag == 'jnlp':
95     arguments = [(t.text or '') for t in xml.iter('argument')]
96     arguments += [''] * (16-len(arguments))
97     cookie = urlencode({'authcookie': arguments[1], 'portal': arguments[3], 'user': arguments[4], 'domain': arguments[7],
98                         'computer': args.computer, 'preferred-ip': arguments[15]})
99     if cert:
100         cert_and_key = ' \\\n        ' + ' '.join('%s "%s"' % (opt, quote(fn)) for opt, fn in zip(('-c','-k'), cert) if fn)
101     else:
102         cert_and_key = ''
103
104     print('''
105
106 Extracted connection cookie from <jnlp>. Use this to connect:
107
108     openconnect --protocol=gp --usergroup=gateway %s \\
109         --cookie %s%s
110 ''' % (quote(endpoint.netloc), quote(cookie), cert_and_key), file=stderr)
111
112 # do SAML request if the result is <prelogin-response><saml...>
113
114 elif xml is not None and xml.tag == 'prelogin-response' and None not in (xml.find('saml-auth-method'), xml.find('saml-request')):
115     import webbrowser
116     sam = xml.find('saml-auth-method').text
117     sr = a2b_base64(xml.find('saml-request').text)
118     if sam == 'POST':
119         with NamedTemporaryFile(delete=False, suffix='.html') as tf:
120             tf.write(sr)
121         if args.browse:
122             print("Got SAML POST, browsing to %s" % tf.name)
123             webbrowser.open('file://' + tf.name)
124         else:
125             print("Got SAML POST, saved to:\n\t%s" % tf.name)
126     elif sam == 'REDIRECT':
127         sr = a2b_base64(sr)
128         if args.browse:
129             print("Got SAML REDIRECT, browsing to %s" % sr)
130             webbrowser.open(sr)
131         else:
132             print("Got SAML REDIRECT to:\n\t%s" % sr)
133
134 # Just print the result
135
136 else:
137     if args.verbose:
138         print(res.headers, file=stderr)
139     print(res.text)

Community Data Science Collective || Want to submit a patch?