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
34 #include <openssl/ssl.h>
35 #include <openssl/err.h>
36 #include <openssl/engine.h>
38 #include "openconnect.h"
40 #define MAX_BUF_LEN 131072
42 * We didn't really want to have to do this for ourselves -- one might have
43 * thought that it would be available in a library somewhere. But neither
44 * cURL nor Neon have reliable cross-platform ways of either using a cert
45 * from the TPM, or just reading from / writing to a transport which is
46 * provided by their caller.
49 static int process_http_response(struct openconnect_info *vpninfo, int *result,
50 int (*header_cb)(struct openconnect_info *, char *, char *),
51 char *body, int buf_len)
53 char buf[MAX_BUF_LEN];
56 int http10 = 0, closeconn = 0;
59 if (openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)) < 0) {
60 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching HTTPS response\n");
65 if (!strncmp(buf, "HTTP/1.0 ", 9)) {
70 if ((!http10 && strncmp(buf, "HTTP/1.1 ", 9)) || !(*result = atoi(buf+9))) {
71 vpninfo->progress(vpninfo, PRG_ERR, "Failed to parse HTTP response '%s'\n", buf);
75 vpninfo->progress(vpninfo, PRG_TRACE, "Got HTTP response: %s\n", buf);
78 while ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
81 vpninfo->progress(vpninfo, PRG_TRACE, "%s\n", buf);
84 vpninfo->progress(vpninfo, PRG_ERR, "Error processing HTTP response\n");
87 colon = strchr(buf, ':');
89 vpninfo->progress(vpninfo, PRG_ERR, "Ignoring unknown HTTP response line '%s'\n", buf);
96 if (!strcmp(buf, "Connection") && !strcmp(colon, "Close"))
99 if (!strcmp(buf, "Location")) {
100 vpninfo->redirect_url = strdup(colon);
101 if (!vpninfo->redirect_url)
104 if (!strcmp(buf, "Content-Length")) {
105 bodylen = atoi(colon);
106 if (bodylen < 0 || bodylen > buf_len) {
107 vpninfo->progress(vpninfo, PRG_ERR, "Response body too large for buffer (%d > %d)\n",
112 if (!strcmp(buf, "Set-Cookie")) {
113 struct vpn_option *new, **this;
114 char *semicolon = strchr(colon, ';');
115 char *equals = strchr(colon, '=');
121 vpninfo->progress(vpninfo, PRG_ERR, "Invalid cookie offered: %s\n", buf);
127 new = malloc(sizeof(*new));
129 vpninfo->progress(vpninfo, PRG_ERR, "No memory for allocating cookies\n");
133 new->option = strdup(colon);
134 new->value = strdup(equals);
136 /* Kill cookie; don't replace it */
139 for (this = &vpninfo->cookies; *this; this = &(*this)->next) {
140 if (!strcmp(colon, (*this)->option)) {
141 /* Replace existing cookie */
143 new->next = (*this)->next;
147 free((*this)->option);
148 free((*this)->value);
159 if (!strcmp(buf, "Transfer-Encoding")) {
160 if (!strcmp(colon, "chunked"))
163 vpninfo->progress(vpninfo, PRG_ERR, "Unknown Transfer-Encoding: %s\n", colon);
167 if (header_cb && !strncmp(buf, "X-", 2))
168 header_cb(vpninfo, buf, colon);
171 /* Handle 'HTTP/1.1 100 Continue'. Not that we should ever see it */
175 /* Now the body, if there is one */
180 /* HTTP 1.0 response. Just eat all we can. */
182 i = SSL_read(vpninfo->https_ssl, body + done, bodylen - done);
188 /* If we were given Content-Length, it's nice and easy... */
190 while (done < bodylen) {
191 i = SSL_read(vpninfo->https_ssl, body + done, bodylen - done);
193 vpninfo->progress(vpninfo, PRG_ERR, "Error reading HTTP response body\n");
201 /* ... else, chunked */
202 while ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
203 int chunklen, lastchunk = 0;
206 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching chunk header\n");
209 chunklen = strtol(buf, NULL, 16);
214 if (chunklen + done > buf_len) {
215 vpninfo->progress(vpninfo, PRG_ERR, "Response body too large for buffer (%d > %d)\n",
216 chunklen + done, buf_len);
220 i = SSL_read(vpninfo->https_ssl, body + done, chunklen);
222 vpninfo->progress(vpninfo, PRG_ERR, "Error reading HTTP response body\n");
229 if ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
231 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching HTTP response body\n");
233 vpninfo->progress(vpninfo, PRG_ERR, "Error in chunked decoding. Expected '', got: '%s'",
244 SSL_free(vpninfo->https_ssl);
245 vpninfo->https_ssl = NULL;
246 close(vpninfo->ssl_fd);
247 vpninfo->ssl_fd = -1;
253 static int fetch_config(struct openconnect_info *vpninfo, char *fu, char *bu,
256 struct vpn_option *opt;
257 char buf[MAX_BUF_LEN];
259 unsigned char local_sha1_bin[SHA_DIGEST_LENGTH];
260 char local_sha1_ascii[(SHA_DIGEST_LENGTH * 2)+1];
264 sprintf(buf, "GET %s%s HTTP/1.1\r\n", fu, bu);
265 sprintf(buf + strlen(buf), "Host: %s\r\n", vpninfo->hostname);
266 sprintf(buf + strlen(buf), "User-Agent: %s\r\n", vpninfo->useragent);
267 sprintf(buf + strlen(buf), "Accept: */*\r\n");
268 sprintf(buf + strlen(buf), "Accept-Encoding: identity\r\n");
270 if (vpninfo->cookies) {
271 sprintf(buf + strlen(buf), "Cookie: ");
272 for (opt = vpninfo->cookies; opt; opt = opt->next)
273 sprintf(buf + strlen(buf), "%s=%s%s", opt->option,
274 opt->value, opt->next ? "; " : "\r\n");
276 sprintf(buf + strlen(buf), "X-Transcend-Version: 1\r\n\r\n");
278 SSL_write(vpninfo->https_ssl, buf, strlen(buf));
280 buflen = process_http_response(vpninfo, &result, NULL, buf, MAX_BUF_LEN);
282 /* We'll already have complained about whatever offended us */
291 EVP_Digest(buf, buflen, local_sha1_bin, NULL, EVP_sha1(), NULL);
292 EVP_MD_CTX_cleanup(&c);
294 for (i = 0; i < SHA_DIGEST_LENGTH; i++)
295 sprintf(&local_sha1_ascii[i*2], "%02x", local_sha1_bin[i]);
297 if (strcasecmp(server_sha1, local_sha1_ascii)) {
298 vpninfo->progress(vpninfo, PRG_ERR, "Downloaded config file did not match intended SHA1\n");
302 return vpninfo->write_new_config(vpninfo, buf, buflen);
305 static int run_csd_script(struct openconnect_info *vpninfo, char *buf)
310 sprintf(fname, "/tmp/csdXXXXXX");
314 vpninfo->progress(vpninfo, PRG_ERR, "Failed to open temporary CSD script file: %s\n",
318 write(fd, buf, strlen(buf));
321 /* FIXME: Add whatever arguments we need */
323 vpninfo->progress(vpninfo, PRG_ERR, "Failed to exec CSD script %s\n", fname);
325 /* FIXME: Remember the filename so we can delete it later */
327 free(vpninfo->csd_stuburl);
328 vpninfo->csd_stuburl = NULL;
329 vpninfo->urlpath = strdup(vpninfo->csd_waiturl);
330 vpninfo->csd_waiturl = NULL;
336 * = 0, no cookie (user cancel)
337 * = 1, obtained cookie
339 int openconnect_obtain_cookie(struct openconnect_info *vpninfo)
341 struct vpn_option *opt, *next;
342 char buf[MAX_BUF_LEN];
344 char request_body[2048];
345 char *request_body_type = NULL;
346 char *method = "GET";
349 if (!vpninfo->https_ssl && openconnect_open_https(vpninfo)) {
350 vpninfo->progress(vpninfo, PRG_ERR, "Failed to open HTTPS connection to %s\n",
356 * It would be nice to use cURL for this, but we really need to guarantee
357 * that we'll be using OpenSSL (for the TPM stuff), and it doesn't seem
358 * to have any way to let us provide our own socket read/write functions.
359 * We can only provide a socket _open_ function. Which would require having
360 * a socketpair() and servicing the "other" end of it.
362 * So we process the HTTP for ourselves...
364 sprintf(buf, "%s /%s HTTP/1.1\r\n", method, vpninfo->urlpath ?: "");
365 sprintf(buf + strlen(buf), "Host: %s\r\n", vpninfo->hostname);
366 sprintf(buf + strlen(buf), "User-Agent: %s\r\n", vpninfo->useragent);
367 sprintf(buf + strlen(buf), "Accept: */*\r\n");
368 sprintf(buf + strlen(buf), "Accept-Encoding: identity\r\n");
370 if (vpninfo->cookies) {
371 sprintf(buf + strlen(buf), "Cookie: ");
372 for (opt = vpninfo->cookies; opt; opt = opt->next)
373 sprintf(buf + strlen(buf), "%s=%s%s", opt->option,
374 opt->value, opt->next ? "; " : "\r\n");
376 if (request_body_type) {
377 sprintf(buf + strlen(buf), "Content-Type: %s\r\n",
379 sprintf(buf + strlen(buf), "Content-Length: %zd\r\n",
380 strlen(request_body));
382 sprintf(buf + strlen(buf), "X-Transcend-Version: 1\r\n\r\n");
383 if (request_body_type)
384 sprintf(buf + strlen(buf), "%s", request_body);
386 vpninfo->progress(vpninfo, PRG_INFO, "%s %s/%s\n", method,
387 vpninfo->hostname, vpninfo->urlpath ?: "");
389 SSL_write(vpninfo->https_ssl, buf, strlen(buf));
391 buflen = process_http_response(vpninfo, &result, NULL, buf, MAX_BUF_LEN);
393 /* We'll already have complained about whatever offended us */
397 if (result != 200 && vpninfo->redirect_url) {
398 if (!strncmp(vpninfo->redirect_url, "https://", 8)) {
399 /* New host. Tear down the existing connection and make a new one */
400 char *host = vpninfo->redirect_url + 8;
401 char *path = strchr(host, '/');
403 free(vpninfo->urlpath);
406 vpninfo->urlpath = strdup(path);
408 vpninfo->urlpath = NULL;
410 if (strcmp(vpninfo->hostname, host)) {
411 free(vpninfo->hostname);
412 vpninfo->hostname = strdup(host);
414 /* Kill the existing connection, and a new one will happen */
415 SSL_free(vpninfo->https_ssl);
416 vpninfo->https_ssl = NULL;
417 close(vpninfo->ssl_fd);
418 vpninfo->ssl_fd = -1;
420 for (opt = vpninfo->cookies; opt; opt = next) {
427 vpninfo->cookies = NULL;
429 free(vpninfo->redirect_url);
430 vpninfo->redirect_url = NULL;
433 } else if (vpninfo->redirect_url[0] == '/') {
434 /* Absolute redirect within same host */
435 free(vpninfo->urlpath);
436 vpninfo->urlpath = strdup(vpninfo->redirect_url + 1);
437 free(vpninfo->redirect_url);
438 vpninfo->redirect_url = NULL;
441 vpninfo->progress(vpninfo, PRG_ERR, "Relative redirect (to '%s') not supported\n",
442 vpninfo->redirect_url);
447 if (vpninfo->csd_stuburl) {
448 /* This is the CSD stub script, which we now need to run */
449 result = run_csd_script(vpninfo, buf);
453 /* Now we'll be redirected to the waiturl */
458 result = parse_xml_response(vpninfo, buf, request_body, sizeof(request_body),
459 &method, &request_body_type);
465 /* A return value of 2 means the XML form indicated
466 success. We _should_ have a cookie... */
468 for (opt = vpninfo->cookies; opt; opt = opt->next) {
470 if (!strcmp(opt->option, "webvpn"))
471 vpninfo->cookie = opt->value;
472 else if (vpninfo->write_new_config && !strcmp(opt->option, "webvpnc")) {
473 char *tok = opt->value;
474 char *bu = NULL, *fu = NULL, *sha = NULL;
477 if (tok != opt->value)
480 if (!strncmp(tok, "bu:", 3))
482 else if (!strncmp(tok, "fu:", 3))
484 else if (!strncmp(tok, "fh:", 3)) {
485 if (!strncasecmp(tok+3, vpninfo->xmlsha1,
486 SHA_DIGEST_LENGTH * 2))
490 } while ((tok = strchr(tok, '&')));
493 fetch_config(vpninfo, bu, fu, sha);
500 char *openconnect_create_useragent(char *base)
502 char *uagent = malloc(strlen(base) + 1 + strlen(openconnect_version));
503 sprintf(uagent, "%s%s", base, openconnect_version);