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