/* * OpenConnect (SSL + DTLS) VPN client * * Copyright © 2008-2015 Intel Corporation. * * Author: David Woodhouse * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public License * version 2.1, as published by the Free Software Foundation. * * This program 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. */ /* * Grateful thanks to Tiebing Zhang, who did much of the hard work * of analysing and decoding the protocol. */ #include #include #include #include #include #include #include #include #include #include #include #include #ifndef _WIN32 #include #endif #include #include #include "openconnect-internal.h" /* XX: This is actually a lot of duplication with the CSTP version. */ void oncp_common_headers(struct openconnect_info *vpninfo, struct oc_text_buf *buf) { http_common_headers(vpninfo, buf); // buf_append(buf, "Content-Length: 256\r\n"); buf_append(buf, "NCP-Version: 3\r\n"); // buf_append(buf, "Accept-Encoding: gzip\r\n"); } static xmlNodePtr htmlnode_next(xmlNodePtr top, xmlNodePtr node) { if (node->children) return node->children; while (!node->next) { node = node->parent; if (!node || node == top) return NULL; } return node->next; } static int oncp_can_gen_tokencode(struct openconnect_info *vpninfo, struct oc_auth_form *form, struct oc_form_opt *opt) { if (vpninfo->token_mode == OC_TOKEN_MODE_NONE || vpninfo->token_bypassed) return -EINVAL; if (strcmp(form->auth_id, "frmDefender") && strcmp(form->auth_id, "frmNextToken") && strcmp(form->auth_id, "frmTotpToken")) return -EINVAL; return can_gen_tokencode(vpninfo, form, opt); } static int parse_input_node(struct openconnect_info *vpninfo, struct oc_auth_form *form, xmlNodePtr node, const char *submit_button) { char *type = (char *)xmlGetProp(node, (unsigned char *)"type"); struct oc_form_opt **p = &form->opts; struct oc_form_opt *opt; int ret = 0; if (!type) return -EINVAL; opt = calloc(1, sizeof(*opt)); if (!opt) { ret = -ENOMEM; goto out; } if (!strcasecmp(type, "hidden")) { opt->type = OC_FORM_OPT_HIDDEN; xmlnode_get_prop(node, "name", &opt->name); xmlnode_get_prop(node, "value", &opt->_value); /* XXX: Handle tz_offset / tz */ } else if (!strcasecmp(type, "password")) { opt->type = OC_FORM_OPT_PASSWORD; xmlnode_get_prop(node, "name", &opt->name); if (asprintf(&opt->label, "%s:", opt->name) == -1) { ret = -ENOMEM; goto out; } if (!oncp_can_gen_tokencode(vpninfo, form, opt)) opt->type = OC_FORM_OPT_TOKEN; } else if (!strcasecmp(type, "text")) { opt->type = OC_FORM_OPT_TEXT; xmlnode_get_prop(node, "name", &opt->name); if (asprintf(&opt->label, "%s:", opt->name) == -1) { ret = -ENOMEM; goto out; } } else if (!strcasecmp(type, "username")) { opt->type = OC_FORM_OPT_TEXT; xmlnode_get_prop(node, "name", &opt->name); if (asprintf(&opt->label, "%s:", opt->name) == -1) { ret = -ENOMEM; goto out; } } else if (!strcasecmp(type, "submit")) { xmlnode_get_prop(node, "name", &opt->name); if (opt->name && (!strcmp(opt->name, submit_button) || !strcmp(opt->name, "sn-postauth-proceed") || !strcmp(opt->name, "sn-preauth-proceed"))) { /* Use this as the 'Submit' action for the form, by implicitly adding it as a hidden option. */ xmlnode_get_prop(node, "value", &opt->_value); opt->type = OC_FORM_OPT_HIDDEN; } else { vpn_progress(vpninfo, PRG_DEBUG, _("Ignoring unknown form submit item '%s'\n"), opt->name); ret = -EINVAL; goto out; } } else if (!strcasecmp(type, "checkbox")) { opt->type = OC_FORM_OPT_HIDDEN; xmlnode_get_prop(node, "name", &opt->name); xmlnode_get_prop(node, "value", &opt->_value); } else { vpn_progress(vpninfo, PRG_DEBUG, _("Ignoring unknown form input type '%s'\n"), type); ret = -EINVAL; goto out; } /* Append to the existing list */ while (*p) { if (!strcmp((*p)->name, opt->name)) { vpn_progress(vpninfo, PRG_DEBUG, _("Discarding duplicate option '%s'\n"), opt->name); goto out; } p = &(*p)->next; } *p = opt; out: if (ret) free_opt(opt); free(type); return ret; } static int parse_select_node(struct openconnect_info *vpninfo, struct oc_auth_form *form, xmlNodePtr node) { xmlNodePtr child; struct oc_form_opt_select *opt; struct oc_choice *choice; opt = calloc(1, sizeof(*opt)); if (!opt) return -ENOMEM; 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")) form->authgroup_opt = opt; for (child = node->children; child; child = child->next) { struct oc_choice **new_choices; if (!child->name || strcasecmp((const char *)child->name, "option")) continue; choice = calloc(1, sizeof(*choice)); if (!choice) return -ENOMEM; xmlnode_get_prop(node, "name", &choice->name); choice->label = (char *)xmlNodeGetContent(child); choice->name = strdup(choice->label); new_choices = realloc(opt->choices, sizeof(opt->choices[0]) * (opt->nr_choices+1)); if (!new_choices) { free_opt((void *)opt); free(choice); return -ENOMEM; } opt->choices = new_choices; opt->choices[opt->nr_choices++] = choice; } /* Prepend to the existing list */ opt->form.next = form->opts; form->opts = &opt->form; return 0; } static struct oc_auth_form *parse_form_node(struct openconnect_info *vpninfo, xmlNodePtr node, const char *submit_button) { struct oc_auth_form *form = calloc(1, sizeof(*form)); xmlNodePtr child; if (!form) return NULL; 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]) { 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); for (child = htmlnode_next(node, node); child && child != node; child = htmlnode_next(node, child)) { if (!child->name) continue; if (!strcasecmp((char *)child->name, "input")) parse_input_node(vpninfo, form, child, submit_button); else if (!strcasecmp((char *)child->name, "select")) { parse_select_node(vpninfo, form, child); /* Skip its children */ while (child->children) child = child->last; } else if (!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") || !strcasecmp(fieldname, "sn-preauth-text"))) { char *postauth_msg = (char *)xmlNodeGetContent(child); if (postauth_msg) { free(form->banner); form->banner = postauth_msg; } } else { vpn_progress(vpninfo, PRG_ERR, _("Unknown textarea field: '%s'\n"), fieldname); } free(fieldname); } } return form; } static xmlNodePtr find_form_node(xmlDocPtr doc) { xmlNodePtr root, node; for (root = node = xmlDocGetRootElement(doc); node; node = htmlnode_next(root, node)) { if (node->name && !strcasecmp((char *)node->name, "form")) return node; } return NULL; } static int check_cookie_success(struct openconnect_info *vpninfo) { const char *dslast = NULL, *dsfirst = NULL, *dsurl = NULL, *dsid = NULL, *dspreauth = NULL; struct oc_vpn_option *cookie; struct oc_text_buf *buf; for (cookie = vpninfo->cookies; cookie; cookie = cookie->next) { if (!strcmp(cookie->option, "DSFirstAccess")) dsfirst = cookie->value; else if (!strcmp(cookie->option, "DSLastAccess")) dslast = cookie->value; else if (!strcmp(cookie->option, "DSID")) dsid = cookie->value; else if (!strcmp(cookie->option, "DSSignInUrl")) dsurl = cookie->value; else if (!strcmp(cookie->option, "DSPREAUTH")) dspreauth = cookie->value; } if (!dsid) return -ENOENT; buf = buf_alloc(); if (vpninfo->tncc_fd != -1) { buf_append(buf, "setcookie\n"); buf_append(buf, "Cookie=%s\n", dspreauth); if (buf_error(buf)) return buf_free(buf); send(vpninfo->tncc_fd, buf->data, buf->pos, 0); buf_truncate(buf); } /* XXX: Do these need escaping? Could they theoreetically have semicolons in? */ buf_append(buf, "DSID=%s", dsid); if (dsfirst) buf_append(buf, "; DSFirst=%s", dsfirst); if (dslast) buf_append(buf, "; DSLast=%s", dslast); if (dsurl) buf_append(buf, "; DSSignInUrl=%s", dsurl); if (buf_error(buf)) return buf_free(buf); free(vpninfo->cookie); vpninfo->cookie = buf->data; buf->data = NULL; buf_free(buf); return 0; } #ifdef _WIN32 static int tncc_preauth(struct openconnect_info *vpninfo) { vpn_progress(vpninfo, PRG_ERR, _("TNCC support not implemented yet on Windows\n")); return -EOPNOTSUPP; } #else static int tncc_preauth(struct openconnect_info *vpninfo) { int sockfd[2]; pid_t pid; struct oc_text_buf *buf; struct oc_vpn_option *cookie; const char *dspreauth = NULL, *dssignin = "null"; char recvbuf[1024]; int len; for (cookie = vpninfo->cookies; cookie; cookie = cookie->next) { if (!strcmp(cookie->option, "DSPREAUTH")) dspreauth = cookie->value; else if (!strcmp(cookie->option, "DSSIGNIN")) dssignin = cookie->value; } if (!dspreauth) { vpn_progress(vpninfo, PRG_ERR, _("No DSPREAUTH cookie; not attempting TNCC\n")); return -EINVAL; } #ifdef SOCK_CLOEXEC if (socketpair(AF_UNIX, SOCK_STREAM | SOCK_CLOEXEC, 0, sockfd)) #endif { if (socketpair(AF_UNIX, SOCK_STREAM, 0, sockfd)) return -errno; set_fd_cloexec(sockfd[0]); set_fd_cloexec(sockfd[1]); } pid = fork(); if (pid == -1) { close(sockfd[0]); close(sockfd[1]); return -errno; } if (!pid) { int i; /* Fork again to detach grandchild */ if (fork()) exit(1); close(sockfd[1]); /* The duplicated fd does not have O_CLOEXEC */ dup2(sockfd[0], 0); /* We really don't want anything going to our stdout. Redirect the child's stdout, to our stderr. */ dup2(2, 1); /* And close everything else.*/ for (i = 3; i < 1024 ; i++) close(i); execl(vpninfo->csd_wrapper, vpninfo->csd_wrapper, vpninfo->hostname, NULL); fprintf(stderr, _("Failed to exec TNCC script %s: %s\n"), vpninfo->csd_wrapper, strerror(errno)); exit(1); } waitpid(pid, NULL, 0); close(sockfd[0]); buf = buf_alloc(); buf_append(buf, "start\n"); buf_append(buf, "IC=%s\n", vpninfo->hostname); buf_append(buf, "Cookie=%s\n", dspreauth); buf_append(buf, "DSSIGNIN=%s\n", dssignin); if (buf_error(buf)) { vpn_progress(vpninfo, PRG_ERR, _("Failed to allocate memory for communication with TNCC\n")); close(sockfd[1]); return buf_free(buf); } if (cancellable_send(vpninfo, sockfd[1], buf->data, buf->pos) != buf->pos) { vpn_progress(vpninfo, PRG_ERR, _("Failed to send start command to TNCC\n")); buf_free(buf); close(sockfd[1]); return -EIO; } buf_free(buf); vpn_progress(vpninfo, PRG_DEBUG, _("Sent start; waiting for response from TNCC\n")); /* First line: HTTP-like response code. */ len = cancellable_gets(vpninfo, sockfd[1], recvbuf, sizeof(recvbuf)); if (len < 0) { respfail: vpn_progress(vpninfo, PRG_ERR, _("Failed to read response from TNCC\n")); close(sockfd[1]); return -EIO; } if (strcmp(recvbuf, "200")) { vpn_progress(vpninfo, PRG_ERR, _("Received unsuccessful %s response from TNCC\n"), recvbuf); close(sockfd[1]); return -EINVAL; } vpn_progress(vpninfo, PRG_TRACE, _("TNCC response 200 OK\n")); /* We're not sure what the second line is. We ignore it. */ len = cancellable_gets(vpninfo, sockfd[1], recvbuf, sizeof(recvbuf)); if (len < 0) goto respfail; vpn_progress(vpninfo, PRG_TRACE, _("Second line of TNCC response: '%s'\n"), recvbuf); /* Third line is the DSPREAUTH cookie */ len = cancellable_gets(vpninfo, sockfd[1], recvbuf, sizeof(recvbuf)); if (len < 0) goto respfail; vpn_progress(vpninfo, PRG_DEBUG, _("Got new DSPREAUTH cookie from TNCC: %s\n"), recvbuf); http_add_cookie(vpninfo, "DSPREAUTH", recvbuf, 1); vpninfo->tncc_fd = sockfd[1]; len = cancellable_gets(vpninfo, sockfd[1], recvbuf, sizeof(recvbuf)); if (len < 0) goto respfail; if (len > 0) { vpn_progress(vpninfo, PRG_ERR, _("Unexpected non-empty line from TNCC after DSPREAUTH cookie: '%s'\n"), recvbuf); goto respfail; } return 0; } #endif static struct oc_auth_form *parse_roles_table_node(xmlNodePtr node) { struct oc_auth_form *form; xmlNodePtr table_itr; xmlNodePtr row_itr; xmlNodePtr data_itr; struct oc_form_opt_select *opt; struct oc_choice *choice; form = calloc(1, sizeof(*form)); if (!form) return NULL; opt = calloc(1, sizeof(*opt)); if (!opt) { free(form); return NULL; } form->opts = &opt->form; opt->form.label = strdup("frmSelectRoles"); opt->form.name = strdup("frmSelectRoles"); opt->form.type = OC_FORM_OPT_SELECT; for (table_itr = node->children; table_itr; table_itr = table_itr->next) { if (!table_itr->name || strcasecmp((const char *)table_itr->name, "tr")) continue; for (row_itr = table_itr->children; row_itr; row_itr = row_itr->next) { if (!row_itr->name || strcasecmp((const char *)row_itr->name, "td")) continue; for (data_itr = row_itr->children; data_itr; data_itr = data_itr->next) { struct oc_choice **new_choices; char *role_link = NULL; char *role_name = NULL; if (!data_itr->name || strcasecmp((const char *)data_itr->name, "a")) continue; // Discovered tag with role selection. role_link = (char *)xmlGetProp(data_itr, (unsigned char *)"href"); if (!role_link) continue; role_name = (char *)xmlNodeGetContent(data_itr); if (!role_name) { // some weird case? free(role_link); continue; } choice = calloc(1, sizeof(*choice)); if (!choice) { free(role_name); free(role_link); free_auth_form(form); return NULL; } choice->label = role_name; choice->name = role_link; new_choices = realloc(opt->choices, sizeof(opt->choices[0]) * (opt->nr_choices+1)); if (!new_choices) { free(choice); free(role_name); free(role_link); free_auth_form(form); return NULL; } opt->choices = new_choices; opt->choices[opt->nr_choices++] = choice; } } } return form; } static struct oc_auth_form *parse_roles_form_node(xmlNodePtr node) { struct oc_auth_form *form = NULL; xmlNodePtr child; // Set form->action here as a redirect url with keys and ids. for (child = htmlnode_next(node, node); child && child != node; child = htmlnode_next(node, child)) { if (child->name && !strcasecmp((char *)child->name, "table")) { char *table_id = (char *)xmlGetProp(child, (unsigned char *)"id"); if (table_id) { if (!strcmp(table_id, "TABLE_SelectRole_1")) form = parse_roles_table_node(child); free(table_id); if (form) break; } } } return form; } int oncp_obtain_cookie(struct openconnect_info *vpninfo) { int ret; struct oc_text_buf *resp_buf = NULL; xmlDocPtr doc = NULL; xmlNodePtr node; struct oc_auth_form *form = NULL; char *form_id = NULL; int try_tncc = !!vpninfo->csd_wrapper; resp_buf = buf_alloc(); if (buf_error(resp_buf)) return -ENOMEM; while (1) { char *form_buf = NULL; int role_select = 0; struct oc_text_buf *url; if (resp_buf && resp_buf->pos) ret = do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded", resp_buf, &form_buf, 2); else ret = do_https_request(vpninfo, "GET", NULL, NULL, &form_buf, 2); if (ret < 0) break; url = buf_alloc(); buf_append(url, "https://%s", vpninfo->hostname); if (vpninfo->port != 443) buf_append(url, ":%d", vpninfo->port); buf_append(url, "/"); if (vpninfo->urlpath) buf_append(url, "%s", vpninfo->urlpath); if (buf_error(url)) { free(form_buf); ret = buf_free(url); break; } if (!check_cookie_success(vpninfo)) { buf_free(url); free(form_buf); ret = 0; break; } doc = htmlReadMemory(form_buf, ret, url->data, NULL, HTML_PARSE_RECOVER|HTML_PARSE_NOERROR|HTML_PARSE_NOWARNING|HTML_PARSE_NONET); buf_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) { if (try_tncc) { try_tncc = 0; ret = tncc_preauth(vpninfo); if (ret) return ret; goto tncc_done; } vpn_progress(vpninfo, PRG_ERR, _("Failed to find or parse web form in login page\n")); ret = -EINVAL; break; } free(form_id); form_id = (char *)xmlGetProp(node, (unsigned char *)"name"); if (!form_id) { vpn_progress(vpninfo, PRG_ERR, _("Encountered form with no ID\n")); goto dump_form; } else if (!strcmp(form_id, "frmLogin")) { form = parse_form_node(vpninfo, node, "btnSubmit"); if (!form) { ret = -EINVAL; break; } } else if (!strcmp(form_id, "frmDefender") || !strcmp(form_id, "frmNextToken")) { form = parse_form_node(vpninfo, node, "btnAction"); if (!form) { ret = -EINVAL; break; } } else if (!strcmp(form_id, "frmConfirmation")) { form = parse_form_node(vpninfo, node, "btnContinue"); if (!form) { ret = -EINVAL; break; } /* XXX: Actually ask the user? */ goto form_done; } else if (!strcmp(form_id, "frmSelectRoles")) { form = parse_roles_form_node(node); if (!form) { ret = -EINVAL; break; } role_select = 1; } else if (!strcmp(form_id, "frmTotpToken")) { form = parse_form_node(vpninfo, node, "totpactionEnter"); if (!form) { ret = -EINVAL; break; } } else { vpn_progress(vpninfo, PRG_ERR, _("Unknown form ID '%s'\n"), form_id); dump_form: fprintf(stderr, _("Dumping unknown HTML form:\n")); htmlNodeDumpFileFormat(stderr, node->doc, node, NULL, 1); ret = -EINVAL; break; } do { ret = process_auth_form(vpninfo, form); } while (ret == OC_FORM_RESULT_NEWGROUP); if (ret) goto out; ret = do_gen_tokencode(vpninfo, form); if (ret) { vpn_progress(vpninfo, PRG_ERR, _("Failed to generate OTP tokencode; disabling token\n")); vpninfo->token_bypassed = 1; goto out; } /* frmSelectRoles is special; it's actually *links*, not a form. So * we need to process it differently... */ if (role_select) { vpninfo->redirect_url = strdup(form->opts[0]._value); goto do_redirect; } form_done: append_form_opts(vpninfo, form, resp_buf); ret = buf_error(resp_buf); if (ret) break; vpninfo->redirect_url = form->action; form->action = NULL; do_redirect: free_auth_form(form); form = NULL; handle_redirect(vpninfo); tncc_done: xmlFreeDoc(doc); doc = NULL; } out: if (doc) xmlFreeDoc(doc); free(form_id); if (form) free_auth_form(form); buf_free(resp_buf); return ret; }