dlenski / openconnect

OpenConnect client extended to support Palo Alto Networks' GlobalProtect VPN
679 stars 130 forks source link

How to indicate a form field for authentication purposes (Global Protect / SAML)? #149

Closed natalie-o-perret closed 5 years ago

natalie-o-perret commented 5 years ago

I am trying to connect to a corporate vpn using openconnect (protocol Global Protect):

perret at perret-pc in ~
$ openconnect --protocol=gp [vpn-address]
POST [vpn-address]/ssl-vpn/prelogin.esp?tmp=tmp&clientVer=4100&clientos=Linux
Connected to 185.183.115.255:443
SSL negotiation with [vpn-address]
Connected to HTTPS on [vpn-address]
POST [vpn-address]/global-protect/prelogin.esp?tmp=tmp&clientVer=4100&clientos=Linux
SAML authentication via POST to <html>
<body>
<form id="myform" method="POST" action="[corporate-address]/auth/realms/vpgrp/protocol/saml">
<input type="hidden" name="SAMLRequest" value="PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBBc3NlcnRpb25Db25zdW1lclNlcnZpY2VVUkw9Imh0dHBzOi8vdnBuLnZlZXBlZS50ZWNoOjQ0My9TQU1MMjAvU1AvQUNTIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9zc29jby5wbGF0Zm9ybS52cGdycC5uZXQvYXV0aC9yZWFsbXMvdnBncnAvcHJvdG9jb2wvc2FtbCIgSUQ9Il85YTkwNDYyMzQyY2YyMWU0ZmE0YzUyYmVmOTRjZDNhNSIgSXNzdWVJbnN0YW50PSIyMDE5LTA4LTAyVDIxOjUyOjE0WiIgUHJvdG9jb2xCaW5kaW5nPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YmluZGluZ3M6SFRUUC1QT1NUIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3VlciB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL3Zwbi52ZWVwZWUudGVjaDo0NDMvU0FNTDIwL1NQPC9zYW1sOklzc3Vlcj48L3NhbWxwOkF1dGhuUmVxdWVzdD4=" />
<input type="hidden" name="RelayState" value="GCkAANn0plw4NzIwNDY4YmM4ZGE2ZjcwMzU3ZWQ5M2E0NjI5ZDE2Yw==" />
</form>
<script>
  document.getElementById('myform').submit();
</script>
</body>
</html>
 is required.
Must specify destination form field by appending :field_name to login URL.
Failed to parse server response
Failed to obtain WebVPN cookie

I am not too sure about this part:

Must specify destination form field by appending :field_name to login URL.

I thought it would be something like that:

openconnect --protocol=gp [vpn-address]:myform
POST https://[vpn-address]:myform/ssl-vpn/prelogin.esp?tmp=tmp&clientVer=4100&clientos=Linux
getaddrinfo failed for host '[vpn-address]:myform': Name or service not known
Failed to open HTTPS connection to [vpn-address]:myform
Failed to obtain WebVPN cookie

But as you can see, it does not work either.

Any idea?

It might be related to this piece of the source code: https://github.com/dlenski/openconnect/blob/master/auth-globalprotect.c#L91

dlenski commented 5 years ago

This is a good find. You are invoking it like this, right?

$ openconnect --prot=gp vpn.veepee.tech:myform

Which is causing openconnect to interpret :myform as the TCP port number.

Workarounds:

  1. Use a slash after the server name to disambiguate this as appended to the URL's path, not part of the server+port:
    openconnect --prot=gp vpn.veepee.tech/:blah_blah
  2. Use --usergroup which has the same effect on openconnect internally, but only available from the command line, not graphical front-ends:
    openconnect --prot=gp vpn.veepee.tech --usergroup :blah_blah

The reason I don't think this has come up before is that you should not normally have to specify an alternate form field manually. It's really intended for use by scripts that handle the SAML/Okta authentication, like @arthepsy's pan-globalprotect-okta wrapper.

natalie-o-perret commented 5 years ago

Thanks for your answer: Actually I noticed that when copying pasting the

<html>
    <body>
        <form id="myform" method="POST" action="https://ssoco.platform.vpgrp.net/auth/realms/vpgrp/protocol/saml">
            <input type="hidden" name="SAMLRequest" value="PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBBc3NlcnRpb25Db25zdW1lclNlcnZpY2VVUkw9Imh0dHBzOi8vdnBuLnZlZXBlZS50ZWNoOjQ0My9TQU1MMjAvU1AvQUNTIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9zc29jby5wbGF0Zm9ybS52cGdycC5uZXQvYXV0aC9yZWFsbXMvdnBncnAvcHJvdG9jb2wvc2FtbCIgSUQ9Il85YzBmZTRlYmU2OWEzMmNkZjY5NjU4NWYzM2IzODIyZiIgSXNzdWVJbnN0YW50PSIyMDE5LTA4LTAyVDIyOjUzOjM1WiIgUHJvdG9jb2xCaW5kaW5nPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YmluZGluZ3M6SFRUUC1QT1NUIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3VlciB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwczovL3Zwbi52ZWVwZWUudGVjaDo0NDMvU0FNTDIwL1NQPC9zYW1sOklzc3Vlcj48L3NhbWxwOkF1dGhuUmVxdWVzdD4=" />
            <input type="hidden" name="RelayState" value="KykAANn0plw1YTMwNTY2ZjI5NWE0Y2E0Nzg5NjQ1ZTgyZjUwOGUwNA==" />
        </form>
        <script>
            document.getElementById('myform').submit();
        </script>
    </body>
</html>

in a html file, I then get the html augmented:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" class="login-pf">

<head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="robots" content="noindex, nofollow">

    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title> Log in to vpgrp
    </title>
    <link rel="icon" href="/auth/resources/3.4.3.final/login/vpgrp-ssoco/img/favicon.png" />
    <link href="/auth/resources/3.4.3.final/login/vpgrp-ssoco/lib/zocial/zocial.css" rel="stylesheet" />
    <link href="/auth/resources/3.4.3.final/login/vpgrp-ssoco/css/typography.css" rel="stylesheet" />
    <link href="/auth/resources/3.4.3.final/login/vpgrp-ssoco/css/login.css" rel="stylesheet" />
    <script src="/auth/resources/3.4.3.final/login/vpgrp-ssoco/js/script.js" type="text/javascript"></script>
    <SCRIPT>
        if (typeof history.replaceState === 'function') {
            history.replaceState({}, "some title", "https://ssoco.platform.vpgrp.net/auth/realms/vpgrp/login-actions/authenticate?execution=cfed83a7-1a4c-4ff5-9fdd-4ca9d171c8b5&client_id=https%3A%2F%2Fvpn.veepee.tech%3A443%2FSAML20%2FSP&tab_id=88hvehNt6ug");
        }
    </SCRIPT>
</head>

<body class="">
    <div id="kc-container" class="">
        <div id="kc-container-wrapper" class="">
            <div class="bgrd-overlay"></div>
            <div class="kc-container-centered">
                <div id="kc-content" class="col-sm-12 col-md-12 col-lg-12 container">
                    <div id="kc-content-wrapper" class="row">

                        <div id="kc-form" class="col-xs-12 col-sm-8 col-md-8 col-lg-7 login">
                            <div id="kc-header" class="col-xs-12 col-sm-8 col-md-8 col-lg-7">
                                <div class="kc-header-bg-blur"></div>
                                <div id="kc-header-wrapper" class="">
                                    <header role="banner">
                                        <h1 id="logoVp" style="background-image: url(/auth/resources/3.4.3.final/login/vpgrp-ssoco/img/veepee_black.png)"></h1>
                                    </header>
                                </div>
                            </div>

                            <div id="kc-form-wrapper" class="">
                                <form id="kc-form-login" class="form-horizontal" onsubmit="login.disabled = true; return true;" action="https://ssoco.platform.vpgrp.net/auth/realms/vpgrp/login-actions/authenticate?code=X8eiFlBIt2COfU1oRld5SzKwaLkzzkGYpI_ouZqm7No&amp;execution=cfed83a7-1a4c-4ff5-9fdd-4ca9d171c8b5&amp;client_id=https%3A%2F%2Fvpn.veepee.tech%3A443%2FSAML20%2FSP&amp;tab_id=88hvehNt6ug" method="post">
                                    <div class="separator"></div>
                                    <div class="kc-form-login-inner">
                                        <div class="form-group input-wrap">
                                            <input tabindex="1" id="username" class="form-control" name="username" value="" type="text" autofocus autocomplete="off" placeholder="Username or Email" />
                                        </div>

                                        <div class="form-group input-wrap">
                                            <input tabindex="2" id="password" class="form-control" name="password" type="password" autocomplete="off" placeholder="Password" />
                                        </div>

                                    </div>

                                    <div class="form-group">
                                        <div id="kc-form-buttons" class="col-xs-8 col-sm-7 col-md-4 col-lg-4 submit">
                                            <div class=" login-button">
                                                <input tabindex="4" class="btn-primary-vp" name="login" id="kc-login" type="submit" value="Log in" />
                                            </div>
                                        </div>
                                        <div id="kc-form-options" class="col-xs-4 col-sm-5 col-md-offset-4 col-md-4 col-lg-offset-3 col-lg-5">
                                            <div class="checkbox">
                                                <a class="checkbox-inner"></a>
                                                <input tabindex="3" onclick="handleCheckboxChange()" id="rememberMe" name="rememberMe" type="checkbox" tabindex="3">
                                                <label for="rememberMe" id="rememberMe">Remember me</label>
                                            </div>
                                            <div class="clear form-options-wrapper">
                                            </div>
                                        </div>
                                    </div>
                                </form>
                            </div>
                        </div>

                    </div>
                </div>
            </div>
        </div>
    </div>
    <footer>
        <section>
        </section>
    </footer>

</body>

</html>

Wondering if two field names can be specified instead of one?

Namely: username and password

dlenski commented 5 years ago

Wondering if two field names can be specified instead of one?

Namely: username and password

In short: no.

The form that prelogin.esp is sending you is not a "normal" GlobalProtect login form with differently-named fields. Submitting it with the right values won't get you the normal GlobalProtect authcookie for VPN tunnel connection purposes.

It's a whole ’nother beast/layer that's somewhat outside the scope of what OpenConnect currently handles in terms of authentication.

For now (as of OpenConnect v8.03), you need to use or write a wrapper script that will do the SAML authentication, figure out what kind of cookie the SAML authentication produces, and then pass that to openconnect (along with the field name of that output cookie, often portal-userauthcookie or something like that).

If you need to write a script to handle this case, I'd probably suggest starting with pan-globalprotect-okta wrapper.

However, in some ways I prefer the style of my own smxlogin, which interfaces with openconnect in a way that's closer to `openconnect --authenticate; it does the authentication process, and then outputs a set of shell variables that can be passed to openconnect to make the actual connection.

natalie-o-perret commented 5 years ago

Alright thanks, that makes things a lil' more clear now. Will probably update this thread with my findings.

Again, thank you!

natalie-o-perret commented 5 years ago

Alright was busy with windows for a while.

@dlenski I automated the whole saml thing in Python (it takes username, password + generated code on the smartphone app) and ended up with that last 2 responses, respectively:

<HTML>

<HEAD>
    <TITLE>SAML HTTP Post Binding</TITLE>
    <SCRIPT>
        if (typeof history.replaceState === 'function') {
            history.replaceState({}, "some title", "https://[another-corporate-url]/auth/realms/vpgrp/login-actions/authenticate?client_id=https%3A%2F%2Fvpn.[my-company-url]%3A443%2FSAML20%2FSP&tab_id=E14BCom1yVk");
        }
    </SCRIPT>
</HEAD>

<BODY Onload="document.forms[0].submit()">
    <FORM METHOD="POST" ACTION="https://vpn.[my-company-url]:443/SAML20/SP/ACS">
        <INPUT TYPE="HIDDEN" NAME="SAMLResponse" VALUE="PHNhbWxwOlJlc3BvbnNlIHhtbG5zOnNhbWxwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiIHhtbG5zOnNhbWw9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIERlc3RpbmF0aW9uPSJodHRwczovL3Zwbi52ZWVwZWUudGVjaDo0NDMvU0FNTDIwL1NQL0FDUyIgSUQ9IklEX2VhM2M3MTkyLTNhMjEtNDZmNC1iNDMxLWU3ZDExNTgxNzE5MSIgSW5SZXNwb25zZVRvPSJfZWUxYTJkNDE5MzZiNWE0YzIwMTg3MzMxM2IzNzA1ODgiIElzc3VlSW5zdGFudD0iMjAxOS0wOC0yMlQyMDozMDo0NC44NTZaIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3Vlcj5odHRwczovL3Nzb2NvLnBsYXRmb3JtLnZwZ3JwLm5ldC9hdXRoL3JlYWxtcy92cGdycDwvc2FtbDpJc3N1ZXI+PGRzaWc6U2lnbmF0dXJlIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkc2lnOlNpZ25lZEluZm8+PGRzaWc6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkc2lnOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48ZHNpZzpSZWZlcmVuY2UgVVJJPSIjSURfZWEzYzcxOTItM2EyMS00NmY0LWI0MzEtZTdkMTE1ODE3MTkxIj48ZHNpZzpUcmFuc2Zvcm1zPjxkc2lnOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzaWc6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kc2lnOlRyYW5zZm9ybXM+PGRzaWc6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzaWc6RGlnZXN0VmFsdWU+VEREc0dTZUhqY0hBdWx6NHlZR3dUTC9hL0xjd1IxL1FnODRoSXRxZCtEND08L2RzaWc6RGlnZXN0VmFsdWU+PC9kc2lnOlJlZmVyZW5jZT48L2RzaWc6U2lnbmVkSW5mbz48ZHNpZzpTaWduYXR1cmVWYWx1ZT5DZnFtZ1hZS01nOUlMbWg1S28xZmxRdTBQSG1OR1RsVWczaGY3cTNLa2loZTljeXpERzNPR0Y1empKaEZpOEJkV1hJWEVnVVNKYWZnQ3ZqV09Vdms4R1RqRmpxS1Y0T0xDSzIrSkhRMERPZXJPSGIzZHIyZ242dmgrK0VxeklYOWZmekdpSk0rTHpjY3lhQ1BKVTBnNjg4RnBFYk5MRHY3WCtyZjVyck1mTVFSVVdoM21OK3ZrQm5QK0tydjhiMVVla1Jhb0wvU3o5QmlFMCtqYVdkNWE2azNkOU9IdE5ET1crdnRiaTl6L0w5MzFwZEFYWUd6aG1LZUpDU2tWM2t5MVUrcTc3SjFOeElvY2o4b3ZWd1l3SWFmNjY2VVcycUdkdWpGZ2gvWGR4V1hYUHU1eGhTcmxmVkVtWEVkVlBCK2NIK2h3TVJRU3hXNUpua0pDcGlOK1E9PTwvZHNpZzpTaWduYXR1cmVWYWx1ZT48ZHNpZzpLZXlJbmZvPjxkc2lnOktleU5hbWU+Q049dnBncnA8L2RzaWc6S2V5TmFtZT48ZHNpZzpYNTA5RGF0YT48ZHNpZzpYNTA5Q2VydGlmaWNhdGU+TUlJQ21UQ0NBWUVDQmdGbFFmMXdkVEFOQmdrcWhraUc5dzBCQVFzRkFEQVFNUTR3REFZRFZRUUREQVYyY0dkeWNEQWVGdzB4T0RBNE1UWXdPVEExTlROYUZ3MHlPREE0TVRZd09UQTNNek5hTUJBeERqQU1CZ05WQkFNTUJYWndaM0p3TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFwa0tkSFhqYmd5b3dsUkFZUVBEWkxHS3AvUUo2bFMyUW9JZEJLT2F0TW9yZm90SmZQd2MrU2l3VExtdmIvUncycmNteGFpMGZydUpJeWpzbndXeUhqY0d0ajJaNFE1YlBaZmcwcUl4NWphRU5INmRLK293Y2FBQ2ZXdW1mZ0p3bEJOOFNZOVZKbW0xdlpOdDVhUnhKeXVJMXl2WWNaczRkMkVXdlA5NDQ5NFRYamhmdEdNREdJZWhSUmhHN21KS0hCNGNwT2ZMY2I0QTA0RUZoa3VaZE1JQ1ZNVGYxTGprV3orWUNZcXZKQjUxam1EUFdtMERvQ3RZOTFuRSs0b3VNZ0pyQkRxaUh5QkR3QXc5cWkzemhXSXBMczgwVGh4bGs5dEo4RGlGdWpYN3NtRExVVGp4VjZQeEtCWjFxblphV0MveFFoejdNSDcxVmtjVXZmOFh6VlFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJ0a29RaWtta0JhaTROU3E3c0pEVlU3ajhqS0NOQm5ac2lmZk9CMmZrOWYxUTYrT0w2czA3cGY3NTBjUGUyeTNUK0hnY0FzKzJqTEtWTDJ2ZHU3enp6NmhFdytLdTRnblM1Y0d5QkNLb1V3NjhmcHd5YU5pSlhHSThHdzgvOFZpMDczNU9NS2hrRFBSVVpMRmhNaElEVnRQdWxKUFVVbnlQQXUydEIvakFSOHdNNTZ5UHFWK2F0STV3NFNZMW1Cc3h2YXY4U2FtQXFYK1ZXWDdMSVQxZmxneE9IODhzRmtUcklOUklhRzlwZ3oxc1BJQk5OazZyZzN3MytIWlJ2eWpYRHduY2JMMHlncUxvUHdMYW5yTjVXSVBFL2FjTDFQYnVMdi9FS1RwRkJWWnZtNFpzNTN6aE5tTDMxM1lFRGUrVXg5MW9uanlOTjhoSFJxSjhrY3pxbjwvZHNpZzpYNTA5Q2VydGlmaWNhdGU+PC9kc2lnOlg1MDlEYXRhPjxkc2lnOktleVZhbHVlPjxkc2lnOlJTQUtleVZhbHVlPjxkc2lnOk1vZHVsdXM+cGtLZEhYamJneW93bFJBWVFQRFpMR0twL1FKNmxTMlFvSWRCS09hdE1vcmZvdEpmUHdjK1Npd1RMbXZiL1J3MnJjbXhhaTBmcnVKSXlqc253V3lIamNHdGoyWjRRNWJQWmZnMHFJeDVqYUVOSDZkSytvd2NhQUNmV3VtZmdKd2xCTjhTWTlWSm1tMXZaTnQ1YVJ4Snl1STF5dlljWnM0ZDJFV3ZQOTQ0OTRUWGpoZnRHTURHSWVoUlJoRzdtSktIQjRjcE9mTGNiNEEwNEVGaGt1WmRNSUNWTVRmMUxqa1d6K1lDWXF2SkI1MWptRFBXbTBEb0N0WTkxbkUrNG91TWdKckJEcWlIeUJEd0F3OXFpM3poV0lwTHM4MFRoeGxrOXRKOERpRnVqWDdzbURMVVRqeFY2UHhLQloxcW5aYVdDL3hRaHo3TUg3MVZrY1V2ZjhYelZRPT08L2RzaWc6TW9kdWx1cz48ZHNpZzpFeHBvbmVudD5BUUFCPC9kc2lnOkV4cG9uZW50PjwvZHNpZzpSU0FLZXlWYWx1ZT48L2RzaWc6S2V5VmFsdWU+PC9kc2lnOktleUluZm8+PC9kc2lnOlNpZ25hdHVyZT48c2FtbHA6U3RhdHVzPjxzYW1scDpTdGF0dXNDb2RlIFZhbHVlPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6c3RhdHVzOlN1Y2Nlc3MiLz48L3NhbWxwOlN0YXR1cz48c2FtbDpBc3NlcnRpb24geG1sbnM9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iIElEPSJJRF9mMDRlNjliMS1mOWUxLTQ4M2QtYmM3NC0yNDZmNGExYTc2MzkiIElzc3VlSW5zdGFudD0iMjAxOS0wOC0yMlQyMDozMDo0NC44NTZaIiBWZXJzaW9uPSIyLjAiPjxzYW1sOklzc3Vlcj5odHRwczovL3Nzb2NvLnBsYXRmb3JtLnZwZ3JwLm5ldC9hdXRoL3JlYWxtcy92cGdycDwvc2FtbDpJc3N1ZXI+PGRzaWc6U2lnbmF0dXJlIHhtbG5zOmRzaWc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkc2lnOlNpZ25lZEluZm8+PGRzaWc6Q2Fub25pY2FsaXphdGlvbk1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjxkc2lnOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48ZHNpZzpSZWZlcmVuY2UgVVJJPSIjSURfZjA0ZTY5YjEtZjllMS00ODNkLWJjNzQtMjQ2ZjRhMWE3NjM5Ij48ZHNpZzpUcmFuc2Zvcm1zPjxkc2lnOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8+PGRzaWc6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8+PC9kc2lnOlRyYW5zZm9ybXM+PGRzaWc6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8+PGRzaWc6RGlnZXN0VmFsdWU+VEFlcEZmS3ZYRElZdkJrVldMM2w0Y1pTejhXNDFydllxNWIzdWp1RHQzST08L2RzaWc6RGlnZXN0VmFsdWU+PC9kc2lnOlJlZmVyZW5jZT48L2RzaWc6U2lnbmVkSW5mbz48ZHNpZzpTaWduYXR1cmVWYWx1ZT5mQ0V4Mmw3YXN2WGtrdjNUKy9EMkt4RmUvU2UvSWRKOFJDL3ZyR2NCaVJsOFhyMlh2SGtaYi9QTW1Tb0R1UmF0TTJBSEkvU2x0d1NkSXdXbEg3ckx3Vzk0U0hhdTdYSXg1YkZocU1TU09JQ0dEU0tkRWEvSUwzSE4zbXAxNVArbXkxZWErWml4RWdjbVRQUWE0NnRXT1AxS0hrUDNoMWNCU0l6SW5mSWYwL0wzRzNLalpNdktKR3UzeStpZWNyNnoyTk5nVTlWNFZlM05CdlFoNGJZTjI0dWRTQWxxdEhpZjl5ZW9zNEc0VHFHYW5CUVhZMnp2aUVLb1U4N0krTExLaUxsYXJqbWtVdG5ncXNDZEVZSXpDWG9jSjhoeWlQdjF2QkorOE12MjcydlRUSWhCWjA0cW1TbGxqZWlsSHdSK3dFVW9PS3A0QkczcXA1K2I5bmdzeUE9PTwvZHNpZzpTaWduYXR1cmVWYWx1ZT48ZHNpZzpLZXlJbmZvPjxkc2lnOktleU5hbWU+Q049dnBncnA8L2RzaWc6S2V5TmFtZT48ZHNpZzpYNTA5RGF0YT48ZHNpZzpYNTA5Q2VydGlmaWNhdGU+TUlJQ21UQ0NBWUVDQmdGbFFmMXdkVEFOQmdrcWhraUc5dzBCQVFzRkFEQVFNUTR3REFZRFZRUUREQVYyY0dkeWNEQWVGdzB4T0RBNE1UWXdPVEExTlROYUZ3MHlPREE0TVRZd09UQTNNek5hTUJBeERqQU1CZ05WQkFNTUJYWndaM0p3TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFwa0tkSFhqYmd5b3dsUkFZUVBEWkxHS3AvUUo2bFMyUW9JZEJLT2F0TW9yZm90SmZQd2MrU2l3VExtdmIvUncycmNteGFpMGZydUpJeWpzbndXeUhqY0d0ajJaNFE1YlBaZmcwcUl4NWphRU5INmRLK293Y2FBQ2ZXdW1mZ0p3bEJOOFNZOVZKbW0xdlpOdDVhUnhKeXVJMXl2WWNaczRkMkVXdlA5NDQ5NFRYamhmdEdNREdJZWhSUmhHN21KS0hCNGNwT2ZMY2I0QTA0RUZoa3VaZE1JQ1ZNVGYxTGprV3orWUNZcXZKQjUxam1EUFdtMERvQ3RZOTFuRSs0b3VNZ0pyQkRxaUh5QkR3QXc5cWkzemhXSXBMczgwVGh4bGs5dEo4RGlGdWpYN3NtRExVVGp4VjZQeEtCWjFxblphV0MveFFoejdNSDcxVmtjVXZmOFh6VlFJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQ3dVQUE0SUJBUUJ0a29RaWtta0JhaTROU3E3c0pEVlU3ajhqS0NOQm5ac2lmZk9CMmZrOWYxUTYrT0w2czA3cGY3NTBjUGUyeTNUK0hnY0FzKzJqTEtWTDJ2ZHU3enp6NmhFdytLdTRnblM1Y0d5QkNLb1V3NjhmcHd5YU5pSlhHSThHdzgvOFZpMDczNU9NS2hrRFBSVVpMRmhNaElEVnRQdWxKUFVVbnlQQXUydEIvakFSOHdNNTZ5UHFWK2F0STV3NFNZMW1Cc3h2YXY4U2FtQXFYK1ZXWDdMSVQxZmxneE9IODhzRmtUcklOUklhRzlwZ3oxc1BJQk5OazZyZzN3MytIWlJ2eWpYRHduY2JMMHlncUxvUHdMYW5yTjVXSVBFL2FjTDFQYnVMdi9FS1RwRkJWWnZtNFpzNTN6aE5tTDMxM1lFRGUrVXg5MW9uanlOTjhoSFJxSjhrY3pxbjwvZHNpZzpYNTA5Q2VydGlmaWNhdGU+PC9kc2lnOlg1MDlEYXRhPjxkc2lnOktleVZhbHVlPjxkc2lnOlJTQUtleVZhbHVlPjxkc2lnOk1vZHVsdXM+cGtLZEhYamJneW93bFJBWVFQRFpMR0twL1FKNmxTMlFvSWRCS09hdE1vcmZvdEpmUHdjK1Npd1RMbXZiL1J3MnJjbXhhaTBmcnVKSXlqc253V3lIamNHdGoyWjRRNWJQWmZnMHFJeDVqYUVOSDZkSytvd2NhQUNmV3VtZmdKd2xCTjhTWTlWSm1tMXZaTnQ1YVJ4Snl1STF5dlljWnM0ZDJFV3ZQOTQ0OTRUWGpoZnRHTURHSWVoUlJoRzdtSktIQjRjcE9mTGNiNEEwNEVGaGt1WmRNSUNWTVRmMUxqa1d6K1lDWXF2SkI1MWptRFBXbTBEb0N0WTkxbkUrNG91TWdKckJEcWlIeUJEd0F3OXFpM3poV0lwTHM4MFRoeGxrOXRKOERpRnVqWDdzbURMVVRqeFY2UHhLQloxcW5aYVdDL3hRaHo3TUg3MVZrY1V2ZjhYelZRPT08L2RzaWc6TW9kdWx1cz48ZHNpZzpFeHBvbmVudD5BUUFCPC9kc2lnOkV4cG9uZW50PjwvZHNpZzpSU0FLZXlWYWx1ZT48L2RzaWc6S2V5VmFsdWU+PC9kc2lnOktleUluZm8+PC9kc2lnOlNpZ25hdHVyZT48c2FtbDpTdWJqZWN0PjxzYW1sOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj5lcGVycmV0PC9zYW1sOk5hbWVJRD48c2FtbDpTdWJqZWN0Q29uZmlybWF0aW9uIE1ldGhvZD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmNtOmJlYXJlciI+PHNhbWw6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgSW5SZXNwb25zZVRvPSJfZWUxYTJkNDE5MzZiNWE0YzIwMTg3MzMxM2IzNzA1ODgiIE5vdE9uT3JBZnRlcj0iMjAxOS0wOC0yMlQyMDozNTo0Mi44NTZaIiBSZWNpcGllbnQ9Imh0dHBzOi8vdnBuLnZlZXBlZS50ZWNoOjQ0My9TQU1MMjAvU1AvQUNTIi8+PC9zYW1sOlN1YmplY3RDb25maXJtYXRpb24+PC9zYW1sOlN1YmplY3Q+PHNhbWw6Q29uZGl0aW9ucyBOb3RCZWZvcmU9IjIwMTktMDgtMjJUMjA6MzA6NDIuODU2WiIgTm90T25PckFmdGVyPSIyMDE5LTA4LTIyVDIwOjMxOjQyLjg1NloiPjxzYW1sOkF1ZGllbmNlUmVzdHJpY3Rpb24+PHNhbWw6QXVkaWVuY2U+aHR0cHM6Ly92cG4udmVlcGVlLnRlY2g6NDQzL1NBTUwyMC9TUDwvc2FtbDpBdWRpZW5jZT48L3NhbWw6QXVkaWVuY2VSZXN0cmljdGlvbj48L3NhbWw6Q29uZGl0aW9ucz48c2FtbDpBdXRoblN0YXRlbWVudCBBdXRobkluc3RhbnQ9IjIwMTktMDgtMjJUMjA6MzA6NDQuODU2WiIgU2Vzc2lvbkluZGV4PSJmN2NkMmM5OC01NWEzLTQxZTAtYmQ2MC00MGI2Y2JiMmRmNzY6OjkxNzZlZDg1LTBjODYtNGY0Yi1iM2RiLWYwNGY2NGExMzg3MSI+PHNhbWw6QXV0aG5Db250ZXh0PjxzYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPnVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphYzpjbGFzc2VzOnVuc3BlY2lmaWVkPC9zYW1sOkF1dGhuQ29udGV4dENsYXNzUmVmPjwvc2FtbDpBdXRobkNvbnRleHQ+PC9zYW1sOkF1dGhuU3RhdGVtZW50PjxzYW1sOkF0dHJpYnV0ZVN0YXRlbWVudD48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJ1c2VybmFtZSIgTmFtZT0idXNlcm5hbWUiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+ZXBlcnJldDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9Imdyb3VwcyIgTmFtZT0iZ3JvdXBzIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPk9mZmljZTM2NV9FMzwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9Imdyb3VwcyIgTmFtZT0iZ3JvdXBzIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnVtYV9hdXRob3JpemF0aW9uPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZ3JvdXBzIiBOYW1lPSJncm91cHMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+U3VwcG9ydF9Db21wdGE8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJncm91cHMiIE5hbWU9Imdyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5VdGlsaXNhdGV1cnMgUEFQSUMgUFA8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJncm91cHMiIE5hbWU9Imdyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5VdGlsaXNhdGV1cnMgUEFQSUM8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJncm91cHMiIE5hbWU9Imdyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5tYW5hZ2UtYWNjb3VudDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9Imdyb3VwcyIgTmFtZT0iZ3JvdXBzIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPm1hbmFnZS1hY2NvdW50LWxpbmtzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZ3JvdXBzIiBOYW1lPSJncm91cHMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+R3N1aXRlX0J1c2luZXNzPC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZ3JvdXBzIiBOYW1lPSJncm91cHMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+VXRpbGlzYXRldXJzIFBBUElDIFI3PC9zYW1sOkF0dHJpYnV0ZVZhbHVlPjwvc2FtbDpBdHRyaWJ1dGU+PHNhbWw6QXR0cmlidXRlIEZyaWVuZGx5TmFtZT0iZ3JvdXBzIiBOYW1lPSJncm91cHMiIE5hbWVGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphdHRybmFtZS1mb3JtYXQ6YmFzaWMiPjxzYW1sOkF0dHJpYnV0ZVZhbHVlIHhtbG5zOnhzPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYSIgeG1sbnM6eHNpPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxL1hNTFNjaGVtYS1pbnN0YW5jZSIgeHNpOnR5cGU9InhzOnN0cmluZyI+YWRtaW48L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJncm91cHMiIE5hbWU9Imdyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj50ZWFtX3Jldm1ndDwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9Imdyb3VwcyIgTmFtZT0iZ3JvdXBzIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPnZpZXctcHJvZmlsZTwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjxzYW1sOkF0dHJpYnV0ZSBGcmllbmRseU5hbWU9Imdyb3VwcyIgTmFtZT0iZ3JvdXBzIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDpBdHRyaWJ1dGVWYWx1ZSB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTp0eXBlPSJ4czpzdHJpbmciPndpZmlfYWNjZXNzX3VzZXI8L3NhbWw6QXR0cmlidXRlVmFsdWU+PC9zYW1sOkF0dHJpYnV0ZT48c2FtbDpBdHRyaWJ1dGUgRnJpZW5kbHlOYW1lPSJncm91cHMiIE5hbWU9Imdyb3VwcyIgTmFtZUZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmF0dHJuYW1lLWZvcm1hdDpiYXNpYyI+PHNhbWw6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj5FeHRlcm5hbF9Vc2Vyczwvc2FtbDpBdHRyaWJ1dGVWYWx1ZT48L3NhbWw6QXR0cmlidXRlPjwvc2FtbDpBdHRyaWJ1dGVTdGF0ZW1lbnQ+PC9zYW1sOkFzc2VydGlvbj48L3NhbWxwOlJlc3BvbnNlPg==" />
        <INPUT TYPE="HIDDEN" NAME="RelayState" VALUE="HUEAANn0plxhMDU2ZjU3OTI5OGEwNDQzN2VjZmYyZjgzNzYwMzc5Yg==" />
        <NOSCRIPT>
            <P>JavaScript is disabled. We strongly recommend to enable it. Click the button below to continue.</P>
            <INPUT TYPE="SUBMIT" VALUE="CONTINUE" />
        </NOSCRIPT>
    </FORM>
</BODY>

</HTML>
b'<html><body>Login Successful!</body><!-- <saml-auth-status>1</saml-auth-status><prelogin-cookie>aQmKLyaqRGRmDsPjqj5Avk70WCoiK/I8MyxafyAXgizEwYteadDhRSAQ52dVZzTE</prelogin-cookie><saml-username>eperret</saml-username><saml-slo>no</saml-slo> --></html>'

Not too sure though, if the cookie you mentionned (portal-userauthcookie) here is prelogin-cookie, that kind of semantic looks odd to me.

I also have some session cookies:

<RequestsCookieJar[
<Cookie AUTH_SESSION_ID=2e91e21a-4540-458c-94dc-59397753d49b.ssoco-kck-fc5f1 for [another-corporate-url]/auth/realms/vpgrp>, 
<Cookie KEYCLOAK_IDENTITY=eyJhbGciOiJIUzI1NiIsImtpZCIgOiAiYmI3NTg5ZWYtN2E0ZC00NDMxLTg1ODctYjA4MTc1MzI1MGU5In0.eyJqdGkiOiI3NzRhODk1ZS03ZDg0LTRjMmQtYTBiMi00ZjcyODg4ZjlkYTciLCJleHAiOjE1Njc4MDY3MzUsIm5iZiI6MCwiaWF0IjoxNTY2NTEwNzM1LCJpc3MiOiJodHRwczovL3Nzb2NvLnBsYXRmb3JtLnZwZ3JwLm5ldC9hdXRoL3JlYWxtcy92cGdycCIsInN1YiI6IjM2MGM4YmRmLWU5OWItNDUxYy1hYjczLTNjMTU5MzNkMjRlMSIsImF1dGhfdGltZSI6MCwic2Vzc2lvbl9zdGF0ZSI6IjJlOTFlMjFhLTQ1NDAtNDU4Yy05NGRjLTU5Mzk3NzUzZDQ5YiIsInJlc291cmNlX2FjY2VzcyI6e30sInN0YXRlX2NoZWNrZXIiOiJTSFpWd1lkUDNLNjR0V0JQaC1jWXhwTGJkTkJ5c1BCVnVDeXdtc0tka0lzIn0.G-Bt4I42MHAykNZYJwjfTJ68hUEvqn4LOPTaHf9fm1s for [another-corporate-url]/auth/realms/vpgrp>,
<Cookie KEYCLOAK_SESSION=vpgrp/360c8bdf-e99b-451c-ab73-3c15933d24e1/2e91e21a-4540-458c-94dc-59397753d49b for [another-corporate-url]/auth/realms/vpgrp>, 
<Cookie PHPSESSID=b755afc7f0b82dfb2e79fb48759f1306 for vpn.[my-company-url]/>, 
<Cookie PHPSESSID=b755afc7f0b82dfb2e79fb48759f1306 for vpn.[my-company-url]/global-protect>
]>

Not sure which one is the right one, could not find any portal-userauthcookie

Btw, is there any release of the window binaries available, can't really find them (not talking about the gui, but the CLI)

natalie-o-perret commented 5 years ago

Read this, insightful: https://github.com/dlenski/openconnect/blob/globalprotect/PAN_GlobalProtect_protocol_doc.md

I sniffed and analysed the different calls from the official windows client with Fiddler to the extent could.

I managed to make it work with the script below, this if obviously a bit company specific (ie. SAML) but hope it can help someone when trying willing to use openconnect with a Global Protect VPN that requires a prior SAML auth before actually using openconnect.

import base64
import subprocess
import sys

import requests
from robobrowser import RoboBrowser
import xml.etree.ElementTree as ET

# Your data... here 
# (need to be changed accordingly to your needs)
username = "my username"
password = "my password"
smartphone_code = "the smartphone app code"

# Get my good old friend: the web scraper
browser = RoboBrowser(parser="html.parser", history=True)
vpn_portal_url = "my vpn portal"
vpn_prelogin_url = "https://{0}/global-protect/prelogin.esp".format(vpn_portal_url)

# Required, otherwise Palo Alto is not gonna be your friend with some of the requests below
browser.session.headers["User-Agent"] = "PAN GlobalProtect"

# Submit SAML Request Form
browser.open(vpn_prelogin_url)
prelogin_response_xml = ET.fromstring(browser.response.text)
saml_request = prelogin_response_xml.find(".//saml-request")
saml_request_raw = base64.b64decode(saml_request.text)
# Dirty but saves you from playing with a few parsing operations
# => just to fetch the form and submit it
saml_request_fake_response = requests.Response()
saml_request_fake_response._content = saml_request_raw
browser._update_state(saml_request_fake_response)
saml_request_form = browser.get_form("myform")
browser.submit_form(saml_request_form)

# Submit the login form (username + password)
login_form = browser.get_form("kc-form-login")
login_form["username"].value = username
login_form["password"].value = password
browser.submit_form(login_form)

# Submit the smartphone code form
code_form = browser.get_form("kc-totp-login-form")
code_form["totp"].value = smartphone_code
submit = code_form["login"]
browser.submit_form(code_form, submit=submit)

# End the SAML process gracefully
saml_response_form = browser.get_forms()[0]
browser.submit_form(saml_response_form)
prelogin_cookie = browser.response.headers["prelogin-cookie"]

# Now we can get our cookie that will act as an openconnect password
vpn_get_config_url = "https://{0}/global-protect/getconfig.esp".format(vpn_portal_url)
vpn_get_config_data = {
    "user": username,
    "prelogin-cookie": prelogin_cookie,
}
browser.open(vpn_get_config_url, method="post", data=vpn_get_config_data)
config_response_xml = ET.fromstring(browser.response.text)
portal_userauthcookie = config_response_xml.find(".//portal-userauthcookie").text

# Call openconnect with the cookie / password we got above
cmd = "echo \"{}\"".format(portal_userauthcookie)
cmd += " |"
cmd += " sudo openconnect"
cmd += " --protocol=gp"
cmd += " --usergroup portal:portal-userauthcookie"
cmd += " --user={}".format(username)
cmd += " {}".format(vpn_portal_url)

process = subprocess.Popen(cmd, shell=True, stdin=sys.stdin, stdout=sys.stdout, stderr=sys.stderr)
out, err = process.communicate()
dlenski commented 5 years ago

Bien joué. :ok_hand:

It looks like the SAML process for your GP VPN is somewhat simpler than for others which are based on SAML (like Okta).

I would like to make OpenConnect “just work” with any SAML-based login, but unfortunately there seem to be a huge number of variants, and it doesn't appear to me that the correct behavior with any given SAML implementation is in any way “discoverable” by the client… and definitely not without a full-blown browser implementing JavaScript. :face_with_head_bandage:

natalie-o-perret commented 5 years ago

@dlenski to be frank, the implementations for Okta available on GitHub seem to be overkill, I might be wrong but at first when I was trying to figure what to do I was following that track and man the code to do the SAML was huuuuge.

I am definitely not a professional Pythonista but I can see when a code has inflated a bit too much (particularly when a different kind of scrapper can be used at some point instead of doing everything with requests, some parsing part also overkilled).

About the SAML based login, yea depends on... the implementation to say the least, so that would be tough to make some generic =/

Agreed that some SAML may even play hard with some dirty JS =/ which does not help =/