From 90092f9bd03db1add7effae1f1becdc670d86ab4 Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Wed, 21 Jan 2015 14:17:19 +0000 Subject: [PATCH] Add TNCC support Signed-off-by: David Woodhouse --- library.c | 3 + oncp.c | 187 ++++++++++++++++++++++++++++++++++++++--- openconnect-internal.h | 1 + tncc-wrapper.py | 122 +++++++++++++++++++++++++++ 4 files changed, 303 insertions(+), 10 deletions(-) create mode 100755 tncc-wrapper.py diff --git a/library.c b/library.c index 2fa1674f..83720fcd 100644 --- a/library.c +++ b/library.c @@ -71,6 +71,7 @@ struct openconnect_info *openconnect_vpninfo_new(const char *useragent, #endif vpninfo->ssl_fd = vpninfo->dtls_fd = -1; vpninfo->cmd_fd = vpninfo->cmd_fd_write = -1; + vpninfo->tncc_fd = -1; vpninfo->cert_expire_warning = 60 * 86400; vpninfo->req_compr = COMPR_STATELESS; vpninfo->max_qlen = 10; @@ -206,6 +207,8 @@ void openconnect_vpninfo_free(struct openconnect_info *vpninfo) openconnect_close_https(vpninfo, 1); if (vpninfo->proto.udp_shutdown) vpninfo->proto.udp_shutdown(vpninfo); + if (vpninfo->tncc_fd != -1) + closesocket(vpninfo->tncc_fd); if (vpninfo->cmd_fd_write != -1) { closesocket(vpninfo->cmd_fd); closesocket(vpninfo->cmd_fd_write); diff --git a/oncp.c b/oncp.c index 696a1e70..115151b9 100644 --- a/oncp.c +++ b/oncp.c @@ -27,6 +27,10 @@ #include #include #include +#include +#ifndef _WIN32 +#include +#endif #include #include @@ -199,9 +203,14 @@ static int oncp_https_submit(struct openconnect_info *vpninfo, char *form_buf = NULL; struct oc_text_buf *url; - ret = do_https_request(vpninfo, req_buf ? "POST" : "GET", - req_buf ? "application/x-www-form-urlencoded" : NULL, - req_buf, &form_buf, 2); + if (req_buf && req_buf->pos) + ret =do_https_request(vpninfo, "POST", + "application/x-www-form-urlencoded", + req_buf, &form_buf, 2); + else + ret = do_https_request(vpninfo, "GET", NULL, NULL, + &form_buf, 2); + if (ret < 0) return ret; @@ -260,8 +269,17 @@ static int check_cookie_success(struct openconnect_info *vpninfo) if (!dsid) return -ENOENT; - /* XXX: Do these need escaping? Could they theoreetically have semicolons in? */ buf = buf_alloc(); + if (vpninfo->tncc_fd != -1) { + buf_append(buf, "setcookie\n"); + buf_append(buf, "Cookie=%s\n", dsid); + 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); @@ -277,6 +295,138 @@ static int check_cookie_success(struct openconnect_info *vpninfo) 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], *p; + 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; + } + + 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")); + return buf_free(buf); + } +#ifdef SOCK_CLOEXEC + if (socketpair(AF_UNIX, SOCK_SEQPACKET | SOCK_CLOEXEC, 0, sockfd)) +#endif + { + if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockfd)) + return -errno; + set_fd_cloexec(sockfd[0]); + set_fd_cloexec(sockfd[1]); + } + pid = fork(); + if (pid == -1) { + buf_free(buf); + 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 stdout */ + dup2(1, 2); + 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]); + + if (send(sockfd[1], buf->data, buf->pos, 0) != 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")); + + len = recv(sockfd[1], recvbuf, sizeof(recvbuf) - 1, 0); + if (len < 0) { + vpn_progress(vpninfo, PRG_ERR, + _("Failed to read response from TNCC\n")); + close(sockfd[1]); + return -EIO; + } + + recvbuf[len] = 0; + + p = strchr(recvbuf, '\n'); + if (!p) { + invalid_response: + vpn_progress(vpninfo, PRG_ERR, + _("Received invalid response from TNCC\n")); + print_response: + vpn_progress(vpninfo, PRG_TRACE, _("TNCC response: -->\n%s\n<--\n"), + recvbuf); + close(sockfd[1]); + return -EINVAL; + } + *p = 0; + if (strcmp(recvbuf, "200")) { + vpn_progress(vpninfo, PRG_ERR, + _("Received unsuccessful %s response from TNCC\n"), + recvbuf); + goto print_response; + } + p = strchr(p + 1, '\n'); + if (!p) + goto invalid_response; + dspreauth = p + 1; + p = strchr(p + 1, '\n'); + if (!p) + goto invalid_response; + *p = 0; + vpn_progress(vpninfo, PRG_DEBUG, + _("Got new DSPREAUTH cookie from TNCC: %s\n"), + dspreauth); + http_add_cookie(vpninfo, "DSPREAUTH", dspreauth, 1); + vpninfo->tncc_fd = sockfd[1]; + return 0; +} +#endif int oncp_obtain_cookie(struct openconnect_info *vpninfo) { @@ -286,6 +436,7 @@ int oncp_obtain_cookie(struct openconnect_info *vpninfo) 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)) @@ -293,19 +444,30 @@ int oncp_obtain_cookie(struct openconnect_info *vpninfo) while (1) { ret = oncp_https_submit(vpninfo, resp_buf, &doc); - if (ret) - return ret; - if (!check_cookie_success(vpninfo)) + if (ret || !check_cookie_success(vpninfo)) break; + + 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; } form_id = (char *)xmlGetProp(node, (unsigned char *)"name"); - if (!strcmp(form_id, "frmLogin")) { + 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; @@ -330,6 +492,9 @@ int oncp_obtain_cookie(struct openconnect_info *vpninfo) 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; } @@ -338,7 +503,6 @@ int oncp_obtain_cookie(struct openconnect_info *vpninfo) if (ret) goto out; - buf_truncate(resp_buf); form_done: append_form_opts(vpninfo, form, resp_buf); ret = buf_error(resp_buf); @@ -349,10 +513,13 @@ int oncp_obtain_cookie(struct openconnect_info *vpninfo) form->action = NULL; free_auth_form(form); form = NULL; + handle_redirect(vpninfo); + + tncc_done: xmlFreeDoc(doc); doc = NULL; + buf_truncate(resp_buf); - handle_redirect(vpninfo); } out: if (doc) diff --git a/openconnect-internal.h b/openconnect-internal.h index d06d69a2..507a539c 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -246,6 +246,7 @@ struct openconnect_info { char *redirect_url; int redirect_type; + int tncc_fd; /* For Juniper TNCC */ const char *csd_xmltag; int csd_nostub; char *platname; diff --git a/tncc-wrapper.py b/tncc-wrapper.py new file mode 100755 index 00000000..0b347563 --- /dev/null +++ b/tncc-wrapper.py @@ -0,0 +1,122 @@ +#!/usr/bin/python + +# Lifted from Russ Dill's juniper-vpn-wrap.py, thus: +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +import subprocess +import mechanize +import cookielib +import getpass +import sys +import os +import zipfile +import urllib +import socket +import ssl +import errno +import argparse +import atexit +import signal +import ConfigParser +import time +import binascii +import hmac +import hashlib + +def mkdir_p(path): + try: + os.mkdir(path) + except OSError, exc: + if exc.errno == errno.EEXIST and os.path.isdir(path): + pass + else: + raise + +class Tncc: + def __init__(self, vpn_host): + self.vpn_host = vpn_host; + self.plugin_jar = '/usr/share/icedtea-web/plugin.jar' + + if not os.path.isfile(self.plugin_jar): + raise Exception(self.plugin_jar + ' not found') + self.user_agent = 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.1) Gecko/2008071615 Fedora/3.0.1-1.fc9 Firefox/3.0.1' + + def tncc_init(self): + class_names = ('net.juniper.tnc.NARPlatform.linux.LinuxHttpNAR', + 'net.juniper.tnc.HttpNAR.HttpNAR') + self.class_name = None + + self.tncc_jar = os.path.expanduser('~/.juniper_networks/tncc.jar') + try: + if zipfile.ZipFile(self.tncc_jar, 'r').testzip() is not None: + raise Exception() + except: + print 'Downloading tncc.jar...' + mkdir_p(os.path.expanduser('~/.juniper_networks')) + urllib.urlretrieve('https://' + self.vpn_host + + '/dana-cached/hc/tncc.jar', self.tncc_jar) + + with zipfile.ZipFile(self.tncc_jar, 'r') as jar: + for name in class_names: + try: + jar.getinfo(name.replace('.', '/') + '.class') + self.class_name = name + break + except: + pass + + if self.class_name is None: + raise Exception('Could not find class name for', self.tncc_jar) + + self.tncc_preload = \ + os.path.expanduser('~/.juniper_networks/tncc_preload.so') + if not os.path.isfile(self.tncc_preload): + raise Exception('Missing', self.tncc_preload) + + def tncc_start(self): + # tncc is the host checker app. It can check different + # security policies of the host and report back. We have + # to send it a preauth key (from the DSPREAUTH cookie) + # and it sends back a new cookie value we submit. + # After logging in, we send back another cookie to tncc. + # Subsequently, it contacts https://