Skip to content

Commit

Permalink
Add fake-gp-server.py and gp-auth-and-config test
Browse files Browse the repository at this point in the history
Another set of Flask-based tests.

TODO: need a way to get parameters into the initial session setup (just as
fake-{f5,fortinet,juniper}-server.py do), in order to configure gateways,
2FA requirement, etc.

Signed-off-by: Daniel Lenski <dlenski@gmail.com>
  • Loading branch information
dlenski committed May 3, 2021
1 parent 808a702 commit 19d9d1a
Show file tree
Hide file tree
Showing 3 changed files with 248 additions and 1 deletion.
3 changes: 2 additions & 1 deletion tests/Makefile.am
Expand Up @@ -47,7 +47,7 @@ EXTRA_DIST = certs/ca.pem certs/ca-key.pem certs/user-cert.pem $(USER_KEYS) $(US
softhsm2.conf.in softhsm ns.sh configs/test-dtls-psk.config \
scripts/vpnc-script scripts/vpnc-script-detect-disconnect \
suppressions.lsan fake-fortinet-server.py fake-f5-server.py fake-juniper-server.py \
fake-juniper-sso-server.py fake-tncc.py
fake-juniper-sso-server.py fake-tncc.py fake-gp-server.py

dist_check_SCRIPTS = autocompletion

Expand All @@ -67,6 +67,7 @@ dist_check_SCRIPTS += fortinet-auth-and-config
dist_check_SCRIPTS += f5-auth-and-config
dist_check_SCRIPTS += juniper-auth
dist_check_SCRIPTS += juniper-sso-auth
dist_check_SCRIPTS += gp-auth-and-config
endif

if TEST_PKCS11
Expand Down
185 changes: 185 additions & 0 deletions tests/fake-gp-server.py
@@ -0,0 +1,185 @@
#!/usr/bin/env python3
#
# Copyright © 2021 Daniel Lenski
#
# This file is part of openconnect.
#
# This is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# as published by the Free Software Foundation; either version 2.1 of
# the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#!/usr/bin/env python3

import sys
import ssl
from random import randint
import base64
import time
from json import dumps
from functools import wraps
from flask import Flask, request, abort, redirect, url_for, make_response, session

host, port, *cert_and_maybe_keyfile = sys.argv[1:]

context = ssl.SSLContext()
context.load_cert_chain(*cert_and_maybe_keyfile)

app = Flask(__name__)
app.config.update(SECRET_KEY=b'fake', DEBUG=True, HOST=host, PORT=int(port), SESSION_COOKIE_NAME='fake')

########################################

def cookify(jsonable):
return base64.urlsafe_b64encode(dumps(jsonable).encode())

def check_form_against_session(*fields, use_query=False, on_failure=None):
def inner(fn):
@wraps(fn)
def wrapped(*args, **kwargs):
source = request.args if use_query else request.form
source_name = 'args' if use_query else 'form'
for f in fields:
if on_failure:
if session.get(f) != source.get(f) or f not in source:
return on_failure
else:
assert session.get(f) == source.get(f), \
f'at step {session.get("step")}: {source_name} {f!r} {source.get(f)!r} != session {f!r} {session.get(f)!r}'
return fn(*args, **kwargs)
return wrapped
return inner

########################################


# TODO: need a way to get parameters into the initial session setup (just as
# fake-{f5,fortinet,juniper}-server.py do), in order to configure gateways,
# 2FA requirement, etc.


# Respond to initial prelogin requests
@app.route('/global-protect/prelogin.esp', methods=('GET','POST',))
@app.route('/ssl-vpn/prelogin.esp', methods=('GET','POST',))
def prelogin():
session.update(step='prelogin')
return '''
<prelogin-response>
<status>Success</status>
<ccusername/>
<autosubmit>false</autosubmit>
<msg/>
<newmsg/>
<authentication-message>Please login to this fake VPN</authentication-message>
<username-label>Username</username-label>
<password-label>Password</password-label>
<panos-version>1</panos-version>
<region>EARTH</region>
</prelogin-response>'''.format(request.path)


# Respond to portal getconfig request
@app.route('/global-protect/getconfig.esp', methods=('POST',))
def portal_config():
session.update(step='portal-config', user=request.form.get('user'), passwd=request.form.get('passwd'))
if not (request.form.get('user') and request.form.get('passwd')):
return 'Invalid username or password', 512

gateways = session.get('gateways') or ('Default gateway',)
gwlist = ''.join('<entry name="{}:{}"><description>{}</description></entry>'.format(app.config['HOST'], app.config['PORT'], gw)
for gw in gateways)

return '''<?xml version="1.0" encoding="UTF-8" ?>
<policy><gateways><external><list>{}</list></external></gateways>
<hip-collection><hip-report-interval>600</hip-report-interval></hip-collection>
</policy>'''.format(gwlist)


# Respond to gateway login request
@app.route('/ssl-vpn/login.esp', methods=('POST',))
def gateway_login():
session.update(step='gateway-login')
if not (request.form.get('user') and request.form.get('passwd')):
return 'Invalid username or password', 512
session.update(user=request.form.get('user'), passwd=request.form.get('passwd'))

for k, v in (('jnlpReady', 'jnlpReady'), ('ok', 'Login'), ('direct', 'yes'), ('clientVer', '4100'), ('prot', 'https:')):
if request.form.get(k) != v:
abort(500)
for k in ('clientos', 'os-version', 'server', 'computer'):
if not request.form.get(k):
abort(500)

portal = 'Portal%d' % randint(1, 10)
auth = 'Auth%d' % randint(1, 10)
domain = 'Domain%d' % randint(1, 10)
preferred_ip = request.form.get('preferred-ip') or '192.168.%d.%d' % (randint(2, 254), randint(2, 254))
session.update(preferred_ip=preferred_ip, portal=portal, auth=auth, domain=domain, computer=request.form.get('computer'))
session['authcookie'] = cookify(dict(session)).decode()

return '''<?xml version="1.0" encoding="utf-8"?> <jnlp> <application-desc>
<argument>(null)</argument>
<argument>{authcookie}</argument>
<argument>PersistentCookie</argument>
<argument>{portal}</argument>
<argument>{user}</argument>
<argument>TestAuth</argument>
<argument>vsys1</argument>
<argument>{domain}</argument>
<argument>(null)</argument>
<argument/>
<argument></argument>
<argument></argument>
<argument>tunnel</argument>
<argument>-1</argument>
<argument>4100</argument>
<argument>{preferred_ip}</argument>
</application-desc></jnlp>'''.format(**session)


# Respond to gateway getconfig request
@app.route('/ssl-vpn/getconfig.esp', methods=('POST',))
@check_form_against_session('user', 'portal', 'domain', 'authcookie', on_failure="errors getting SSL/VPN config")
def getconfig():
session.update(step='gateway-config')
return '''<response><ip-address>{preferred_ip}</ip-address>
<ssl-tunnel-url>/ssl-tunnel-connect.sslvpn</ssl-tunnel-url>
</response>'''.format(**session)


# Respond to gateway getconfig request
@app.route('/ssl-vpn/hipreportcheck.esp', methods=('POST',))
@check_form_against_session('user', 'portal', 'domain', 'authcookie', 'computer')
def hipcheck():
session.update(step='gateway-config')
return '''<response><hip-report-needed>no</hip-report-needed></response>'''


# Respond to faux-CONNECT GET-tunnel with 502
# (what the real GP server responds with when it doesn't like the cookie, intended
# to trigger "cookie rejected" error in OpenConnect)
@app.route('/ssl-tunnel-connect.sslvpn')
# Can't use because OpenConnect doesn't send headers here
# @check_form_against_session('user', 'authcookie', use_query=True)
def tunnel():
assert 'user' in request.args and 'authcookie' in request.args
session.update(step='GET-tunnel')
abort(502)


# Respond to 'GET /ssl-vpn/logout.esp' by clearing session and MRHSession
@app.route('/ssl-vpn/logout.esp')
# XX: real server really requires all these fields; see auth-globalprotect.c
@check_form_against_session('authcookie', 'portal', 'user', 'computer')
def logout():
return '<response status="success"/>'


app.run(host=app.config['HOST'], port=app.config['PORT'], debug=True, ssl_context=context)
61 changes: 61 additions & 0 deletions tests/gp-auth-and-config
@@ -0,0 +1,61 @@
#!/bin/sh
#
# Copyright © 2021 Daniel Lenski
#
# This file is part of openconnect.
#
# This is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public License
# as published by the Free Software Foundation; either version 2.1 of
# the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>

# This test uses LD_PRELOAD
PRELOAD=1
srcdir=${srcdir:-.}
top_builddir=${top_builddir:-..}

. `dirname $0`/common.sh

FINGERPRINT="--servercert=d66b507ae074d03b02eafca40d35f87dd81049d3"
CERT=$certdir/server-cert.pem
KEY=$certdir/server-key.pem

echo "Testing GlobalProtect auth against fake server ... "

OCSERV=${srcdir}/fake-gp-server.py
launch_simple_sr_server $ADDRESS 443 $CERT $KEY >/dev/null 2>&1
PID=$!
wait_server $PID 1

echo -n "Authenticating with username/password via portal... "
( echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=gp -q $ADDRESS:443/portal -u test $FINGERPRINT --cookieonly >/dev/null 2>&1) ||
fail $PID "Could not receive cookie from fake GlobalProtect server"

echo ok

echo -n "Authenticating with username/password via gateway... "
( echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=gp -q $ADDRESS:443/gateway -u test $FINGERPRINT --cookieonly >/dev/null 2>&1) ||
fail $PID "Could not receive cookie from fake GlobalProtect server"

echo ok

# TODO: add tests with 2FA

echo -n "Authenticating with username/password via portal, then proceeding to tunnel stage... "
echo "test" | LD_PRELOAD=libsocket_wrapper.so $OPENCONNECT --protocol=gp -q $ADDRESS:443/portal -u test $FINGERPRINT >/dev/null 2>&1
test $? = 2 || # what OpenConnect returns when server rejects cookie upon tunnel connection, as the fake server does
fail $PID "Something went wrong in fake GlobalProtect server (other than the expected rejection of cookie)"

echo ok

cleanup

exit 0

0 comments on commit 19d9d1a

Please sign in to comment.