]> code.communitydata.science - nu-vpn-proxy.git/commitdiff
updated to new version fo gp-saml-gui and new api
authorBenjamin Mako Hill <mako@atdot.cc>
Wed, 28 Jun 2023 18:59:51 +0000 (11:59 -0700)
committerBenjamin Mako Hill <mako@atdot.cc>
Wed, 28 Jun 2023 19:42:28 +0000 (12:42 -0700)
Apparently new versions of GP hav changed the SAML API some. The
openconnect command now requires --gateway so this has been added.

I haven't tested the general and http scripts but I assume they
work. Someone else should verify.

README-GP-SAML.md
gp-saml-gui.py
openconnect_command-general.sh
openconnect_command-http.sh
openconnect_command-ssh.sh

index 1bc0d2057ad9b14b17f76daf554f11fd098e84b6..12e851ac7f2e73a3c65e4aaaf3dfe0e6d70e68e2 100644 (file)
@@ -1,18 +1,45 @@
-[![Build Status](https://api.travis-ci.org/dlenski/gp-saml-gui.png)](https://travis-ci.org/dlenski/gp-saml-gui)
-
 gp-saml-gui
 ===========
 
 gp-saml-gui
 ===========
 
+[![Test Workflow Status](https://github.com/dlenski/gp-saml-gui/workflows/build/badge.svg)](https://github.com/dlenski/gp-saml-gui/actions/workflows/test.yml)
+
+Table of Contents
+=================
+
+  * [Introduction](#introduction)
+  * [Installation](#installation)
+    * [First, non-Python Dependencies](#first-non-python-dependencies)
+    * [Second, gp-saml-gui itself](#second-gp-saml-gui-itself)
+  * [How to use](#how-to-use)
+    * [Extra arguments to OpenConnect](#extra-arguments-to-openconnect)
+  * [License](#license)
+
+Introduction
+============
+
 This is a helper script to allow you to interactively login to a GlobalProtect VPN
 This is a helper script to allow you to interactively login to a GlobalProtect VPN
-that uses SAML authentication.
+that uses [SAML](https://en.wikipedia.org/wiki/Security_Assertion_Markup_Language)
+authentication, so that you can subsequently connect with [OpenConnect](https://www.infradead.org/openconnect).
+(The GlobalProtect protocol is supported in OpenConnect v8.0 or newer; v8.06+ is recommended.)
 
 Interactive login is, unfortunately, sometimes a necessary alternative to automated
 login via scripts such as
 [zdave/openconnect-gp-okta](https://github.com/zdave/openconnect-gp-okta).
 
 
 Interactive login is, unfortunately, sometimes a necessary alternative to automated
 login via scripts such as
 [zdave/openconnect-gp-okta](https://github.com/zdave/openconnect-gp-okta).
 
+This script is known to work with many GlobalProtect VPNs using the major single-sign-on (SSO) providers:
+
+- Okta (sign-in URLs typically `https://<company>.okta.com/login/*`)
+- Microsoft (sign-in URLs typically `https://login.microsoftonline.com/*`)
+
+Please search and file [issues](https://github.com/dlenski/gp-saml-gui/issues) if you can report success
+or failure with other SSO SAML providers.
+
 Installation
 ============
 
 Installation
 ============
 
+First, non-Python Dependencies
+------------------------------
+
 gp-saml-gui uses GTK, which requires Python 3 bindings.
 
 On Debian / Ubuntu, these are packaged as `python3-gi`, `gir1.2-gtk-3.0`, and
 gp-saml-gui uses GTK, which requires Python 3 bindings.
 
 On Debian / Ubuntu, these are packaged as `python3-gi`, `gir1.2-gtk-3.0`, and
@@ -22,13 +49,34 @@ On Debian / Ubuntu, these are packaged as `python3-gi`, `gir1.2-gtk-3.0`, and
 $ sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-webkit2-4.0
 ```
 
 $ sudo apt install python3-gi gir1.2-gtk-3.0 gir1.2-webkit2-4.0
 ```
 
-Then, set up a virtual environment that can access these system packages,
-activate it, and install the Python dependencies:
+On Fedora (and possibly RHEL/CentOS) the matching libraries are packaged in
+`python3-gobject`, `gtk3-devel`, and `webkit2gtk3-devel`:
 
 ```
 
 ```
-$ virtualenv --python=python3 --system-site-packages venv
-$ . venv/bin/activate
-$ pip install requests
+$ sudo dnf install python3-gobject gtk3-devel webkit2gtk3-devel
+```
+
+On Arch Linux, the libraries are packaged in `gtk3`, `gobject-introspection`
+and `webkit2gtk`:
+
+```
+$ sudo pacman -S gtk3 gobject-introspection webkit2gtk
+```
+
+Second, gp-saml-gui itself
+--------------------------
+
+Install gp-saml-gui itself using `pip`:
+
+```
+$ pip3 install https://github.com/dlenski/gp-saml-gui/archive/master.zip
+...
+$ gp-saml-gui
+usage: gp-saml-gui [-h] [--no-verify] [-C COOKIES | -K] [-p | -g] [-c CERT]
+                   [--key KEY] [-v | -q] [-x | -P | -S] [-u]
+                   [--clientos {Windows,Linux,Mac}] [-f EXTRA]
+                   server [openconnect_extra [openconnect_extra ...]]
+gp-saml-gui: error: the following arguments are required: server, openconnect_extra
 ```
 
 How to use
 ```
 
 How to use
@@ -39,14 +87,15 @@ arguments, such as `--clientos=Windows` (because many GlobalProtect
 servers don't require SAML login, but apparently omit it in their configuration
 for OSes other than Windows).
 
 servers don't require SAML login, but apparently omit it in their configuration
 for OSes other than Windows).
 
-This script will pop up a [GTK WebKit2 WebView](https://webkitgtk.org/) window.
-After you succesfully complete the SAML login via web forms, the script will output
+This script will pop up a [GTK WebKit2 WebView](https://webkitgtk.org/) window
+alongside your terminal window (see this [screenshot](screenshot.png)).
+After you successfully complete the SAML login via web forms, the script will output
 `HOST`, `USER`, `COOKIE`, and `OS` variables in a form that can be used by
 [OpenConnect](http://www.infradead.org/openconnect/juniper.html)
 (similar to the output of `openconnect --authenticate`):
 
 ```sh
 `HOST`, `USER`, `COOKIE`, and `OS` variables in a form that can be used by
 [OpenConnect](http://www.infradead.org/openconnect/juniper.html)
 (similar to the output of `openconnect --authenticate`):
 
 ```sh
-$ eval $( gp-saml-gui.py --clientos=Windows vpn.company.com )
+$ eval $( gp-saml-gui --gateway --clientos=Windows vpn.company.com )
 Got SAML POST content, opening browser...
 Finished loading about:blank...
 Finished loading https://company.okta.com/app/panw_globalprotect/deadbeefFOOBARba1234/sso/saml...
 Got SAML POST content, opening browser...
 Finished loading about:blank...
 Finished loading https://company.okta.com/app/panw_globalprotect/deadbeefFOOBARba1234/sso/saml...
@@ -57,7 +106,7 @@ Got SAML relevant headers, done: {'prelogin-cookie': 'blahblahblah', 'saml-usern
 SAML response converted to OpenConnect command line invocation:
 
     echo 'blahblahblah' |
 SAML response converted to OpenConnect command line invocation:
 
     echo 'blahblahblah' |
-        openconnect --protocol=gp --user='foo12345@corp.company.com' --os=win --usergroup=prelogin-cookie:gateway --passwd-on-stdin vpn.company.com
+        openconnect --protocol=gp --user='foo12345@corp.company.com' --os=win --usergroup=gateway:prelogin-cookie --passwd-on-stdin vpn.company.com
 
 $ echo $HOST; echo $USER; echo $COOKIE; echo $OS
 https://vpn.company.com/gateway:prelogin-cookie
 
 $ echo $HOST; echo $USER; echo $COOKIE; echo $OS
 https://vpn.company.com/gateway:prelogin-cookie
@@ -68,10 +117,24 @@ win
 $ echo "$COOKIE" | openconnect --protocol=gp -u "$USER" --os="$OS" --passwd-on-stdin "$HOST"
 ```
 
 $ echo "$COOKIE" | openconnect --protocol=gp -u "$USER" --os="$OS" --passwd-on-stdin "$HOST"
 ```
 
-TODO
-====
+If you specify either the `-P`/`--pkexec-openconnect` or `-S`/`--sudo-openconnect` options, the script
+will automatically invoke OpenConnect as described, using either [`pkexec` from Polkit](https://www.freedesktop.org/software/polkit/docs/0.106/polkit.8.html)
+or [`sudo`](https://www.sudo.ws/), as specified.
+
+# Extra Arguments to OpenConnect
+
+Extra arguments needed for OpenConnect can be specified by adding ` -- ` to the command line, and then
+appending these. For example:
 
 
-* Packaging
+```sh
+$ gp-saml-gui -P --gateway --clientos=Windows vpn.company.com -- --csd-wrapper=hip-report.sh
+…
+Launching OpenConnect with pkexec, equivalent to:
+    echo blahblahblahlongrandomcookievalue |
+        sudo openconnect --protocol=gp --user=foo12345@corp.company.com --os=win --usergroup=gateway:prelogin-cookie --passwd-on-stdin vpn.company.com
+<pkexec authentication dialog pops up>
+<openconnect runs>
+```
 
 License
 =======
 
 License
 =======
index b702d0c51da9e800c57faa4bdd6adc33723e9857..f540a3b27f80319b1bc9a76e727936963bd70358 100755 (executable)
@@ -2,34 +2,51 @@
 
 try:
     import gi
 
 try:
     import gi
+    gi.require_version('Gtk', '3.0')
+    gi.require_version('WebKit2', '4.0')
+    from gi.repository import Gtk, WebKit2, GLib
 except ImportError:
     try:
         import pgi as gi
 except ImportError:
     try:
         import pgi as gi
+        gi.require_version('Gtk', '3.0')
+        gi.require_version('WebKit2', '4.0')
+        from pgi.repository import Gtk, WebKit2, GLib
     except ImportError:
         gi = None
 if gi is None:
     raise ImportError("Either gi (PyGObject) or pgi module is required.")
 
 import argparse
     except ImportError:
         gi = None
 if gi is None:
     raise ImportError("Either gi (PyGObject) or pgi module is required.")
 
 import argparse
-import pprint
-import urllib
+import urllib3
 import requests
 import xml.etree.ElementTree as ET
 import ssl
 import requests
 import xml.etree.ElementTree as ET
 import ssl
+import tempfile
 
 from operator import setitem
 
 from operator import setitem
-from os import path
+from os import path, dup2, execvp
 from shlex import quote
 from sys import stderr, platform
 from binascii import a2b_base64, b2a_base64
 from urllib.parse import urlparse, urlencode
 from shlex import quote
 from sys import stderr, platform
 from binascii import a2b_base64, b2a_base64
 from urllib.parse import urlparse, urlencode
+from html.parser import HTMLParser
+
+
+class CommentHtmlParser(HTMLParser):
+    def __init__(self):
+        super().__init__()
+        self.comments = []
+
+    def handle_comment(self, data: str) -> None:
+        self.comments.append(data)
+
+
+COOKIE_FIELDS = ('prelogin-cookie', 'portal-userauthcookie')
 
 
-gi.require_version('Gtk', '3.0')
-gi.require_version('WebKit2', '4.0')
-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, user_agent=None):
+        Gtk.init(None)
         window = Gtk.Window()
 
         # API reference: https://lazka.github.io/pgi-docs/#WebKit2-4.0
         window = Gtk.Window()
 
         # API reference: https://lazka.github.io/pgi-docs/#WebKit2-4.0
@@ -40,20 +57,26 @@ class SAMLLoginView:
         self.verbose = verbose
 
         self.ctx = WebKit2.WebContext.get_default()
         self.verbose = verbose
 
         self.ctx = WebKit2.WebContext.get_default()
-        if not args.verify:
+        if not verify:
             self.ctx.set_tls_errors_policy(WebKit2.TLSErrorsPolicy.IGNORE)
         self.cookies = self.ctx.get_cookie_manager()
             self.ctx.set_tls_errors_policy(WebKit2.TLSErrorsPolicy.IGNORE)
         self.cookies = self.ctx.get_cookie_manager()
-        if args.cookies:
+        if cookies:
             self.cookies.set_accept_policy(WebKit2.CookieAcceptPolicy.ALWAYS)
             self.cookies.set_accept_policy(WebKit2.CookieAcceptPolicy.ALWAYS)
-            self.cookies.set_persistent_storage(args.cookies, WebKit2.CookiePersistentStorage.TEXT)
+            self.cookies.set_persistent_storage(cookies, WebKit2.CookiePersistentStorage.TEXT)
         self.wview = WebKit2.WebView()
 
         self.wview = WebKit2.WebView()
 
+        if user_agent is None:
+            user_agent = 'PAN GlobalProtect'
+        settings = self.wview.get_settings()
+        settings.set_user_agent(user_agent)
+        self.wview.set_settings(settings)
+
         window.resize(500, 500)
         window.add(self.wview)
         window.show_all()
         window.set_title("SAML Login")
         window.connect('delete-event', self.close)
         window.resize(500, 500)
         window.add(self.wview)
         window.show_all()
         window.set_title("SAML Login")
         window.connect('delete-event', self.close)
-        self.wview.connect('load-changed', self.get_saml_headers)
+        self.wview.connect('load-changed', self.on_load_changed)
         self.wview.connect('resource-load-started', self.log_resources)
 
         if html:
         self.wview.connect('resource-load-started', self.log_resources)
 
         if html:
@@ -93,42 +116,111 @@ class SAMLLoginView:
         if charset or content_type.startswith('text/'):
             print(data.decode(charset or 'utf-8'), 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 on_load_changed(self, webview, event):
         if event != WebKit2.LoadEvent.FINISHED:
             return
 
         mr = webview.get_main_resource()
         uri = mr.get_uri()
         rs = mr.get_response()
         if event != WebKit2.LoadEvent.FINISHED:
             return
 
         mr = webview.get_main_resource()
         uri = mr.get_uri()
         rs = mr.get_response()
-        h = rs.get_http_headers()
+        h = rs.get_http_headers() if rs else None
+        ct = h.get_content_type()
+
         if self.verbose:
             print('[PAGE   ] Finished loading page %s' % uri, file=stderr)
         if self.verbose:
             print('[PAGE   ] Finished loading page %s' % uri, file=stderr)
-        if not h:
-            return
 
         # convert to normal dict
         d = {}
 
         # convert to normal dict
         d = {}
-        h.foreach(lambda k, v: setitem(d, k, v))
+        h.foreach(lambda k, v: setitem(d, k.lower(), v))
         # filter to interesting headers
         # filter to interesting headers
-        fd = {name:v for name, v in d.items() if name.startswith('saml-') or name in ('prelogin-cookie', 'portal-userauthcookie')}
-        if fd and self.verbose:
-            print("[SAML   ] Got SAML result headers: %r" % fd, file=stderr)
+        fd = {name: v for name, v in d.items() if name.startswith('saml-') or name in COOKIE_FIELDS}
+
+        if fd:
+            if self.verbose:
+                print("[SAML   ] Got SAML result headers: %r" % fd, file=stderr)
+                if self.verbose > 1:
+                    # display everything we found
+                    mr.get_data(None, self.log_resource_text, ct[0], ct.params.get('charset'), d)
+            self.saml_result.update(fd, server=urlparse(uri).netloc)
+            self.check_done()
+
+        if not self.success:
             if self.verbose > 1:
             if self.verbose > 1:
-                # display everything we found
-                ct = h.get_content_type()
-                mr.get_data(None, self.log_resource_text, ct[0], ct.params.get('charset'), d)
+                print("[SAML   ] No headers in response, searching body for xml comments", file=stderr)
+            # asynchronous call to fetch body content, continue processing in callback:
+            mr.get_data(None, self.response_callback, ct)
+
+    def response_callback(self, resource, result, ct):
+        data = resource.get_data_finish(result)
+        content = data.decode(ct.params.get("charset") or "utf-8")
+
+        html_parser = CommentHtmlParser()
+        html_parser.feed(content)
 
 
-        # check if we're done
-        self.saml_result.update(fd, server=urlparse(uri).netloc)
-        GLib.timeout_add(1000, self.check_done)
+        fd = {}
+        for comment in html_parser.comments:
+            if self.verbose > 1:
+                print("[SAML   ] Found comment in response body: '%s'" % comment, file=stderr)
+            try:
+                # xml parser requires valid xml with a single root tag, but our expected content
+                # is just a list of data tags, so we need to improvise
+                xmlroot = ET.fromstring("<fakexmlroot>%s</fakexmlroot>" % comment)
+                # search for any valid first level xml tags (inside our fake root) that could contain SAML data
+                for elem in xmlroot:
+                    if elem.tag.startswith("saml-") or elem.tag in COOKIE_FIELDS:
+                        fd[elem.tag] = elem.text
+            except ET.ParseError:
+                pass  # silently ignore any comments that don't contain valid xml
+
+        if self.verbose > 1:
+            print("[SAML   ] Finished parsing response body for %s" % resource.get_uri(), file=stderr)
+        if fd:
+            if self.verbose:
+                print("[SAML   ] Got SAML result tags: %s" % fd, file=stderr)
+            self.saml_result.update(fd, server=urlparse(resource.get_uri()).netloc)
+
+        if not self.check_done():
+            # Work around timing/race condition by retrying check_done after 1 second
+            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):
 
     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:
+            if self.verbose:
                 print("[SAML   ] Got all required SAML headers, done.", file=stderr)
             self.success = True
             Gtk.main_quit()
                 print("[SAML   ] Got all required SAML headers, done.", file=stderr)
             self.success = True
             Gtk.main_quit()
+            return True
+
+
+class TLSAdapter(requests.adapters.HTTPAdapter):
+    '''Adapt to older TLS stacks that would raise errors otherwise.
+
+    We try to work around different issues:
+    * Enable weak ciphers such as 3DES or RC4, that have been disabled by default
+      in OpenSSL 3.0 or recent Linux distributions.
+    * Enable weak Diffie-Hellman key exchange sizes.
+    * Enable unsafe legacy renegotiation for servers without RFC 5746 support.
+
+    See Also
+    --------
+    https://github.com/psf/requests/issues/4775#issuecomment-478198879
+
+    Notes
+    -----
+    Python is missing an ssl.OP_LEGACY_SERVER_CONNECT constant.
+    We have extracted the relevant value from <openssl/ssl.h>.
+
+    '''
+    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
+        self.poolmanager = urllib3.PoolManager(
+                num_pools=connections,
+                maxsize=maxsize,
+                block=block,
+                ssl_context=ssl_context)
 
 def parse_args(args = None):
     pf2clientos = dict(linux='Linux', darwin='Mac', win32='Windows', cygwin='Windows')
 
 def parse_args(args = None):
     pf2clientos = dict(linux='Linux', darwin='Mac', win32='Windows', cygwin='Windows')
@@ -144,10 +236,10 @@ 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()
     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.add_argument('-p','--portal', dest='interface', action='store_const', const='portal', default='gateway',
-                   help='SAML auth to portal')
-    x.add_argument('-g','--gateway', dest='interface', action='store_const', const='gateway',
-                   help='SAML auth to gateway (default)')
+    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',
+                   help='SAML auth to portal (default)')
     g = p.add_argument_group('Client certificate')
     g.add_argument('-c','--cert', help='PEM file containing client certificate (and optionally private key)')
     g.add_argument('--key', help='PEM file containing client private key (if not included in same file as certificate)')
     g = p.add_argument_group('Client certificate')
     g.add_argument('-c','--cert', help='PEM file containing client certificate (and optionally private key)')
     g.add_argument('--key', help='PEM file containing client private key (if not included in same file as certificate)')
@@ -155,11 +247,20 @@ def parse_args(args = None):
     x = p.add_mutually_exclusive_group()
     x.add_argument('-v','--verbose', default=1, action='count', help='Increase verbosity of explanatory output to stderr')
     x.add_argument('-q','--quiet', dest='verbose', action='store_const', const=0, help='Reduce verbosity to a minimum')
     x = p.add_mutually_exclusive_group()
     x.add_argument('-v','--verbose', default=1, action='count', help='Increase verbosity of explanatory output to stderr')
     x.add_argument('-q','--quiet', dest='verbose', action='store_const', const=0, help='Reduce verbosity to a minimum')
-    g.add_argument('-x','--external', action='store_true', help='Launch external browser (for debugging)')
+    x = p.add_mutually_exclusive_group()
+    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')
     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)")
     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('extra', nargs='*', help='Extra form field(s) to pass to include in the login query string (e.g. "magic-cookie-value=deadbeef01234567")')
-    args = p.parse_args(args = None)
+    p.add_argument('-f','--field', dest='extra', action='append', default=[],
+                   help='Extra form field(s) to pass to include in the login query string (e.g. "-f magic-cookie-value=deadbeef01234567")')
+    p.add_argument('--allow-insecure-crypto', dest='insecure', action='store_true',
+                   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('openconnect_extra', nargs='*', help="Extra arguments to include in output OpenConnect command-line")
+    args = p.parse_args(args)
 
     args.ocos = clientos2ocos[args.clientos]
     args.extra = dict(x.split('=', 1) for x in args.extra)
 
     args.ocos = clientos2ocos[args.clientos]
     args.extra = dict(x.split('=', 1) for x in args.extra)
@@ -178,11 +279,13 @@ def parse_args(args = None):
 
     return p, args
 
 
     return p, args
 
-if __name__ == "__main__":
-    p, args = parse_args()
+def main(args = None):
+    p, args = parse_args(args)
 
     s = requests.Session()
 
     s = requests.Session()
-    s.headers['User-Agent'] = 'PAN GlobalProtect'
+    if args.insecure:
+        s.mount('https://', TLSAdapter())
+    s.headers['User-Agent'] = 'PAN GlobalProtect' if args.user_agent is None else args.user_agent
     s.cert = args.cert
 
     if2prelogin = {'portal':'global-protect/prelogin.esp','gateway':'ssl-vpn/prelogin.esp'}
     s.cert = args.cert
 
     if2prelogin = {'portal':'global-protect/prelogin.esp','gateway':'ssl-vpn/prelogin.esp'}
@@ -193,7 +296,7 @@ if __name__ == "__main__":
         sam, uri, html = 'URI', args.server, None
     else:
         endpoint = 'https://{}/{}'.format(args.server, if2prelogin[args.interface])
         sam, uri, html = 'URI', args.server, None
     else:
         endpoint = 'https://{}/{}'.format(args.server, if2prelogin[args.interface])
-        data = {'tmp':'tmp', 'kerberos-support':'yes', 'ipv6-support':'no', 'clientVer':4100, 'clientos':args.clientos, **args.extra}
+        data = {'tmp':'tmp', 'kerberos-support':'yes', 'ipv6-support':'yes', 'clientVer':4100, 'clientos':args.clientos, **args.extra}
         if args.verbose:
             print("Looking for SAML auth tags in response to %s..." % endpoint, file=stderr)
         try:
         if args.verbose:
             print("Looking for SAML auth tags in response to %s..." % endpoint, file=stderr)
         try:
@@ -209,12 +312,20 @@ if __name__ == "__main__":
             if isinstance(rootex, ssl.CertificateError):
                 p.error("SSL certificate error (try --no-verify to ignore): %s" % rootex)
             elif isinstance(rootex, ssl.SSLError):
             if isinstance(rootex, ssl.CertificateError):
                 p.error("SSL certificate error (try --no-verify to ignore): %s" % rootex)
             elif isinstance(rootex, ssl.SSLError):
-                p.error("SSL error: %s" % rootex)
+                p.error("SSL error (try --allow-insecure-crypto to ignore): %s" % rootex)
             else:
                 raise
         xml = ET.fromstring(res.content)
         if xml.tag != 'prelogin-response':
             p.error("This does not appear to be a GlobalProtect prelogin response\nCheck in browser: {}?{}".format(endpoint, urlencode(data)))
             else:
                 raise
         xml = ET.fromstring(res.content)
         if xml.tag != 'prelogin-response':
             p.error("This does not appear to be a GlobalProtect prelogin response\nCheck in browser: {}?{}".format(endpoint, urlencode(data)))
+        status = xml.find('status')
+        if status != None and status.text != 'Success':
+            msg = xml.find('msg')
+            if msg != None and msg.text == 'GlobalProtect {} does not exist'.format(args.interface):
+                p.error("{} interface does not exist; specify {} instead".format(
+                    args.interface.title(), '--portal' if args.interface=='gateway' else '--gateway'))
+            else:
+                p.error("Error in {} prelogin response: {}".format(args.interface, msg.text))
         sam = xml.find('saml-auth-method')
         sr = xml.find('saml-request')
         if sam is None or sr is None:
         sam = xml.find('saml-auth-method')
         sr = xml.find('saml-request')
         if sam is None or sr is None:
@@ -243,7 +354,7 @@ if __name__ == "__main__":
     # spawn WebKit view to do SAML interactive login
     if args.verbose:
         print("Got SAML %s, opening browser..." % sam, file=stderr)
     # 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)
+    slv = SAMLLoginView(uri, html, verbose=args.verbose, cookies=args.cookies, verify=args.verify, user_agent=args.user_agent)
     Gtk.main()
     if slv.closed:
         print("Login window closed by user.", file=stderr)
     Gtk.main()
     if slv.closed:
         print("Login window closed by user.", file=stderr)
@@ -263,6 +374,29 @@ if __name__ == "__main__":
         cn = ifh = None
         p.error("Didn't get an expected cookie. Something went wrong.")
 
         cn = ifh = None
         p.error("Didn't get an expected cookie. Something went wrong.")
 
+    urlpath = args.interface + ":" + cn
+    openconnect_args = [
+        "--protocol=gp",
+        "--user="+un,
+        "--os="+args.ocos,
+        "--usergroup="+urlpath,
+        "--passwd-on-stdin",
+        server
+    ] + args.openconnect_extra
+
+    if args.insecure:
+        openconnect_args.insert(1, "--allow-insecure-crypto")
+    if args.user_agent:
+        openconnect_args.insert(1, "--useragent="+args.user_agent)
+    if args.cert:
+        cert, key = args.cert
+        if key:
+            openconnect_args.insert(1, "--sslkey="+key)
+        openconnect_args.insert(1, "--certificate="+cert)
+
+    openconnect_command = '''    echo {} |\n        sudo openconnect {}'''.format(
+        quote(cv), " ".join(map(quote, openconnect_args)))
+
     if args.verbose:
         # Warn about ambiguities
         if server != args.server and not args.uri:
     if args.verbose:
         # Warn about ambiguities
         if server != args.server and not args.uri:
@@ -274,14 +408,33 @@ if __name__ == "__main__":
                   '''that's often associated with the {} interface. You should probably try both.\n'''.format(args.interface, ifh),
                   file=stderr)
         print('''\nSAML response converted to OpenConnect command line invocation:\n''', file=stderr)
                   '''that's often associated with the {} interface. You should probably try both.\n'''.format(args.interface, ifh),
                   file=stderr)
         print('''\nSAML response converted to OpenConnect command line invocation:\n''', file=stderr)
-        print('''    echo {} |\n        openconnect --protocol=gp --user={} --os={} --usergroup={}:{} --passwd-on-stdin {}'''.format(
-            quote(cv), quote(un), quote(args.ocos), quote(args.interface), quote(cn), quote(server)), file=stderr)
+        print(openconnect_command, file=stderr)
 
         print('''\nSAML response converted to test-globalprotect-login.py invocation:\n''', file=stderr)
         print('''    test-globalprotect-login.py --user={} --clientos={} -p '' \\\n         https://{}/{} {}={}\n'''.format(
             quote(un), quote(args.clientos), quote(server), quote(if2auth[args.interface]), quote(cn), quote(cv)), file=stderr)
 
         print('''\nSAML response converted to test-globalprotect-login.py invocation:\n''', file=stderr)
         print('''    test-globalprotect-login.py --user={} --clientos={} -p '' \\\n         https://{}/{} {}={}\n'''.format(
             quote(un), quote(args.clientos), quote(server), quote(if2auth[args.interface]), quote(cn), quote(cv)), file=stderr)
-    varvals = {
-        'HOST': quote('https://%s/%s:%s' % (server, if2auth[args.interface], cn)),
-        'USER': quote(un), 'COOKIE': quote(cv), 'OS': quote(args.ocos),
-    }
-    print('\n'.join('%s=%s' % pair for pair in varvals.items()))
+
+    if args.exec:
+        print('''Launching OpenConnect with {}, equivalent to:\n{}'''.format(args.exec, openconnect_command), file=stderr)
+        with tempfile.TemporaryFile('w+') as tf:
+            tf.write(cv)
+            tf.flush()
+            tf.seek(0)
+            # 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)
+        if args.exec == 'pkexec':
+            cmd = ["pkexec", "--user", "root", "openconnect"] + openconnect_args
+        elif args.exec == 'sudo':
+            cmd = ["sudo", "openconnect"] + openconnect_args
+        execvp(cmd[0], cmd)
+
+    else:
+        varvals = {
+            'HOST': quote('https://%s/%s' % (server, urlpath)),
+            'USER': quote(un), 'COOKIE': quote(cv), 'OS': quote(args.ocos),
+        }
+        print('\n'.join('%s=%s' % pair for pair in varvals.items()))
+
+if __name__ == "__main__":
+    main()
index 11d7dab3b3b8574702fbfe7872c5dae56ad9e5ed..b7df39957214439f2225472dcfccf6d14091ac8d 100755 (executable)
@@ -4,7 +4,7 @@
 cd ~/bin/nu-vpn-proxy
 
 ## do the authentication
 cd ~/bin/nu-vpn-proxy
 
 ## do the authentication
-eval $( ./gp-saml-gui.py -v --clientos=Linux vpn-connect2.northwestern.edu ) 
+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='5.1.0-101' --protocol=gp -u "$USER" --os="$OS" --passwd-on-stdin "$HOST" --csd-wrapper="hipreport-modified.sh" --reconnect-timeout 60
 
index 0c519c62e6fe6371fa325ab884ecb1acb3b043c8..e7821b88da21f27e1f42b5cb1e9a79ff4b65e5ee 100755 (executable)
@@ -9,7 +9,7 @@ exec > $LOG_FILE
 cd ~/bin/nu-vpn-proxy
 
 ## do the authentication
 cd ~/bin/nu-vpn-proxy
 
 ## do the authentication
-eval $( ./gp-saml-gui.py -v --clientos=Linux vpn-connect2.northwestern.edu ) 
+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='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}"
index daa1bc095612878daa6ed0bfddfc10aee940e2a9..2a875ff3370936b1cf62a5111e412273e45bb229 100755 (executable)
@@ -1,5 +1,6 @@
-#!/bin/bash
+#!/bin/bash -x
 
 
+OS="linux"
 LOG_FILE=/tmp/nu-globalprotect-saml.log
 PID_FILE=/tmp/nu-vpn-openconnect.pid
 
 LOG_FILE=/tmp/nu-globalprotect-saml.log
 PID_FILE=/tmp/nu-vpn-openconnect.pid
 
@@ -9,7 +10,7 @@ exec > $LOG_FILE
 cd ~/bin/nu-vpn-proxy
 
 ## do the authentication
 cd ~/bin/nu-vpn-proxy
 
 ## do the authentication
-eval $( ./gp-saml-gui.py -v --clientos=Linux vpn-connect2.northwestern.edu ) 
+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 --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}"
 

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