]> code.communitydata.science - nu-vpn-proxy.git/blob - test-globalprotect-login.py
noting version requirement for openconnect
[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 from itertools import chain
21
22 clientos_map = dict(linux='Linux', darwin='Mac', win32='Windows', cygwin='Windows')
23 default_clientos = clientos_map.get(platform, 'Windows')
24
25 p = argparse.ArgumentParser()
26 p.add_argument('-v','--verbose', default=0, action='count')
27 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')
28 p.add_argument('extra', nargs='*', help='Extra field to pass to include in the login query string (e.g. "portal-userauthcookie=deadbeef01234567")')
29 g = p.add_argument_group('Login credentials')
30 g.add_argument('-u','--user', help='Username (will prompt if unspecified)')
31 g.add_argument('-p','--password', help='Password (will prompt if unspecified)')
32 g.add_argument('-c','--cert', help='PEM file containing client certificate (and optionally private key)')
33 g.add_argument('--computer', default=os.uname()[1], help="Computer name (default is `hostname`)")
34 g.add_argument('--clientos', choices=set(clientos_map.values()), default=default_clientos, help="clientos value to send (default is %(default)s)")
35 g.add_argument('-k','--key', help='PEM file containing client private key (if not included in same file as certificate)')
36 p.add_argument('-b','--browse', action='store_true', help='Automatically spawn browser for SAML')
37 p.add_argument('--no-verify', dest='verify', action='store_false', default=True, help='Ignore invalid server certificate')
38 args = p.parse_args()
39
40 extra = dict(x.split('=', 1) for x in args.extra)
41 endpoint = urlparse(('https://' if '//' not in args.endpoint else '') + args.endpoint, 'https:')
42 if not endpoint.path:
43     print("Endpoint path unspecified: defaulting to /ssl-vpn/login.esp", file=stderr)
44     endpoint = endpoint._replace(path = '/ssl-vpn/login.esp')
45 prelogin = (posixpath.split(endpoint.path)[-1] == 'prelogin.esp')
46
47 if args.cert and args.key:
48     cert = (args.cert, args.key)
49 elif args.cert:
50     cert = (args.cert, None)
51 elif args.key:
52     p.error('--key specified without --cert')
53 else:
54     cert = None
55
56 s = requests.Session()
57 s.headers['User-Agent'] = 'PAN GlobalProtect'
58 s.cert = cert
59
60 if prelogin:
61     data={
62         # sent by many clients but not known to have any effect
63         'tmp': 'tmp', 'clientVer': 4100, 'kerberos-support': 'yes', 'ipv6-support': 'yes',
64         # affects some clients' behavior (https://github.com/dlenski/gp-saml-gui/issues/6#issuecomment-599743060)
65         'clientos': args.clientos,
66         **extra
67     }
68 else:
69     # same request params work for /global-protect/getconfig.esp as for /ssl-vpn/login.esp
70     if args.user == None:
71         args.user = raw_input('Username: ')
72     if args.password == None:
73         args.password = getpass.getpass('Password: ')
74     data=dict(user=args.user, passwd=args.password,
75               # required
76               jnlpReady='jnlpReady', ok='Login', direct='yes',
77               # optional but might affect behavior
78               clientVer=4100, server=endpoint.netloc, prot='https:',
79               computer=args.computer,
80               **extra)
81 res = s.post(endpoint.geturl(), verify=args.verify, data=data)
82
83 if args.verbose:
84     print("Request body:\n", res.request.body, file=stderr)
85
86 res.raise_for_status()
87
88 # build openconnect "cookie" if the result is a <jnlp>
89
90 try:
91     xml = ET.fromstring(res.text)
92 except Exception:
93     xml = None
94
95 if cert:
96     cert_and_key = '\\\n        ' + ' '.join('%s "%s"' % (opt, quote(fn)) for opt, fn in zip(('-c','-k'), cert) if fn) + ' \\\n'
97 else:
98     cert_and_key = ''
99
100 if xml is not None and xml.tag == 'jnlp':
101     arguments = [(t.text or '') for t in xml.iter('argument')]
102     arguments += [''] * (16-len(arguments))
103     cookie = urlencode({'authcookie': arguments[1], 'portal': arguments[3], 'user': arguments[4], 'domain': arguments[7],
104                         'computer': args.computer, 'preferred-ip': arguments[15]})
105
106     print('''
107
108 Extracted connection cookie from <jnlp>. Use this to connect:
109
110     openconnect --protocol=gp --usergroup=gateway %s \\
111         --cookie %s%s
112 ''' % (quote(endpoint.netloc), quote(cookie), cert_and_key), file=stderr)
113
114 # do SAML request if the result is <prelogin-response><saml...>
115
116 elif xml is not None and xml.tag == 'prelogin-response' and None not in (xml.find('saml-auth-method'), xml.find('saml-request')):
117     import webbrowser
118     sam = xml.find('saml-auth-method').text
119     sr = a2b_base64(xml.find('saml-request').text)
120     if sam == 'POST':
121         with NamedTemporaryFile(delete=False, suffix='.html') as tf:
122             tf.write(sr)
123         if args.browse:
124             print("Got SAML POST, browsing to %s" % tf.name)
125             webbrowser.open('file://' + tf.name)
126         else:
127             print("Got SAML POST, saved to:\n\t%s" % tf.name)
128     elif sam == 'REDIRECT':
129         sr = a2b_base64(sr)
130         if args.browse:
131             print("Got SAML REDIRECT, browsing to %s" % sr)
132             webbrowser.open(sr)
133         else:
134             print("Got SAML REDIRECT to:\n\t%s" % sr)
135
136 # if it's a portal config response, pass along to gateway
137
138 elif xml is not None and xml.tag == 'policy':
139
140     uemail = xml.find('user-email')
141     if uemail: uemail = uemail.text
142     cookies = [(cn, xml.find(cn).text) for cn in ('portal-prelogonuserauthcookie', 'portal-userauthcookie')]
143     gateways = [(e.find('description').text, e.get('name')) for e in set(chain(xml.findall('gateways/external/list/entry'), xml.findall('gateways6/external/list/entry')))]
144
145     print('''\nPortal config response response converted to new test-globalprotect-login.py invocation for gateway login:\n'''
146           '''    test-globalprotect-login.py --user={} --clientos={} -p {} {}\\\n'''
147           '''        https://{}/ssl-vpn/login.esp \\\n'''
148           '''        {}\n'''.format(
149               quote(args.user), quote(args.clientos), quote(args.password), cert_and_key, quote(gateways[0][1]),
150               ' '.join(cn+'='+quote(cv) for cn, cv in cookies),
151               file=stderr))
152
153     if uemail and uemail != args.user:
154         print('''IMPORTANT: Portal config contained different username. You might need to try\n'''
155               '''{} instead.\n'''.format(uemail))
156     if len(gateways)>1:
157         print('''Received multiple gateways. Options include:\n    {}\n'''.format('\n    '.join('%s => %s' % (desc, host) for desc, host in gateways)))
158
159 # Just print the result
160
161 else:
162     if args.verbose:
163         print(res.headers, file=stderr)
164     print(res.text)

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