]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
F5: implement f5_obtain_cookie
authorDaniel Lenski <dlenski@gmail.com>
Mon, 8 Feb 2021 20:43:07 +0000 (12:43 -0800)
committerDaniel Lenski <dlenski@gmail.com>
Mon, 29 Mar 2021 03:13:30 +0000 (20:13 -0700)
Like Fortinet, F5's authentication interface is very Javascript-heavy.
Unlike with Fortinet, I didn't even both to try to parse and "follow" HTML forms.

For now, we just create a static form (with username and password fields),
ask the user to fill it out, submit it, and attempt to detect successful login
by a length-0 response including F5_ST and MRHSession cookies.

The F5_ST cookie appears to be a reliable(?) indicator of successful login.
It's a "session timeout" cookie (https://support.f5.com/csp/article/K15387),
which looks like '1z1z1z1612808487z604800'. The 4th field appears to be the
Unix epoch timestamp of session activation, and the 5th appears to be the
duration in seconds until it expires.

Signed-off-by: Daniel Lenski <dlenski@gmail.com>
f5.c
test-f5-login.py

diff --git a/f5.c b/f5.c
index 213691c76a6e910e6fb323d963568f5d9ea6f93b..cf12ffcba2ed49c157c2ac6568f83e2fcf08470c 100644 (file)
--- a/f5.c
+++ b/f5.c
 
 int f5_obtain_cookie(struct openconnect_info *vpninfo)
 {
-       return -EINVAL;
+       int ret;
+       struct oc_text_buf *resp_buf = NULL;
+       char *form_buf = NULL;
+       struct oc_auth_form *form = NULL;
+       struct oc_form_opt *opt, *opt2;
+
+       resp_buf = buf_alloc();
+       if ((ret = buf_error(resp_buf)))
+               goto out;
+
+       /* XX: Is this initial GET / (to populate LastMRH_Session and MRHSession
+        * cookies) actually necessary?
+        */
+       ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 1);
+       free(form_buf);
+       form_buf = NULL;
+       if (ret < 0)
+               return ret;
+
+       /* XX: Is this second GET /my.policy (to update MRHSession cookie)
+        * also necessary?
+        */
+       free(vpninfo->urlpath);
+       if (!(vpninfo->urlpath = strdup("my.policy"))) {
+       nomem:
+               ret = -ENOMEM;
+               goto out;
+       }
+       ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 1);
+       free(form_buf);
+       form_buf = NULL;
+       if (ret < 0)
+               return ret;
+
+       /* XX: build static form (username and password) */
+       form = calloc(1, sizeof(*form));
+       if (!form)
+               goto nomem;
+       opt = form->opts = calloc(1, sizeof(*opt));
+       if (!opt)
+               goto nomem;
+       opt->label = strdup("Username: ");
+       opt->name = strdup("username");
+       opt->type = OC_FORM_OPT_TEXT;
+
+       opt2 = opt->next = calloc(1, sizeof(*opt2));
+       if (!opt2)
+               goto nomem;
+       opt2->label = strdup("Password: ");
+       opt2->name = strdup("password");
+       opt2->type = OC_FORM_OPT_PASSWORD;
+
+       /* XX: submit form repeatedly until success? */
+       for (;;) {
+               ret = process_auth_form(vpninfo, form);
+               if (ret == OC_FORM_RESULT_CANCELLED || ret < 0)
+                       goto out;
+
+               buf_truncate(resp_buf);
+               append_form_opts(vpninfo, form, resp_buf);
+               if ((ret = buf_error(resp_buf)))
+                       goto out;
+               do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded",
+                                resp_buf, &form_buf, 0);
+
+               /* XX: if this worked, we should have a response size of zero, and F5_ST
+                * and MRHSession cookies in the response.
+                */
+               if (!form_buf || *form_buf == '\0') {
+                       struct oc_vpn_option *cookie;
+                       const char *session=NULL, *f5_st=NULL;
+                       for (cookie = vpninfo->cookies; cookie; cookie = cookie->next) {
+                               if (!strcmp(cookie->option, "MRHSession"))
+                                       session = cookie->value;
+                               else if (!strcmp(cookie->option, "F5_ST"))
+                                       f5_st = cookie->value;
+                       }
+
+                       if (session && f5_st) {
+                               free(vpninfo->cookie);
+                               if (asprintf(&vpninfo->cookie, "MRHSession=%s; F5_ST=%s",
+                                            session, f5_st) <= 0)
+                                       goto nomem;
+                               ret = 0;
+                               goto out;
+                       }
+               }
+               if (!form->message)
+                       form->message = strdup(_("Authentication failed. Please retry."));
+               nuke_opt_values(form->opts);
+       }
+
+ out:
+       if (form) free_auth_form(form);
+       if (resp_buf) buf_free(resp_buf);
+       return ret;
 }
 
 /*
@@ -329,6 +424,7 @@ int f5_connect(struct openconnect_info *vpninfo)
 {
        int ret;
        struct oc_text_buf *reqbuf = NULL;
+       struct oc_vpn_option *cookie;
        char *profile_params = NULL;
        char *sid = NULL, *ur_z = NULL;
        int ipv4 = -1, ipv6 = -1, hdlc = -1;
@@ -345,6 +441,18 @@ int f5_connect(struct openconnect_info *vpninfo)
                        return ret;
        }
 
+       /* XX: parse "session timeout" cookie to get auth expiration */
+       for (cookie = vpninfo->cookies; cookie; cookie = cookie->next) {
+               if (!strcmp(cookie->option, "F5_ST")) {
+                       int junk, start, dur;
+                       char c = 0;
+                       if (sscanf(cookie->value, "%dz%dz%dz%dz%d%c", &junk, &junk, &junk, &start, &dur, &c) >= 5
+                           && (c == 0 || c == 'z'))
+                               vpninfo->auth_expiration = start + dur;
+                       break;
+               }
+       }
+
        free(vpninfo->urlpath);
        vpninfo->urlpath = strdup("vdesk/vpn/index.php3?outform=xml&client_version=2.0");
        ret = do_https_request(vpninfo, "GET", NULL, NULL, &res_buf, 0);
index de988aecc5a66f8628c24b95c9b21e020104ba04..aec2701e56674a64ec66227f141e40d387ffd81a 100755 (executable)
@@ -14,6 +14,7 @@ import sys
 import requests
 import argparse
 import getpass
+from datetime import datetime
 from shlex import quote
 
 p = argparse.ArgumentParser()
@@ -65,9 +66,18 @@ data=dict(username=args.username, password=args.password,
           **extra)
 print("POST /my.policy to submit login credentials...", file=stderr)
 res = s.post(endpoint._replace(path='/my.policy').geturl(), data=data, headers={'Referer': res.url})
-
 res.raise_for_status()
 
+st_cookie = next((c for c in s.cookies if c.name=='F5_ST'), None)
+if not st_cookie:
+    print("No F5_ST cookie in response (ended at %s) -- bad credentials?" % res.url, file=stderr)
+else:
+    # Parse "session timeout" cookie (https://support.f5.com/csp/article/K15387) which looks like '1z1z1z1612808487z604800'
+    fields = st_cookie.value.split('z')
+    authat, expat = datetime.fromtimestamp(int(fields[3])), datetime.fromtimestamp(int(fields[3])+int(fields[4]))
+    print("F5_ST cookie %r valid from %s - %s" % (st_cookie.value, authat, expat), file=stderr)
+
+
 # Build openconnect --cookie argument from the result:
 url = urlparse(res.url)
 if any(c.name=='MRHSession' for c in s.cookies) and url.path.startswith('/vdesk/'):