The current names were borrowed from Juniper source, and are too confusing.
The newly-renamed 'req_buf' now contains data that we send to the servers,
and 'resp_buf' contains data that the server sends back to us.
Signed-off-by: Daniel Lenski <dlenski@gmail.com>
}
int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form,
- xmlNodePtr node, const char *submit_button,
+ xmlNodePtr node, const char *submit_button, int flavor,
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"), *style = (char *)xmlGetProp(node, (unsigned char *)"style");
ret = -ENOMEM;
goto out;
}
- if (!strcmp(form->auth_id, "loginForm") &&
+ if (flavor == FORM_FLAVOR_JUNIPER &&
+ !strcmp(form->auth_id, "loginForm") &&
!strcmp(opt->name, "VerificationCode") &&
can_gen_tokencode && !can_gen_tokencode(vpninfo, form, opt))
opt->type = OC_FORM_OPT_TOKEN;
- } else if (!strcasecmp(type, "submit")) {
+ } else if (flavor == FORM_FLAVOR_JUNIPER && !strcasecmp(type, "submit")) {
+
xmlnode_get_prop(node, "name", &opt->name);
if (opt->name && submit_button && (!strcmp(opt->name, submit_button) ||
- /* XX: Juniper-specific */
!strcmp(opt->name, "sn-postauth-proceed") ||
!strcmp(opt->name, "sn-preauth-proceed") ||
!strcmp(opt->name, "secidactionEnter"))) {
}
int parse_select_node(struct openconnect_info *vpninfo, struct oc_auth_form *form,
- xmlNodePtr node)
+ xmlNodePtr node, int flavor)
{
xmlNodePtr child;
struct oc_form_opt_select *opt;
xmlnode_get_prop(node, "name", &opt->form.name);
opt->form.label = strdup(opt->form.name);
opt->form.type = OC_FORM_OPT_SELECT;
- if (!strcmp(opt->form.name, "realm")) /* XX: Juniper-specific */
+ if ((flavor == FORM_FLAVOR_JUNIPER && !strcmp(opt->form.name, "realm")) ||
+ (flavor == FORM_FLAVOR_F5 && !strcmp(opt->form.name, "domain")))
form->authgroup_opt = opt;
for (child = node->children; child; child = child->next) {
}
struct oc_auth_form *parse_form_node(struct openconnect_info *vpninfo,
- xmlNodePtr node, const char *submit_button,
+ xmlNodePtr node, const char *submit_button, int flavor,
int (*can_gen_tokencode)(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt))
{
struct oc_auth_form *form = calloc(1, sizeof(*form));
xmlnode_get_prop(node, "method", &form->method);
xmlnode_get_prop(node, "action", &form->action);
- if (!form->method || strcasecmp(form->method, "POST") ||
- !form->action || !form->action[0]) {
+ if (!form->method || strcasecmp(form->method, "POST")) {
vpn_progress(vpninfo, PRG_ERR,
_("Cannot handle form method='%s', action='%s'\n"),
form->method, form->action);
free(form);
return NULL;
}
- xmlnode_get_prop(node, "name", &form->auth_id);
- form->banner = strdup(form->auth_id);
+
+ if (flavor == FORM_FLAVOR_JUNIPER) {
+ xmlnode_get_prop(node, "name", &form->auth_id);
+ form->banner = strdup(form->auth_id);
+ } else if (flavor == FORM_FLAVOR_F5)
+ xmlnode_get_prop(node, "id", &form->auth_id);
for (child = htmlnode_dive(node, node); child && child != node; child = htmlnode_dive(node, child)) {
if (!child->name)
continue;
if (!strcasecmp((char *)child->name, "input"))
- parse_input_node(vpninfo, form, child, submit_button, can_gen_tokencode);
+ parse_input_node(vpninfo, form, child, submit_button, flavor, can_gen_tokencode);
else if (!strcasecmp((char *)child->name, "select")) {
- parse_select_node(vpninfo, form, child);
+ parse_select_node(vpninfo, form, child, flavor);
/* Skip its children */
while (child->children)
child = child->last;
- } else if (!strcasecmp((char *)child->name, "textarea")) {
+ } else if (flavor == FORM_FLAVOR_F5
+ && !strcasecmp((char *)child->name, "td")) {
+
+ char *id = (char *)xmlGetProp(child, (unsigned char *)"id");
+ if (id && !strcmp(id, "credentials_table_header")) {
+ char *msg = (char *)xmlNodeGetContent(child);
+ if (msg) {
+ free(form->banner);
+ form->banner = msg;
+ }
+ } else if (id && !strcmp(id, "credentials_table_postheader")) {
+ char *msg = (char *)xmlNodeGetContent(child);
+ if (msg) {
+ free(form->message);
+ form->message = msg;
+ }
+ }
+ free(id);
+
+ } else if (flavor == FORM_FLAVOR_JUNIPER &&
+ !strcasecmp((char *)child->name, "textarea")) {
+
/* display the post sign-in message, if any */
char *fieldname = (char *)xmlGetProp(child, (unsigned char *)"name");
if (fieldname && (!strcasecmp(fieldname, "sn-postauth-text") || /* XX: Juniper-specific */
_("Encountered form with no 'name' or 'id'\n"));
goto dump_form;
} else if (form_name && !strcmp(form_name, "frmLogin")) {
- form = parse_form_node(vpninfo, node, "btnSubmit", oncp_can_gen_tokencode);
+ form = parse_form_node(vpninfo, node, "btnSubmit", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode);
} else if (form_id && !strcmp(form_id, "loginForm")) {
- form = parse_form_node(vpninfo, node, "submitButton", oncp_can_gen_tokencode);
+ form = parse_form_node(vpninfo, node, "submitButton", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode);
} else if ((form_name && !strcmp(form_name, "frmDefender")) ||
(form_name && !strcmp(form_name, "frmNextToken"))) {
- form = parse_form_node(vpninfo, node, "btnAction", oncp_can_gen_tokencode);
+ form = parse_form_node(vpninfo, node, "btnAction", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode);
} else if (form_name && !strcmp(form_name, "frmConfirmation")) {
- form = parse_form_node(vpninfo, node, "btnContinue", oncp_can_gen_tokencode);
+ form = parse_form_node(vpninfo, node, "btnContinue", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode);
if (!form) {
ret = -EINVAL;
break;
form = parse_roles_form_node(node);
role_select = 1;
} else if (form_name && !strcmp(form_name, "frmTotpToken")) {
- form = parse_form_node(vpninfo, node, "totpactionEnter", oncp_can_gen_tokencode);
+ form = parse_form_node(vpninfo, node, "totpactionEnter", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode);
} else if ((form_name && !strcmp(form_name, "hiddenform")) ||
(form_id && !strcmp(form_id, "formSAMLSSO"))) {
- form = parse_form_node(vpninfo, node, "submit", oncp_can_gen_tokencode);
+ form = parse_form_node(vpninfo, node, "submit", FORM_FLAVOR_JUNIPER, oncp_can_gen_tokencode);
} else {
char *form_action = (char *)xmlGetProp(node, (unsigned char *)"action");
if (form_action && strstr(form_action, "remediate.cgi")) {
#include <stdarg.h>
#include <sys/types.h>
+#include <libxml/HTMLparser.h>
+#include <libxml/HTMLtree.h>
+
#include "openconnect-internal.h"
#define XCAST(x) ((const xmlChar *)(x))
+static int check_cookie_success(struct openconnect_info *vpninfo)
+{
+ struct oc_vpn_option *cookie;
+ const char *session = NULL, *f5_st = NULL;
+
+ /* XX: if login succeeded worked, we should have a response size of zero, and F5_ST
+ * and MRHSession cookies in the response.
+ */
+ 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)
+ return -ENOMEM;
+ return 0;
+ }
+ return -ENOENT;
+}
+
int f5_obtain_cookie(struct openconnect_info *vpninfo)
{
int ret;
- struct oc_text_buf *resp_buf = NULL;
- char *form_buf = NULL;
+ xmlDocPtr doc = NULL;
+ xmlNode *node;
+ struct oc_text_buf *req_buf = NULL;
struct oc_auth_form *form = NULL;
- struct oc_form_opt *opt, *opt2;
+ char *form_id = NULL;
- resp_buf = buf_alloc();
- if ((ret = buf_error(resp_buf)))
+ req_buf = buf_alloc();
+ if ((ret = buf_error(req_buf)))
goto out;
- /* XX: This initial 'GET /' seems to be necessary to populate LastMRH_Session and
- * MRHSession cookies, without which the subsequent 'POST' will fail.
- */
- ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 1);
- free(form_buf);
- form_buf = NULL;
- if (ret < 0)
- return ret;
+ while (1) {
+ char *resp_buf = NULL;
+ char *url;
- /* XX: build static form (username and password) */
- /* TODO: parse out 'domain' form field from /my.policy as authgroup */
- form = calloc(1, sizeof(*form));
- if (!form) {
- nomem:
- ret = -ENOMEM;
- goto out;
- }
- 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;
+ if (req_buf && req_buf->pos)
+ ret = do_https_request(vpninfo, "POST",
+ "application/x-www-form-urlencoded",
+ req_buf, &resp_buf, 2);
+ else
+ ret = do_https_request(vpninfo, "GET", NULL, NULL,
+ &resp_buf, 2);
- buf_truncate(resp_buf);
- append_form_opts(vpninfo, form, resp_buf);
- if (vpninfo->authgroup)
- append_opt(resp_buf, "domain", vpninfo->authgroup);
+ if (ret < 0)
+ break;
- if ((ret = buf_error(resp_buf)))
- goto out;
- do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded",
- resp_buf, &form_buf, 0);
+ if (!check_cookie_success(vpninfo)) {
+ free(resp_buf);
+ ret = 0;
+ break;
+ }
- /* 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;
- }
+ url = internal_get_url(vpninfo);
+ if (!url) {
+ free(resp_buf);
+ nomem:
+ ret = -ENOMEM;
+ break;
+ }
- 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;
+ doc = htmlReadMemory(resp_buf, ret, url, NULL,
+ HTML_PARSE_RECOVER|HTML_PARSE_NOERROR|HTML_PARSE_NOWARNING|HTML_PARSE_NONET);
+ free(url);
+ free(resp_buf);
+ if (!doc) {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Failed to parse HTML document\n"));
+ ret = -EINVAL;
+ break;
+ }
+
+ buf_truncate(req_buf);
+
+ node = find_form_node(doc);
+ if (!node) {
+ /* XX: some F5 VPNs simply do not have a static HTML form to parse */
+ struct oc_form_opt *opt, *opt2;
+
+ vpn_progress(vpninfo, PRG_ERR,
+ _("WARNING: no HTML login form found; assuming username and password fields\n"));
+
+ 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;
+
+ } else {
+ if (!xmlnode_get_prop(node, "id", &form_id) && !strcmp(form_id, "auth_form"))
+ form = parse_form_node(vpninfo, node, NULL, FORM_FLAVOR_F5, NULL);
+ else {
+ vpn_progress(vpninfo, PRG_ERR, _("Unknown form ID '%s' (expected 'auth_form')\n"),
+ form_id);
+
+ fprintf(stderr, _("Dumping unknown HTML form:\n"));
+ htmlNodeDumpFileFormat(stderr, node->doc, node, NULL, 1);
+ ret = -EINVAL;
+ break;
}
}
- if (!form->message)
- form->message = strdup(_("Authentication failed. Please retry."));
- nuke_opt_values(form->opts);
+
+ if (!form) {
+ ret = -EINVAL;
+ break;
+ }
+
+ /* XX: do_gen_tokencode would go here, if we knew of any
+ * token-based 2FA options for F5.
+ */
+
+ do {
+ ret = process_auth_form(vpninfo, form);
+ } while (ret == OC_FORM_RESULT_NEWGROUP);
+ if (ret)
+ goto out;
+
+ append_form_opts(vpninfo, form, req_buf);
+ if ((ret = buf_error(req_buf)))
+ goto out;
+
+ if (form->action) {
+ vpninfo->redirect_url = form->action;
+ form->action = NULL;
+ }
+ free_auth_form(form);
+ form = NULL;
+ if (vpninfo->redirect_url)
+ handle_redirect(vpninfo);
+
+ xmlFreeDoc(doc);
+ doc = NULL;
}
out:
+ if (doc)
+ xmlFreeDoc(doc);
+ free(form_id);
if (form) free_auth_form(form);
- if (resp_buf) buf_free(resp_buf);
+ if (req_buf) buf_free(req_buf);
return ret;
}
int fortinet_obtain_cookie(struct openconnect_info *vpninfo)
{
int ret;
- struct oc_text_buf *resp_buf = NULL;
+ struct oc_text_buf *req_buf = NULL;
struct oc_auth_form *form = NULL;
struct oc_form_opt *opt, *opt2;
- char *form_buf = NULL, *realm = NULL;
+ char *resp_buf = NULL, *realm = NULL;
- resp_buf = buf_alloc();
- if (buf_error(resp_buf)) {
- ret = buf_error(resp_buf);
+ req_buf = buf_alloc();
+ if (buf_error(req_buf)) {
+ ret = buf_error(req_buf);
goto out;
}
- ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 1);
- free(form_buf);
- form_buf = NULL;
+ ret = do_https_request(vpninfo, "GET", NULL, NULL, &resp_buf, 1);
+ free(resp_buf);
+ resp_buf = NULL;
if (ret < 0)
goto out;
goto out;
}
- buf_truncate(resp_buf);
- append_form_opts(vpninfo, form, resp_buf);
- buf_append(resp_buf, "&realm=%s", realm ?: ""); /* XX: already URL-escaped */
+ buf_truncate(req_buf);
+ append_form_opts(vpninfo, form, req_buf);
+ buf_append(req_buf, "&realm=%s", realm ?: ""); /* XX: already URL-escaped */
if (!form->action) {
/* "normal" form (fields 'username', 'credential') */
- buf_append(resp_buf, "&ajax=1&just_logged_in=1");
+ buf_append(req_buf, "&ajax=1&just_logged_in=1");
} else {
/* 2FA form (fields 'username', 'code', and a bunch of values
* from the previous response which we mindlessly parrot back)
*/
- buf_append(resp_buf, "&code2=&%s", form->action);
+ buf_append(req_buf, "&code2=&%s", form->action);
}
- if ((ret = buf_error(resp_buf)))
+ if ((ret = buf_error(req_buf)))
goto out;
ret = do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded",
- resp_buf, &form_buf, 0);
+ req_buf, &resp_buf, 0);
/* XX: if this worked, we should have 200 status */
if (ret >= 0) {
}
/* XX: We didn't get SVPNCOOKIE. 2FA? */
- if (!strncmp(form_buf, "ret=", 4) && strstr(form_buf, ",tokeninfo=")) {
+ if (!strncmp(resp_buf, "ret=", 4) && strstr(resp_buf, ",tokeninfo=")) {
const char *prompt;
struct oc_text_buf *action_buf = buf_alloc();
opt2->type = OC_FORM_OPT_PASSWORD;
/* Save a bunch of values to parrot back */
- filter_opts(action_buf, form_buf, "reqid,polid,grp,portal,peer,magic", 1);
+ filter_opts(action_buf, resp_buf, "reqid,polid,grp,portal,peer,magic", 1);
if ((ret = buf_error(action_buf)))
goto out;
free(form->action);
action_buf->data = NULL;
buf_free(action_buf);
- if ((prompt = strstr(form_buf, ",chal_msg="))) {
+ if ((prompt = strstr(resp_buf, ",chal_msg="))) {
char *end = strchrnul(prompt, ',');
prompt += 10;
free(form->message);
out:
free(realm);
- free(form_buf);
+ free(resp_buf);
if (form)
free_auth_form(form);
- buf_free(resp_buf);
+ buf_free(req_buf);
return ret;
}
#define DTLS_CONNECTING 4 /* ESP probe received; must tell server */
#define DTLS_CONNECTED 5 /* Server informed and should be sending ESP */
+/* Flavors of HTML forms to screen-scrape */
+#define FORM_FLAVOR_JUNIPER 1
+#define FORM_FLAVOR_F5 2
+#define FORM_FLAVOR_FORTINET 3
+
/* All supported PPP packet framings/encapsulations */
#define PPP_ENCAP_RFC1661 1 /* Plain/synchronous/pre-framed PPP (RFC1661) */
#define PPP_ENCAP_RFC1662_HDLC 2 /* PPP with HDLC-like framing (RFC1662) */
xmlNodePtr htmlnode_dive(xmlNodePtr top, xmlNodePtr node);
xmlNodePtr find_form_node(xmlDocPtr doc);
int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form,
- xmlNodePtr node, const char *submit_button,
+ xmlNodePtr node, const char *submit_button, int flavor,
int (*can_gen_tokencode)(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt));
int parse_select_node(struct openconnect_info *vpninfo, struct oc_auth_form *form,
- xmlNodePtr node);
+ xmlNodePtr node, int flavor);
struct oc_auth_form *parse_form_node(struct openconnect_info *vpninfo,
- xmlNodePtr node, const char *submit_button,
+ xmlNodePtr node, const char *submit_button, int flavor,
int (*can_gen_tokencode)(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt));
/* auth-juniper.c */
return redirect(url_for('get_policy'))
-# Respond to 'GET /my.policy with a placeholder stub (since OpenConnect doesn't even try to parse the form)
+# Respond to 'GET /my.policy with a login form
@app.route('/my.policy')
def get_policy():
session.update(step='GET-login-form')
- return 'login page'
+ return '''
+<html><body><form id="auth_form" method="post">
+<input type="text" name="username"/>
+<input type="password" name="password"/>
+</form></body></html>'''
-# Respond to 'POST /my.policy with an empty response containing MRHSession and F5_ST
+# Respond to 'POST /my.policy with a redirect response containing MRHSession and F5_ST
# cookies (OpenConnect uses the combination of the two to detect successful authentication)
@app.route('/my.policy', methods=['POST'])
def post_policy():
session.update(step='POST-login', username=request.form.get('username'), credential=request.form.get('password'))
# print(session)
- resp = make_response('')
+ resp = redirect(url_for('webtop'))
resp.set_cookie('MRHSession', cookify(dict(session)))
resp.set_cookie('F5_ST', '1z1z1z%dz%d' % (time.time(), 3600))
return resp
+@app.route('/vdesk/webtop.eui')
+def webtop():
+ session.update(step='POST-login-webtop')
+ # print(session)
+
+ return 'some junk HTML webtop'
+
+
# Respond to 'GET /vdesk/vpn/index.php3?outform=xml&client_version=2.0 with an XML config
# [Save VPN resource name in the session for verification of client state later]
@app.route('/vdesk/vpn/index.php3')