Minor fixes of quotation marks in CSD script arguments
[platform/upstream/openconnect.git] / http.c
1 /*
2  * OpenConnect (SSL + DTLS) VPN client
3  *
4  * Copyright © 2008 Intel Corporation.
5  * Copyright © 2008 Nick Andrew <nick@nick-andrew.net>
6  *
7  * Author: David Woodhouse <dwmw2@infradead.org>
8  *
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.
12  *
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.
17  *
18  * You should have received a copy of the GNU Lesser General Public
19  * License along with this library; if not, write to:
20  *
21  *   Free Software Foundation, Inc.
22  *   51 Franklin Street, Fifth Floor,
23  *   Boston, MA 02110-1301 USA
24  */
25
26 #define _GNU_SOURCE
27 #include <netdb.h>
28 #include <unistd.h>
29 #include <fcntl.h>
30 #include <time.h>
31 #include <string.h>
32 #include <ctype.h>
33 #include <sys/stat.h>
34
35 #include <openssl/ssl.h>
36 #include <openssl/err.h>
37 #include <openssl/engine.h>
38
39 #include "openconnect.h"
40
41 #define MAX_BUF_LEN 131072
42 /*
43  * We didn't really want to have to do this for ourselves -- one might have
44  * thought that it would be available in a library somewhere. But neither
45  * cURL nor Neon have reliable cross-platform ways of either using a cert
46  * from the TPM, or just reading from / writing to a transport which is
47  * provided by their caller.
48  */
49
50 static int process_http_response(struct openconnect_info *vpninfo, int *result,
51                                  int (*header_cb)(struct openconnect_info *, char *, char *),
52                                  char *body, int buf_len)
53 {
54         char buf[MAX_BUF_LEN];
55         int bodylen = 0;
56         int done = 0;
57         int http10 = 0, closeconn = 0;
58         int i;
59
60         if (openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)) < 0) {
61                 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching HTTPS response\n");
62                 return -EINVAL;
63         }
64
65  cont:
66         if (!strncmp(buf, "HTTP/1.0 ", 9)) {
67                 http10 = 1;
68                 closeconn = 1;
69         }
70
71         if ((!http10 && strncmp(buf, "HTTP/1.1 ", 9)) || !(*result = atoi(buf+9))) {
72                 vpninfo->progress(vpninfo, PRG_ERR, "Failed to parse HTTP response '%s'\n", buf);
73                 return -EINVAL;
74         }
75
76         vpninfo->progress(vpninfo, PRG_TRACE, "Got HTTP response: %s\n", buf);
77
78         /* Eat headers... */
79         while ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
80                 char *colon;
81
82                 vpninfo->progress(vpninfo, PRG_TRACE, "%s\n", buf);
83
84                 if (i < 0) {
85                         vpninfo->progress(vpninfo, PRG_ERR, "Error processing HTTP response\n");
86                         return -EINVAL;
87                 }
88                 colon = strchr(buf, ':');
89                 if (!colon) {
90                         vpninfo->progress(vpninfo, PRG_ERR, "Ignoring unknown HTTP response line '%s'\n", buf);
91                         continue;
92                 }
93                 *(colon++) = 0;
94                 if (*colon == ' ')
95                         colon++;
96
97                 if (!strcmp(buf, "Connection") && !strcmp(colon, "Close"))
98                         closeconn = 1;
99
100                 if (!strcmp(buf, "Location")) {
101                         vpninfo->redirect_url = strdup(colon);
102                         if (!vpninfo->redirect_url)
103                                 return -ENOMEM;
104                 }
105                 if (!strcmp(buf, "Content-Length")) {
106                         bodylen = atoi(colon);
107                         if (bodylen < 0 || bodylen > buf_len) {
108                                 vpninfo->progress(vpninfo, PRG_ERR, "Response body too large for buffer (%d > %d)\n",
109                                         bodylen, buf_len);
110                                 return -EINVAL;
111                         }
112                 }
113                 if (!strcmp(buf, "Set-Cookie")) {
114                         struct vpn_option *new, **this;
115                         char *semicolon = strchr(colon, ';');
116                         char *equals = strchr(colon, '=');
117
118                         if (semicolon)
119                                 *semicolon = 0;
120
121                         if (!equals) {
122                                 vpninfo->progress(vpninfo, PRG_ERR, "Invalid cookie offered: %s\n", buf);
123                                 return -EINVAL;
124                         }
125                         *(equals++) = 0;
126
127                         if (*equals) {
128                                 new = malloc(sizeof(*new));
129                                 if (!new) {
130                                         vpninfo->progress(vpninfo, PRG_ERR, "No memory for allocating cookies\n");
131                                         return -ENOMEM;
132                                 }
133                                 new->next = NULL;
134                                 new->option = strdup(colon);
135                                 new->value = strdup(equals);
136                         } else {
137                                 /* Kill cookie; don't replace it */
138                                 new = NULL;
139                         }
140                         for (this = &vpninfo->cookies; *this; this = &(*this)->next) {
141                                 if (!strcmp(colon, (*this)->option)) {
142                                         /* Replace existing cookie */
143                                         if (new)
144                                                 new->next = (*this)->next;
145                                         else
146                                                 new = (*this)->next;
147
148                                         free((*this)->option);
149                                         free((*this)->value);
150                                         free(*this);
151                                         *this = new;
152                                         break;
153                                 }
154                         }
155                         if (new && !*this) {
156                                 *this = new;
157                                 new->next = NULL;
158                         }
159                 }
160                 if (!strcmp(buf, "Transfer-Encoding")) {
161                         if (!strcmp(colon, "chunked"))
162                                 bodylen = -1;
163                         else {
164                                 vpninfo->progress(vpninfo, PRG_ERR, "Unknown Transfer-Encoding: %s\n", colon);
165                                 return -EINVAL;
166                         }
167                 }
168                 if (header_cb && !strncmp(buf, "X-", 2))
169                         header_cb(vpninfo, buf, colon);
170         }
171
172         /* Handle 'HTTP/1.1 100 Continue'. Not that we should ever see it */
173         if (*result == 100)
174                 goto cont;
175
176         /* Now the body, if there is one */
177         if (!bodylen)
178                 goto fin;
179
180         if (http10) {
181                 /* HTTP 1.0 response. Just eat all we can. */
182                 while (1) {
183                         i = SSL_read(vpninfo->https_ssl, body + done, bodylen - done);
184                         if (i < 0)
185                                 goto fin;
186                         done += i;
187                 }
188         }
189         /* If we were given Content-Length, it's nice and easy... */
190         if (bodylen > 0) {
191                 while (done < bodylen) {
192                         i = SSL_read(vpninfo->https_ssl, body + done, bodylen - done);
193                         if (i < 0) {
194                                 vpninfo->progress(vpninfo, PRG_ERR, "Error reading HTTP response body\n");
195                                 return -EINVAL;
196                         }
197                         done += i;
198                 }
199                 goto fin;
200         }
201
202         /* ... else, chunked */
203         while ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
204                 int chunklen, lastchunk = 0;
205
206                 if (i < 0) {
207                         vpninfo->progress(vpninfo, PRG_ERR, "Error fetching chunk header\n");
208                         exit(1);
209                 }
210                 chunklen = strtol(buf, NULL, 16);
211                 if (!chunklen) {
212                         lastchunk = 1;
213                         goto skip;
214                 }
215                 if (chunklen + done > buf_len) {
216                         vpninfo->progress(vpninfo, PRG_ERR, "Response body too large for buffer (%d > %d)\n",
217                                 chunklen + done, buf_len);
218                         return -EINVAL;
219                 }
220                 while (chunklen) {
221                         i = SSL_read(vpninfo->https_ssl, body + done, chunklen);
222                         if (i < 0) {
223                                 vpninfo->progress(vpninfo, PRG_ERR, "Error reading HTTP response body\n");
224                                 return -EINVAL;
225                         }
226                         chunklen -= i;
227                         done += i;
228                 }
229         skip:
230                 if ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
231                         if (i < 0) {
232                                 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching HTTP response body\n");
233                         } else {
234                                 vpninfo->progress(vpninfo, PRG_ERR, "Error in chunked decoding. Expected '', got: '%s'",
235                                         buf);
236                         }
237                         return -EINVAL;
238                 }
239
240                 if (lastchunk)
241                         break;
242         }
243  fin:
244         if (closeconn) {
245                 SSL_free(vpninfo->https_ssl);
246                 vpninfo->https_ssl = NULL;
247                 close(vpninfo->ssl_fd);
248                 vpninfo->ssl_fd = -1;
249         }
250         body[done] = 0;
251         return done;
252 }
253
254 static int fetch_config(struct openconnect_info *vpninfo, char *fu, char *bu,
255                         char *server_sha1)
256 {
257         struct vpn_option *opt;
258         char buf[MAX_BUF_LEN];
259         int result, buflen;
260         unsigned char local_sha1_bin[SHA_DIGEST_LENGTH];
261         char local_sha1_ascii[(SHA_DIGEST_LENGTH * 2)+1];
262         EVP_MD_CTX c;
263         int i;
264
265         sprintf(buf, "GET %s%s HTTP/1.1\r\n", fu, bu);
266         sprintf(buf + strlen(buf), "Host: %s\r\n", vpninfo->hostname);
267         sprintf(buf + strlen(buf),  "User-Agent: %s\r\n", vpninfo->useragent);
268         sprintf(buf + strlen(buf),  "Accept: */*\r\n");
269         sprintf(buf + strlen(buf),  "Accept-Encoding: identity\r\n");
270
271         if (vpninfo->cookies) {
272                 sprintf(buf + strlen(buf),  "Cookie: ");
273                 for (opt = vpninfo->cookies; opt; opt = opt->next)
274                         sprintf(buf + strlen(buf),  "%s=%s%s", opt->option,
275                                       opt->value, opt->next ? "; " : "\r\n");
276         }
277         sprintf(buf + strlen(buf),  "X-Transcend-Version: 1\r\n\r\n");
278
279         SSL_write(vpninfo->https_ssl, buf, strlen(buf));
280
281         buflen = process_http_response(vpninfo, &result, NULL, buf, MAX_BUF_LEN);
282         if (buflen < 0) {
283                 /* We'll already have complained about whatever offended us */
284                 return -EINVAL;
285         }
286
287         if (result != 200)
288                 return -EINVAL;
289
290
291         EVP_MD_CTX_init(&c);
292         EVP_Digest(buf, buflen, local_sha1_bin, NULL, EVP_sha1(), NULL);
293         EVP_MD_CTX_cleanup(&c);
294
295         for (i = 0; i < SHA_DIGEST_LENGTH; i++)
296                 sprintf(&local_sha1_ascii[i*2], "%02x", local_sha1_bin[i]);
297
298         if (strcasecmp(server_sha1, local_sha1_ascii)) {
299                 vpninfo->progress(vpninfo, PRG_ERR, "Downloaded config file did not match intended SHA1\n");
300                 return -EINVAL;
301         }
302
303         return vpninfo->write_new_config(vpninfo, buf, buflen);
304 }
305
306 static int run_csd_script(struct openconnect_info *vpninfo, char *buf, int buflen)
307 {
308         char fname[16];
309         int fd;
310
311         sprintf(fname, "/tmp/csdXXXXXX");
312         fd = mkstemp(fname);
313         if (fd < 0) {
314                 int err = -errno;
315                 vpninfo->progress(vpninfo, PRG_ERR, "Failed to open temporary CSD script file: %s\n",
316                                   strerror(errno));
317                 return err;
318         }
319         write(fd, buf, buflen);
320         fchmod(fd, 0700);
321         close(fd);
322
323         if (!fork()) {
324                 X509 *cert = SSL_get_peer_certificate(vpninfo->https_ssl);
325                 char certbuf[EVP_MAX_MD_SIZE * 2 + 1];
326                 char *csd_argv[32];
327                 int i = 0;
328
329                 csd_argv[i++] = fname;
330                 csd_argv[i++] = "-ticket";
331                 asprintf(&csd_argv[i++], "\"%s\"", vpninfo->csd_ticket);
332                 csd_argv[i++] = "-stub";
333                 csd_argv[i++] = "\"0\"";
334                 csd_argv[i++] = "-group";
335                 asprintf(&csd_argv[i++], "\"%s\"", vpninfo->authgroup?:"");
336
337                 if (0) {
338                         /* FIXME: This probably isn't the hash they wanted */
339                         get_cert_fingerprint(cert, certbuf);
340                         csd_argv[i++] = "-certhash";
341                         asprintf(&csd_argv[i++], "\"%s\"", certbuf);
342                 }
343
344                 csd_argv[i++] = "-url";
345                 asprintf(&csd_argv[i++], "\"https://%s%s\"", vpninfo->hostname, vpninfo->csd_starturl);
346                 /* WTF would it want to know this for? */
347                 csd_argv[i++] = "-vpnclient";
348                 csd_argv[i++] = "\"/opt/cisco/vpn/bin/vpnui";
349                 csd_argv[i++] = "-connect";
350                 asprintf(&csd_argv[i++], "https://%s/%s", vpninfo->hostname, vpninfo->csd_preurl);
351                 csd_argv[i++] = "-connectparam";
352                 asprintf(&csd_argv[i++], "#csdtoken=%s\"", vpninfo->csd_token);
353                 csd_argv[i++] = "-langselen";
354                         
355                 csd_argv[i++] = NULL;
356
357                 execv(fname, csd_argv);
358                 vpninfo->progress(vpninfo, PRG_ERR, "Failed to exec CSD script %s\n", fname);
359                 exit(1);
360         }
361
362         free(vpninfo->csd_stuburl);
363         vpninfo->csd_stuburl = NULL;
364         vpninfo->urlpath = strdup(vpninfo->csd_waiturl +
365                                   (vpninfo->csd_waiturl[0] == '/' ? 1 : 0));
366         vpninfo->csd_waiturl = NULL;
367         vpninfo->csd_scriptname = strdup(fname);
368         return 0;
369 }
370
371 /* Return value:
372  *  < 0, on error
373  *  = 0, no cookie (user cancel)
374  *  = 1, obtained cookie
375  */
376 int openconnect_obtain_cookie(struct openconnect_info *vpninfo)
377 {
378         struct vpn_option *opt, *next;
379         char buf[MAX_BUF_LEN];
380         int result, buflen;
381         char request_body[2048];
382         char *request_body_type = NULL;
383         char *method = "GET";
384
385  retry:
386         if (!vpninfo->https_ssl && openconnect_open_https(vpninfo)) {
387                 vpninfo->progress(vpninfo, PRG_ERR, "Failed to open HTTPS connection to %s\n",
388                         vpninfo->hostname);
389                 return -EINVAL;
390         }
391
392         /*
393          * It would be nice to use cURL for this, but we really need to guarantee
394          * that we'll be using OpenSSL (for the TPM stuff), and it doesn't seem
395          * to have any way to let us provide our own socket read/write functions.
396          * We can only provide a socket _open_ function. Which would require having
397          * a socketpair() and servicing the "other" end of it.
398          *
399          * So we process the HTTP for ourselves...
400          */
401         sprintf(buf, "%s /%s HTTP/1.1\r\n", method, vpninfo->urlpath ?: "");
402         sprintf(buf + strlen(buf), "Host: %s\r\n", vpninfo->hostname);
403         sprintf(buf + strlen(buf),  "User-Agent: %s\r\n", vpninfo->useragent);
404         sprintf(buf + strlen(buf),  "Accept: */*\r\n");
405         sprintf(buf + strlen(buf),  "Accept-Encoding: identity\r\n");
406
407         if (vpninfo->cookies) {
408                 sprintf(buf + strlen(buf),  "Cookie: ");
409                 for (opt = vpninfo->cookies; opt; opt = opt->next)
410                         sprintf(buf + strlen(buf),  "%s=%s%s", opt->option,
411                                       opt->value, opt->next ? "; " : "\r\n");
412         }
413         if (request_body_type) {
414                 sprintf(buf + strlen(buf),  "Content-Type: %s\r\n",
415                               request_body_type);
416                 sprintf(buf + strlen(buf),  "Content-Length: %zd\r\n",
417                               strlen(request_body));
418         }
419         sprintf(buf + strlen(buf),  "X-Transcend-Version: 1\r\n\r\n");
420         if (request_body_type)
421                 sprintf(buf + strlen(buf), "%s", request_body);
422
423         vpninfo->progress(vpninfo, PRG_INFO, "%s %s/%s\n", method,
424                           vpninfo->hostname, vpninfo->urlpath ?: "");
425
426         SSL_write(vpninfo->https_ssl, buf, strlen(buf));
427
428         buflen = process_http_response(vpninfo, &result, NULL, buf, MAX_BUF_LEN);
429         if (buflen < 0) {
430                 /* We'll already have complained about whatever offended us */
431                 exit(1);
432         }
433
434         if (result != 200 && vpninfo->redirect_url) {
435         redirect:
436                 if (!strncmp(vpninfo->redirect_url, "https://", 8)) {
437                         /* New host. Tear down the existing connection and make a new one */
438                         char *host = vpninfo->redirect_url + 8;
439                         char *path = strchr(host, '/');
440
441                         free(vpninfo->urlpath);
442                         if (path) {
443                                 *(path++) = 0;
444                                 vpninfo->urlpath = strdup(path);
445                         } else
446                                 vpninfo->urlpath = NULL;
447
448                         if (strcmp(vpninfo->hostname, host)) {
449                                 free(vpninfo->hostname);
450                                 vpninfo->hostname = strdup(host);
451
452                                 /* Kill the existing connection, and a new one will happen */
453                                 SSL_free(vpninfo->https_ssl);
454                                 vpninfo->https_ssl = NULL;
455                                 close(vpninfo->ssl_fd);
456                                 vpninfo->ssl_fd = -1;
457
458                                 for (opt = vpninfo->cookies; opt; opt = next) {
459                                         next = opt->next;
460
461                                         free(opt->option);
462                                         free(opt->value);
463                                         free(opt);
464                                 }
465                                 vpninfo->cookies = NULL;
466                         }
467                         free(vpninfo->redirect_url);
468                         vpninfo->redirect_url = NULL;
469
470                         goto retry;
471                 } else if (vpninfo->redirect_url[0] == '/') {
472                         /* Absolute redirect within same host */
473                         free(vpninfo->urlpath);
474                         vpninfo->urlpath = strdup(vpninfo->redirect_url + 1);
475                         free(vpninfo->redirect_url);
476                         vpninfo->redirect_url = NULL;
477                         goto retry;
478                 } else {
479                         vpninfo->progress(vpninfo, PRG_ERR, "Relative redirect (to '%s') not supported\n",
480                                 vpninfo->redirect_url);
481                         return -EINVAL;
482                 }
483         }
484
485         if (vpninfo->csd_stuburl) {
486                 /* This is the CSD stub script, which we now need to run */
487                 result = run_csd_script(vpninfo, buf, buflen);
488                 if (result)
489                         return result;
490
491                 /* Now we'll be redirected to the waiturl */
492                 goto retry;
493         }
494         if (strncmp(buf, "<?xml", 5)) {
495                 /* Not XML? Perhaps it's HTML with a refresh... */
496                 if (strcasestr(buf, "http-equiv=\"refresh\"")) {
497                         vpninfo->progress(vpninfo, PRG_INFO, "Refreshing %s after 1 second...\n",
498                                           vpninfo->urlpath);
499                         sleep(1);
500                         goto retry;
501                 }
502                 vpninfo->progress(vpninfo, PRG_ERR, "Unknown response from server\n");
503                 return -EINVAL;
504         }
505         request_body[0] = 0;
506         result = parse_xml_response(vpninfo, buf, request_body, sizeof(request_body),
507                                     &method, &request_body_type);
508         if (!result)
509                 goto redirect;
510
511         if (result != 2)
512                 return result;
513         /* A return value of 2 means the XML form indicated
514            success. We _should_ have a cookie... */
515
516         for (opt = vpninfo->cookies; opt; opt = opt->next) {
517
518                 if (!strcmp(opt->option, "webvpn"))
519                         vpninfo->cookie = opt->value;
520                 else if (vpninfo->write_new_config && !strcmp(opt->option, "webvpnc")) {
521                         char *tok = opt->value;
522                         char *bu = NULL, *fu = NULL, *sha = NULL;
523
524                         do {
525                                 if (tok != opt->value)
526                                         *(tok++) = 0;
527
528                                 if (!strncmp(tok, "bu:", 3))
529                                         bu = tok + 3;
530                                 else if (!strncmp(tok, "fu:", 3))
531                                         fu = tok + 3;
532                                 else if (!strncmp(tok, "fh:", 3)) {
533                                         if (!strncasecmp(tok+3, vpninfo->xmlsha1,
534                                                          SHA_DIGEST_LENGTH * 2))
535                                                 break;
536                                         sha = tok + 3;
537                                 }
538                         } while ((tok = strchr(tok, '&')));
539
540                         if (bu && fu && sha)
541                                 fetch_config(vpninfo, bu, fu, sha);
542                 }
543         }
544         if (vpninfo->csd_scriptname) {
545                 unlink(vpninfo->csd_scriptname);
546                 free(vpninfo->csd_scriptname);
547                 vpninfo->csd_scriptname = NULL;
548         }
549         return 0;
550 }
551
552 char *openconnect_create_useragent(char *base)
553 {
554         char *uagent = malloc(strlen(base) + 1 + strlen(openconnect_version));
555         sprintf(uagent, "%s%s", base, openconnect_version);
556         return uagent;
557 }