]> code.communitydata.science - nu-vpn-proxy.git/blob - test-globalprotect-login.py
include clientos in prelogin.esp parameters (ping #6)
[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 = xml.find('saml-request').text
118     if sam == 'POST':
119         with NamedTemporaryFile(delete=False, suffix='.html') as tf:
120             tf.write(a2b_base64(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         if args.browse:
128             print("Got SAML REDIRECT, browsing to %s" % sr)
129             webbrowser.open(sr)
130         else:
131             print("Got SAML REDIRECT to:\n\t%s" % sr)
132
133 # Just print the result
134
135 else:
136     if args.verbose:
137         print(res.headers, file=stderr)
138     print(res.text)

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