From: Benjamin Mako Hill Date: Mon, 10 Mar 2025 06:19:40 +0000 (-0700) Subject: Merge branch 'master' of ssh://gitea.communitydata.science:2200/collective/nu-vpn... X-Git-Url: https://code.communitydata.science/nu-vpn-proxy.git/commitdiff_plain/ea64bb6af50a1b0a82a4a9b3cbdb89896754fec8?hp=afa6ad3bdc47f8743a03814a4e7be5fac3f19559 Merge branch 'master' of ssh://gitea.communitydata.science:2200/collective/nu-vpn-proxy into cdsc --- diff --git a/gp-saml-gui.py b/gp-saml-gui.py index f540a3b..a046e53 100755 --- a/gp-saml-gui.py +++ b/gp-saml-gui.py @@ -1,9 +1,15 @@ #!/usr/bin/env python3 +import warnings try: import gi + 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 except ImportError: try: @@ -11,10 +17,11 @@ except ImportError: gi.require_version('Gtk', '3.0') gi.require_version('WebKit2', '4.0') 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: gi = 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 urllib3 @@ -24,11 +31,11 @@ import ssl import tempfile from operator import setitem -from os import path, dup2, execvp +from os import path, dup2, execvp, environ from shlex import quote from sys import stderr, platform 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 @@ -45,30 +52,35 @@ COOKIE_FIELDS = ('prelogin-cookie', 'portal-userauthcookie') 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) - window = Gtk.Window() + self.window = window = Gtk.Window() # API reference: https://lazka.github.io/pgi-docs/#WebKit2-4.0 self.closed = False self.success = False self.saml_result = {} - self.verbose = verbose + self.verbose = args.verbose self.ctx = WebKit2.WebContext.get_default() - if not verify: + if not args.verify: self.ctx.set_tls_errors_policy(WebKit2.TLSErrorsPolicy.IGNORE) self.cookies = self.ctx.get_cookie_manager() - if cookies: + if args.cookies: 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() - if user_agent is None: - user_agent = 'PAN GlobalProtect' + if args.no_proxy: + 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.set_user_agent(user_agent) + settings.set_user_agent(args.user_agent) self.wview.set_settings(settings) window.resize(500, 500) @@ -101,7 +113,8 @@ class SAMLLoginView: 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_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 '') print('[RECEIVE] %sresource %s %s' % (content_details if h else '', m, uri), file=stderr) @@ -124,10 +137,17 @@ class SAMLLoginView: uri = mr.get_uri() rs = mr.get_response() 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: 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 d = {} @@ -212,10 +232,25 @@ class TLSAdapter(requests.adapters.HTTPAdapter): We have extracted the relevant value from . ''' + + def __init__(self, verify=True): + self.verify = verify + super().__init__() + def init_poolmanager(self, connections, maxsize, block=False): ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) ssl_context.set_ciphers('DEFAULT:@SECLEVEL=1') 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( num_pools=connections, maxsize=maxsize, @@ -236,6 +271,7 @@ def parse_args(args = None): 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() + 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', help='SAML auth to gateway') 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('-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('-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('--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=[], @@ -259,6 +296,7 @@ def parse_args(args = None): help='Allow use of insecure renegotiation or ancient 3DES and RC4 ciphers') 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)') + 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") args = p.parse_args(args) @@ -284,7 +322,7 @@ def main(args = None): s = requests.Session() 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.cert = args.cert @@ -354,7 +392,7 @@ def main(args = None): # spawn WebKit view to do SAML interactive login if args.verbose: 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() if slv.closed: print("Login window closed by user.", file=stderr) @@ -364,7 +402,10 @@ def main(args = None): # extract response and convert to OpenConnect command-line 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')): cv = slv.saml_result.get(cn) @@ -393,6 +434,8 @@ def main(args = None): if key: openconnect_args.insert(1, "--sslkey="+key) openconnect_args.insert(1, "--certificate="+cert) + if args.no_proxy: + openconnect_args.insert(1, "--no-proxy") openconnect_command = ''' echo {} |\n sudo openconnect {}'''.format( quote(cv), " ".join(map(quote, openconnect_args))) @@ -400,9 +443,14 @@ def main(args = None): if args.verbose: # Warn about ambiguities if server != args.server and not args.uri: - print('''IMPORTANT: During the SAML auth, you were redirected from {0} to {1}. This probably ''' - '''means you should specify {1} as the server for final connection, but we're not 100% ''' - '''sure about this. You should probably try both.\n'''.format(args.server, server), file=stderr) + if args.ignore_redirects: + print('''IMPORTANT: During the SAML auth, you were redirected from {0} to {1}. This probably ''' + '''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: 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), @@ -423,10 +471,11 @@ def main(args = None): # redirect stdin from this file, before it is closed by the context manager # (it will remain accessible via the open file descriptor) dup2(tf.fileno(), 0) + cmd = ["openconnect"] + openconnect_args if args.exec == 'pkexec': - cmd = ["pkexec", "--user", "root", "openconnect"] + openconnect_args + cmd = ["pkexec", "--user", "root"] + cmd elif args.exec == 'sudo': - cmd = ["sudo", "openconnect"] + openconnect_args + cmd = ["sudo"] + cmd execvp(cmd[0], cmd) else: diff --git a/hipreport-modified.sh b/hipreport-modified.sh index 2253596..68be2a5 100755 --- a/hipreport-modified.sh +++ b/hipreport-modified.sh @@ -69,8 +69,8 @@ cat <$NOW - 5.1.0-101 - Linux 4.19.0-6-amd64 + 6.3.0-33 + Linux 6.1.0-31-amd64 Linux domain.com spes @@ -102,8 +102,8 @@ cat <4 - 5.1.0-101 - Linux 4.19.0-6-amd64 + 6.3.0-33 + Linux 6.1.0-31-amd64 Linux domain.com spes diff --git a/openconnect_command-general.sh b/openconnect_command-general.sh index b7df399..59f32e9 100755 --- a/openconnect_command-general.sh +++ b/openconnect_command-general.sh @@ -6,5 +6,5 @@ cd ~/bin/nu-vpn-proxy ## do the authentication eval $( ./gp-saml-gui.py -v --gateway --clientos=Linux vpn-connect2.northwestern.edu ) -echo "$COOKIE" | sudo openconnect --useragent="PAN GlobalConnect" --version-string='5.1.0-101' --protocol=gp -u "$USER" --os="$OS" --passwd-on-stdin "$HOST" --csd-wrapper="hipreport-modified.sh" --reconnect-timeout 60 +echo "$COOKIE" | sudo openconnect --useragent="PAN GlobalConnect" --version-string='6.3.0-33' --protocol=gp -u "$USER" --os="$OS" --passwd-on-stdin "$HOST" --csd-wrapper="hipreport-modified.sh" --reconnect-timeout 60 diff --git a/openconnect_command-http.sh b/openconnect_command-http.sh index e7821b8..335fd81 100755 --- a/openconnect_command-http.sh +++ b/openconnect_command-http.sh @@ -12,6 +12,6 @@ cd ~/bin/nu-vpn-proxy eval $( ./gp-saml-gui.py -v --gateway --clientos=Linux vpn-connect2.northwestern.edu ) -echo "$COOKIE" | /usr/sbin/openconnect --verbose --useragent="PAN GlobalConnect" --version-string='5.1.0-101' --protocol=gp -u "$USER" --os="$OS" --passwd-on-stdin "$HOST" --csd-wrapper="hipreport-modified.sh" --reconnect-timeout 60 --script-tun --script "ocproxy -D 8181 --keepalive 5 --verbose" -b --pid-file "${PID_FILE}" +echo "$COOKIE" | /usr/sbin/openconnect --verbose --useragent="PAN GlobalConnect" --version-string='6.3.0-33' --protocol=gp -u "$USER" --os="$OS" --passwd-on-stdin "$HOST" --csd-wrapper="hipreport-modified.sh" --reconnect-timeout 60 --script-tun --script "ocproxy -D 8181 --keepalive 5 --verbose" -b --pid-file "${PID_FILE}" diff --git a/openconnect_command-ssh.sh b/openconnect_command-ssh.sh index 2a875ff..d20f723 100755 --- a/openconnect_command-ssh.sh +++ b/openconnect_command-ssh.sh @@ -12,5 +12,4 @@ cd ~/bin/nu-vpn-proxy ## do the authentication eval $( ./gp-saml-gui.py -v --gateway --clientos=Linux vpn-connect2.northwestern.edu ) -echo "$COOKIE" | /usr/sbin/openconnect --useragent="PAN GlobalConnect" --version-string='5.1.0-101' --protocol=gp -u "$USER" --os="$OS" --passwd-on-stdin "$HOST" --csd-wrapper="hipreport-modified.sh" --reconnect-timeout 60 --script-tun --script "ocproxy -D 9052" -b --pid-file "${PID_FILE}" - +echo "${COOKIE}" | /usr/sbin/openconnect --protocol=gp '--useragent=PAN GlobalProtect' --user="${USER}" --os="${OS}" --usergroup=gateway:prelogin-cookie --passwd-on-stdin vpn-connect2.northwestern.edu --csd-wrapper="hipreport-modified.sh" --reconnect-timeout 60 --script-tun --script "ocproxy -D 9052" -b --disable-ipv6 --no-dtls --pid-file "${PID_FILE}" diff --git a/ssh-vpn-proxy b/ssh-vpn-proxy index 58f847a..6f3644c 100755 --- a/ssh-vpn-proxy +++ b/ssh-vpn-proxy @@ -2,7 +2,8 @@ export OPENSSL_CONF="${HOME}/bin/nu-vpn-proxy/openssl.conf" # this allows for legacy renegotation which seems to be required now -SEARCH_PATTERN="ESP tunnel connected; exiting HTTPS mainloop." +# SEARCH_PATTERN="ESP tunnel connected; exiting HTTPS mainloop." +SEARCH_PATTERN="Continuing in background; pid" # connects to SSH through openconnect and VPN # for use with ProxyCommand in SSH diff --git a/test-globalprotect-login.py b/test-globalprotect-login.py index 74a8605..8cee96f 100755 --- a/test-globalprotect-login.py +++ b/test-globalprotect-login.py @@ -1,13 +1,7 @@ #!/usr/bin/python3 -from __future__ import print_function -from sys import stderr, version_info, platform -if (version_info >= (3, 0)): - from urllib.parse import urlparse, urlencode - raw_input = input -else: - from urlparse import urlparse - from urllib import urlencode +from sys import stderr, platform +from urllib.parse import urlparse, urlencode import requests import argparse import getpass @@ -68,7 +62,7 @@ if prelogin: else: # same request params work for /global-protect/getconfig.esp as for /ssl-vpn/login.esp if args.user == None: - args.user = raw_input('Username: ') + args.user = input('Username: ') if args.password == None: args.password = getpass.getpass('Password: ') data=dict(user=args.user, passwd=args.password,