]> www.infradead.org Git - users/dwmw2/openconnect.git/commitdiff
implement fortinet_obtain_cookie
authorDaniel Lenski <dlenski@gmail.com>
Mon, 8 Feb 2021 01:49:10 +0000 (17:49 -0800)
committerDaniel Lenski <dlenski@gmail.com>
Mon, 29 Mar 2021 03:13:31 +0000 (20:13 -0700)
Fortinet uses HTML-based login forms, similar to Juniper's, resulting in an SVPNCOOKIE.

Believe it or not, they're even more badly designed than Juniper's forms:

1. Inconsistent use of HTTP redirects ('Location' header) vs. Javascript-only redirects
2. Significant <input> fields that aren't actually nested within the <form> tag.
3. Misleading HTTP 405 status ("Method Not Allowed") after incorrect credentials are entered.
4. No apparent way to determine the allowed values for the realm field.
   (Though various articles floating around suggest it comes from the URL-path entry point,
   from which I assume it's triggered by JavaScript.)

Signed-off-by: Daniel Lenski <dlenski@gmail.com>
auth-html.c
fortinet.c
http.c

index 404dcf2abb50b737c0af27254887372bab2bcef7..5b7bbcc4081d27cd90011e9c243a5aed43c8954e 100644 (file)
@@ -57,10 +57,11 @@ int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form
                     xmlNodePtr node, const char *submit_button,
                     int (*can_gen_tokencode)(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt))
 {
-       char *type = (char *)xmlGetProp(node, (unsigned char *)"type");
+       char *type = (char *)xmlGetProp(node, (unsigned char *)"type"), *style = (char *)xmlGetProp(node, (unsigned char *)"style");
        struct oc_form_opt **p = &form->opts;
        struct oc_form_opt *opt;
        int ret = 0;
+       int nodisplay = style && !strcmp(style, "display: none;"); /* XX: Fortinet-specific */
 
        if (!type)
                return -EINVAL;
@@ -71,7 +72,7 @@ int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form
                goto out;
        }
 
-       if (!strcasecmp(type, "hidden")) {
+       if (nodisplay || !strcasecmp(type, "hidden")) {
                opt->type = OC_FORM_OPT_HIDDEN;
                xmlnode_get_prop(node, "name", &opt->name);
                xmlnode_get_prop(node, "value", &opt->_value);
@@ -142,6 +143,7 @@ int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form
        if (ret)
                free_opt(opt);
        free(type);
+       free(style);
        return ret;
 }
 
index 0703cc9782bc86c7e02dc5d3962e9a4cb9b0ebc0..8b9dd9fe4592cffde2fc01a48b70c453a4b7ffa8 100644 (file)
 #include <stdarg.h>
 #include <sys/types.h>
 
-#include "openconnect-internal.h"
+#include <libxml/HTMLparser.h>
+#include <libxml/HTMLtree.h>
 
-#define XCAST(x) ((const xmlChar *)(x))
+#include "openconnect-internal.h"
 
 void fortinet_common_headers(struct openconnect_info *vpninfo,
                         struct oc_text_buf *buf)
@@ -60,7 +61,142 @@ void fortinet_common_headers(struct openconnect_info *vpninfo,
 
 int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
 {
-       return -EINVAL;
+       int ret;
+       struct oc_text_buf *resp_buf = NULL;
+       xmlDocPtr doc = NULL;
+       xmlNodePtr node;
+       struct oc_auth_form *form = NULL;
+       struct oc_vpn_option *cookie;
+       char *form_name = NULL;
+
+       resp_buf = buf_alloc();
+       if (buf_error(resp_buf)) {
+               ret = buf_error(resp_buf);
+               goto out;
+       }
+
+       /* XX: Fortinet HTML forms *seem* like they should be about as easy to follow
+        * as Juniper HTML forms, but some redirects use Javascript EXCLUSIVELY (no
+        * 'Location' header).
+        *
+        * Also, a failed login returns the misleading HTTP status "405 Method Not Allowed",
+        * rather than 403/401.
+        */
+       while (1) {
+               char *form_buf = NULL;
+               char *url;
+
+               if (resp_buf && resp_buf->pos)
+                       ret = do_https_request(vpninfo, "POST",
+                                              "application/x-www-form-urlencoded",
+                                              resp_buf, &form_buf, 1);
+               else
+                       ret = do_https_request(vpninfo, "GET", NULL, NULL,
+                                              &form_buf, 1);
+
+               /* XX: special-cased, because most Fortinet servers return a Javascript-only redirect from
+                * a status 405 page, after a failed login.
+                */
+               if (ret == -EACCES) {
+                       free(form_buf);
+                       buf_truncate(resp_buf);
+                       goto try_remote_login;
+               }
+
+               for (cookie = vpninfo->cookies; cookie; cookie = cookie->next) {
+                       if (!strcmp(cookie->option, "SVPNCOOKIE")) {
+                               vpninfo->cookie = strdup(cookie->value);
+                               free(form_buf);
+                               ret = 0;
+                               goto out;
+                       }
+               }
+
+               url = internal_get_url(vpninfo);
+               if (!url) {
+                       free(form_buf);
+                       ret = -ENOMEM;
+                       break;
+               }
+
+               doc = htmlReadMemory(form_buf, ret, url, NULL,
+                                    HTML_PARSE_RECOVER|HTML_PARSE_NOERROR|HTML_PARSE_NOWARNING|HTML_PARSE_NONET);
+               free(url);
+               free(form_buf);
+               if (!doc) {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Failed to parse HTML document\n"));
+                       ret = -EINVAL;
+                       break;
+               }
+
+               buf_truncate(resp_buf);
+
+               node = find_form_node(doc);
+               if (!node) {
+                       /* XX: special-cased, because most Fortinet servers return a Javascript-only redirect from 'GET /' */
+                       if (!vpninfo->urlpath) {
+                       try_remote_login:
+                               vpninfo->urlpath = strdup("remote/login");
+                               continue;
+                       }
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Failed to find or parse web form in login page\n"));
+                       ret = -EINVAL;
+                       break;
+               }
+               free(form_name);
+               form_name = (char *)xmlGetProp(node, (unsigned char *)"name");
+               if (form_name && !strcmp(form_name, "f")) {
+                       form = parse_form_node(vpninfo, node, NULL, NULL);
+               } else {
+                       vpn_progress(vpninfo, PRG_ERR,
+                                    _("Unknown form name '%s'\n"),
+                                    form_name);
+
+                       fprintf(stderr, _("Dumping unknown HTML form:\n"));
+                       htmlNodeDumpFileFormat(stderr, node->doc, node, NULL, 1);
+                       ret = -EINVAL;
+                       break;
+               }
+
+               if (!form) {
+                       ret = -EINVAL;
+                       break;
+               }
+
+               /* XX: Fortinet servers put extra <input type='hidden'> fields after the </form>.
+                * Sometimes they're even after the </body>. Great job, Fortinet.
+                */
+               for (node = htmlnode_next(NULL, node); node; node = htmlnode_dive(NULL, node))
+                       if (node->name && !strcasecmp((char *)node->name, "input"))
+                               parse_input_node(vpninfo, form, node, NULL, NULL);
+
+               ret = process_auth_form(vpninfo, form);
+               if (ret)
+                       break;
+
+               append_form_opts(vpninfo, form, resp_buf);
+               ret = buf_error(resp_buf);
+               if (ret)
+                       break;
+
+               vpninfo->redirect_url = form->action;
+               form->action = NULL;
+               free_auth_form(form);
+               form = NULL;
+               handle_redirect(vpninfo);
+               xmlFreeDoc(doc);
+               doc = NULL;
+       }
+ out:
+       if (doc)
+               xmlFreeDoc(doc);
+       free(form_name);
+       if (form)
+               free_auth_form(form);
+       buf_free(resp_buf);
+       return ret;
 }
 
 /* We behave like CSTP — create a linked list in vpninfo->cstp_options
diff --git a/http.c b/http.c
index 333d4028e8d5e95f9a6f4cf736ad01e830fae3fe..53dbeefd9fd45b641ea1d48ecac7a7046623abe4 100644 (file)
--- a/http.c
+++ b/http.c
@@ -1205,6 +1205,8 @@ int do_https_request(struct openconnect_info *vpninfo, const char *method,
                             result);
                if (result == 401 || result == 403)
                        result = -EPERM;
+               else if (result == 405) /* Fortinet invalid username/password "Method Not Allowed" */
+                       result = -EACCES;
                else if (result == 512) /* GlobalProtect invalid username/password */
                        result = -EACCES;
                else