2 * OpenConnect (SSL + DTLS) VPN client
4 * Copyright © 2008 Intel Corporation.
5 * Copyright © 2008 Nick Andrew <nick@nick-andrew.net>
7 * Author: David Woodhouse <dwmw2@infradead.org>
9 * This program is free software; you can redistribute it and/or
10 * modify it under the terms of the GNU Lesser General Public License
11 * version 2.1, as published by the Free Software Foundation.
13 * This program is distributed in the hope that it will be useful, but
14 * WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
16 * Lesser General Public License for more details.
18 * You should have received a copy of the GNU Lesser General Public
19 * License along with this library; if not, write to:
21 * Free Software Foundation, Inc.
22 * 51 Franklin Street, Fifth Floor,
23 * Boston, MA 02110-1301 USA
35 #include <sys/types.h>
37 #include <openssl/ssl.h>
38 #include <openssl/err.h>
39 #include <openssl/engine.h>
41 #include "openconnect.h"
43 #define MAX_BUF_LEN 131072
45 * We didn't really want to have to do this for ourselves -- one might have
46 * thought that it would be available in a library somewhere. But neither
47 * cURL nor Neon have reliable cross-platform ways of either using a cert
48 * from the TPM, or just reading from / writing to a transport which is
49 * provided by their caller.
52 static int process_http_response(struct openconnect_info *vpninfo, int *result,
53 int (*header_cb)(struct openconnect_info *, char *, char *),
54 char *body, int buf_len)
56 char buf[MAX_BUF_LEN];
59 int http10 = 0, closeconn = 0;
62 if (openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)) < 0) {
63 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching HTTPS response\n");
68 if (!strncmp(buf, "HTTP/1.0 ", 9)) {
73 if ((!http10 && strncmp(buf, "HTTP/1.1 ", 9)) || !(*result = atoi(buf+9))) {
74 vpninfo->progress(vpninfo, PRG_ERR, "Failed to parse HTTP response '%s'\n", buf);
78 vpninfo->progress(vpninfo, PRG_TRACE, "Got HTTP response: %s\n", buf);
81 while ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
84 vpninfo->progress(vpninfo, PRG_TRACE, "%s\n", buf);
87 vpninfo->progress(vpninfo, PRG_ERR, "Error processing HTTP response\n");
90 colon = strchr(buf, ':');
92 vpninfo->progress(vpninfo, PRG_ERR, "Ignoring unknown HTTP response line '%s'\n", buf);
99 if (!strcmp(buf, "Connection") && !strcmp(colon, "Close"))
102 if (!strcmp(buf, "Location")) {
103 vpninfo->redirect_url = strdup(colon);
104 if (!vpninfo->redirect_url)
107 if (!strcmp(buf, "Content-Length")) {
108 bodylen = atoi(colon);
109 if (bodylen < 0 || bodylen > buf_len) {
110 vpninfo->progress(vpninfo, PRG_ERR, "Response body too large for buffer (%d > %d)\n",
115 if (!strcmp(buf, "Set-Cookie")) {
116 struct vpn_option *new, **this;
117 char *semicolon = strchr(colon, ';');
118 char *equals = strchr(colon, '=');
124 vpninfo->progress(vpninfo, PRG_ERR, "Invalid cookie offered: %s\n", buf);
130 new = malloc(sizeof(*new));
132 vpninfo->progress(vpninfo, PRG_ERR, "No memory for allocating cookies\n");
136 new->option = strdup(colon);
137 new->value = strdup(equals);
139 /* Kill cookie; don't replace it */
142 for (this = &vpninfo->cookies; *this; this = &(*this)->next) {
143 if (!strcmp(colon, (*this)->option)) {
144 /* Replace existing cookie */
146 new->next = (*this)->next;
150 free((*this)->option);
151 free((*this)->value);
162 if (!strcmp(buf, "Transfer-Encoding")) {
163 if (!strcmp(colon, "chunked"))
166 vpninfo->progress(vpninfo, PRG_ERR, "Unknown Transfer-Encoding: %s\n", colon);
170 if (header_cb && !strncmp(buf, "X-", 2))
171 header_cb(vpninfo, buf, colon);
174 /* Handle 'HTTP/1.1 100 Continue'. Not that we should ever see it */
178 /* Now the body, if there is one */
183 /* HTTP 1.0 response. Just eat all we can. */
185 i = SSL_read(vpninfo->https_ssl, body + done, bodylen - done);
191 /* If we were given Content-Length, it's nice and easy... */
193 while (done < bodylen) {
194 i = SSL_read(vpninfo->https_ssl, body + done, bodylen - done);
196 vpninfo->progress(vpninfo, PRG_ERR, "Error reading HTTP response body\n");
204 /* ... else, chunked */
205 while ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
206 int chunklen, lastchunk = 0;
209 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching chunk header\n");
212 chunklen = strtol(buf, NULL, 16);
217 if (chunklen + done > buf_len) {
218 vpninfo->progress(vpninfo, PRG_ERR, "Response body too large for buffer (%d > %d)\n",
219 chunklen + done, buf_len);
223 i = SSL_read(vpninfo->https_ssl, body + done, chunklen);
225 vpninfo->progress(vpninfo, PRG_ERR, "Error reading HTTP response body\n");
232 if ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
234 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching HTTP response body\n");
236 vpninfo->progress(vpninfo, PRG_ERR, "Error in chunked decoding. Expected '', got: '%s'",
247 SSL_free(vpninfo->https_ssl);
248 vpninfo->https_ssl = NULL;
249 close(vpninfo->ssl_fd);
250 vpninfo->ssl_fd = -1;
256 static int fetch_config(struct openconnect_info *vpninfo, char *fu, char *bu,
259 struct vpn_option *opt;
260 char buf[MAX_BUF_LEN];
262 unsigned char local_sha1_bin[SHA_DIGEST_LENGTH];
263 char local_sha1_ascii[(SHA_DIGEST_LENGTH * 2)+1];
267 sprintf(buf, "GET %s%s HTTP/1.1\r\n", fu, bu);
268 sprintf(buf + strlen(buf), "Host: %s\r\n", vpninfo->hostname);
269 sprintf(buf + strlen(buf), "User-Agent: %s\r\n", vpninfo->useragent);
270 sprintf(buf + strlen(buf), "Accept: */*\r\n");
271 sprintf(buf + strlen(buf), "Accept-Encoding: identity\r\n");
273 if (vpninfo->cookies) {
274 sprintf(buf + strlen(buf), "Cookie: ");
275 for (opt = vpninfo->cookies; opt; opt = opt->next)
276 sprintf(buf + strlen(buf), "%s=%s%s", opt->option,
277 opt->value, opt->next ? "; " : "\r\n");
279 sprintf(buf + strlen(buf), "X-Transcend-Version: 1\r\n\r\n");
281 SSL_write(vpninfo->https_ssl, buf, strlen(buf));
283 buflen = process_http_response(vpninfo, &result, NULL, buf, MAX_BUF_LEN);
285 /* We'll already have complained about whatever offended us */
294 EVP_Digest(buf, buflen, local_sha1_bin, NULL, EVP_sha1(), NULL);
295 EVP_MD_CTX_cleanup(&c);
297 for (i = 0; i < SHA_DIGEST_LENGTH; i++)
298 sprintf(&local_sha1_ascii[i*2], "%02x", local_sha1_bin[i]);
300 if (strcasecmp(server_sha1, local_sha1_ascii)) {
301 vpninfo->progress(vpninfo, PRG_ERR, "Downloaded config file did not match intended SHA1\n");
305 return vpninfo->write_new_config(vpninfo, buf, buflen);
308 static int run_csd_script(struct openconnect_info *vpninfo, char *buf, int buflen)
313 if (!vpninfo->uid_csd_given) {
314 vpninfo->progress(vpninfo, PRG_ERR, "Error: You are trying to "
315 "run insecure CSD code without specifying the CSD user.\n"
316 " Use command line option \"--csd-user\"\n");
321 vpninfo->progress(vpninfo, PRG_INFO,
322 "Trying to run Linux CSD trojan script.");
325 sprintf(fname, "/tmp/csdXXXXXX");
329 vpninfo->progress(vpninfo, PRG_ERR, "Failed to open temporary CSD script file: %s\n",
333 write(fd, buf, buflen);
338 X509 *cert = SSL_get_peer_certificate(vpninfo->https_ssl);
339 char certbuf[EVP_MAX_MD_SIZE * 2 + 1];
343 if (vpninfo->uid_csd != getuid()) {
346 if (setuid(vpninfo->uid_csd)) {
347 fprintf(stderr, "Failed to set uid %d\n",
351 if (!(pw = getpwuid(vpninfo->uid_csd))) {
352 fprintf(stderr, "Invalid user uid=%d\n",
356 setenv("HOME", pw->pw_dir, 1);
359 if (vpninfo->uid_csd == 0) {
360 fprintf(stderr, "Warning: you are running insecure "
361 "CSD code with root privileges\n"
362 "\t Use command line option \"--csd-user\"\n");
365 csd_argv[i++] = fname;
366 csd_argv[i++] = "-ticket";
367 asprintf(&csd_argv[i++], "\"%s\"", vpninfo->csd_ticket);
368 csd_argv[i++] = "-stub";
369 csd_argv[i++] = "\"0\"";
370 csd_argv[i++] = "-group";
371 asprintf(&csd_argv[i++], "\"%s\"", vpninfo->authgroup?:"");
372 get_cert_md5_fingerprint(vpninfo, cert, certbuf);
373 csd_argv[i++] = "-certhash";
374 asprintf(&csd_argv[i++], "\"%s:%s\"", certbuf, vpninfo->cert_md5_fingerprint ?: "");
375 csd_argv[i++] = "-url";
376 asprintf(&csd_argv[i++], "\"https://%s%s\"", vpninfo->hostname, vpninfo->csd_starturl);
377 /* WTF would it want to know this for? */
378 csd_argv[i++] = "-vpnclient";
379 csd_argv[i++] = "\"/opt/cisco/vpn/bin/vpnui";
380 csd_argv[i++] = "-connect";
381 asprintf(&csd_argv[i++], "https://%s/%s", vpninfo->hostname, vpninfo->csd_preurl);
382 csd_argv[i++] = "-connectparam";
383 asprintf(&csd_argv[i++], "#csdtoken=%s\"", vpninfo->csd_token);
384 csd_argv[i++] = "-langselen";
385 csd_argv[i++] = NULL;
387 execv(fname, csd_argv);
388 vpninfo->progress(vpninfo, PRG_ERR, "Failed to exec CSD script %s\n", fname);
392 free(vpninfo->csd_stuburl);
393 vpninfo->csd_stuburl = NULL;
394 vpninfo->urlpath = strdup(vpninfo->csd_waiturl +
395 (vpninfo->csd_waiturl[0] == '/' ? 1 : 0));
396 vpninfo->csd_waiturl = NULL;
397 vpninfo->csd_scriptname = strdup(fname);
399 /* AB: add cookie "sdesktop=__TOKEN__" */
401 struct vpn_option *new, **this;
402 new = malloc(sizeof(*new));
404 vpninfo->progress(vpninfo, PRG_ERR, "No memory for allocating cookies\n");
408 new->option = strdup("sdesktop");
409 new->value = strdup(vpninfo->csd_token);
411 for (this = &vpninfo->cookies; *this; this = &(*this)->next) {
412 if (!strcmp(new->option, (*this)->option)) {
413 /* Replace existing cookie */
415 new->next = (*this)->next;
419 free((*this)->option);
420 free((*this)->value);
435 char *local_strcasestr(const char *haystack, const char *needle)
437 int hlen = strlen(haystack);
438 int nlen = strlen(needle);
441 for (i = 0; i < hlen - nlen + 1; i++) {
442 for (j = 0; j < nlen; j++) {
443 if (tolower(haystack[i + j]) !=
448 return (char *)haystack + i;
452 #define strcasestr local_strcasestr
457 * = 0, no cookie (user cancel)
458 * = 1, obtained cookie
460 int openconnect_obtain_cookie(struct openconnect_info *vpninfo)
462 struct vpn_option *opt, *next;
463 char buf[MAX_BUF_LEN];
465 char request_body[2048];
466 char *request_body_type = NULL;
467 char *method = "GET";
470 if (!vpninfo->https_ssl && openconnect_open_https(vpninfo)) {
471 vpninfo->progress(vpninfo, PRG_ERR, "Failed to open HTTPS connection to %s\n",
477 * It would be nice to use cURL for this, but we really need to guarantee
478 * that we'll be using OpenSSL (for the TPM stuff), and it doesn't seem
479 * to have any way to let us provide our own socket read/write functions.
480 * We can only provide a socket _open_ function. Which would require having
481 * a socketpair() and servicing the "other" end of it.
483 * So we process the HTTP for ourselves...
485 sprintf(buf, "%s /%s HTTP/1.1\r\n", method, vpninfo->urlpath ?: "");
486 sprintf(buf + strlen(buf), "Host: %s\r\n", vpninfo->hostname);
487 sprintf(buf + strlen(buf), "User-Agent: %s\r\n", vpninfo->useragent);
488 sprintf(buf + strlen(buf), "Accept: */*\r\n");
489 sprintf(buf + strlen(buf), "Accept-Encoding: identity\r\n");
491 if (vpninfo->cookies) {
492 sprintf(buf + strlen(buf), "Cookie: ");
493 for (opt = vpninfo->cookies; opt; opt = opt->next)
494 sprintf(buf + strlen(buf), "%s=%s%s", opt->option,
495 opt->value, opt->next ? "; " : "\r\n");
497 if (request_body_type) {
498 sprintf(buf + strlen(buf), "Content-Type: %s\r\n",
500 sprintf(buf + strlen(buf), "Content-Length: %zd\r\n",
501 strlen(request_body));
503 sprintf(buf + strlen(buf), "X-Transcend-Version: 1\r\n\r\n");
504 if (request_body_type)
505 sprintf(buf + strlen(buf), "%s", request_body);
507 vpninfo->progress(vpninfo, PRG_INFO, "%s %s/%s\n", method,
508 vpninfo->hostname, vpninfo->urlpath ?: "");
510 SSL_write(vpninfo->https_ssl, buf, strlen(buf));
512 buflen = process_http_response(vpninfo, &result, NULL, buf, MAX_BUF_LEN);
514 /* We'll already have complained about whatever offended us */
518 if (result != 200 && vpninfo->redirect_url) {
520 if (!strncmp(vpninfo->redirect_url, "https://", 8)) {
521 /* New host. Tear down the existing connection and make a new one */
522 char *host = vpninfo->redirect_url + 8;
523 char *path = strchr(host, '/');
525 free(vpninfo->urlpath);
528 vpninfo->urlpath = strdup(path);
530 vpninfo->urlpath = NULL;
532 if (strcmp(vpninfo->hostname, host)) {
533 free(vpninfo->hostname);
534 vpninfo->hostname = strdup(host);
536 /* Kill the existing connection, and a new one will happen */
537 SSL_free(vpninfo->https_ssl);
538 vpninfo->https_ssl = NULL;
539 close(vpninfo->ssl_fd);
540 vpninfo->ssl_fd = -1;
542 for (opt = vpninfo->cookies; opt; opt = next) {
549 vpninfo->cookies = NULL;
551 free(vpninfo->redirect_url);
552 vpninfo->redirect_url = NULL;
555 } else if (vpninfo->redirect_url[0] == '/') {
556 /* Absolute redirect within same host */
557 free(vpninfo->urlpath);
558 vpninfo->urlpath = strdup(vpninfo->redirect_url + 1);
559 free(vpninfo->redirect_url);
560 vpninfo->redirect_url = NULL;
563 vpninfo->progress(vpninfo, PRG_ERR, "Relative redirect (to '%s') not supported\n",
564 vpninfo->redirect_url);
569 if (vpninfo->csd_stuburl) {
570 /* This is the CSD stub script, which we now need to run */
571 result = run_csd_script(vpninfo, buf, buflen);
575 /* Now we'll be redirected to the waiturl */
578 if (strncmp(buf, "<?xml", 5)) {
579 /* Not XML? Perhaps it's HTML with a refresh... */
580 if (strcasestr(buf, "http-equiv=\"refresh\"")) {
581 vpninfo->progress(vpninfo, PRG_INFO, "Refreshing %s after 1 second...\n",
586 vpninfo->progress(vpninfo, PRG_ERR, "Unknown response from server\n");
590 result = parse_xml_response(vpninfo, buf, request_body, sizeof(request_body),
591 &method, &request_body_type);
597 /* A return value of 2 means the XML form indicated
598 success. We _should_ have a cookie... */
600 for (opt = vpninfo->cookies; opt; opt = opt->next) {
602 if (!strcmp(opt->option, "webvpn"))
603 vpninfo->cookie = opt->value;
604 else if (vpninfo->write_new_config && !strcmp(opt->option, "webvpnc")) {
605 char *tok = opt->value;
606 char *bu = NULL, *fu = NULL, *sha = NULL;
609 if (tok != opt->value)
612 if (!strncmp(tok, "bu:", 3))
614 else if (!strncmp(tok, "fu:", 3))
616 else if (!strncmp(tok, "fh:", 3)) {
617 if (!strncasecmp(tok+3, vpninfo->xmlsha1,
618 SHA_DIGEST_LENGTH * 2))
622 } while ((tok = strchr(tok, '&')));
625 fetch_config(vpninfo, bu, fu, sha);
628 if (vpninfo->csd_scriptname) {
629 unlink(vpninfo->csd_scriptname);
630 free(vpninfo->csd_scriptname);
631 vpninfo->csd_scriptname = NULL;
636 char *openconnect_create_useragent(char *base)
638 char *uagent = malloc(strlen(base) + 1 + strlen(openconnect_version));
639 sprintf(uagent, "%s %s", base, openconnect_version);