Delete CSD script after authentication, use CSD only once
[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 #include <netdb.h>
27 #include <unistd.h>
28 #include <fcntl.h>
29 #include <time.h>
30 #include <string.h>
31 #include <ctype.h>
32 #include <sys/stat.h>
33
34 #include <openssl/ssl.h>
35 #include <openssl/err.h>
36 #include <openssl/engine.h>
37
38 #include "openconnect.h"
39
40 #define MAX_BUF_LEN 131072
41 /*
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.
47  */
48
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)
52 {
53         char buf[MAX_BUF_LEN];
54         int bodylen = 0;
55         int done = 0;
56         int http10 = 0, closeconn = 0;
57         int i;
58
59         if (openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)) < 0) {
60                 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching HTTPS response\n");
61                 return -EINVAL;
62         }
63
64  cont:
65         if (!strncmp(buf, "HTTP/1.0 ", 9)) {
66                 http10 = 1;
67                 closeconn = 1;
68         }
69
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);
72                 return -EINVAL;
73         }
74
75         vpninfo->progress(vpninfo, PRG_TRACE, "Got HTTP response: %s\n", buf);
76
77         /* Eat headers... */
78         while ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
79                 char *colon;
80
81                 vpninfo->progress(vpninfo, PRG_TRACE, "%s\n", buf);
82
83                 if (i < 0) {
84                         vpninfo->progress(vpninfo, PRG_ERR, "Error processing HTTP response\n");
85                         return -EINVAL;
86                 }
87                 colon = strchr(buf, ':');
88                 if (!colon) {
89                         vpninfo->progress(vpninfo, PRG_ERR, "Ignoring unknown HTTP response line '%s'\n", buf);
90                         continue;
91                 }
92                 *(colon++) = 0;
93                 if (*colon == ' ')
94                         colon++;
95
96                 if (!strcmp(buf, "Connection") && !strcmp(colon, "Close"))
97                         closeconn = 1;
98
99                 if (!strcmp(buf, "Location")) {
100                         vpninfo->redirect_url = strdup(colon);
101                         if (!vpninfo->redirect_url)
102                                 return -ENOMEM;
103                 }
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",
108                                         bodylen, buf_len);
109                                 return -EINVAL;
110                         }
111                 }
112                 if (!strcmp(buf, "Set-Cookie")) {
113                         struct vpn_option *new, **this;
114                         char *semicolon = strchr(colon, ';');
115                         char *equals = strchr(colon, '=');
116
117                         if (semicolon)
118                                 *semicolon = 0;
119
120                         if (!equals) {
121                                 vpninfo->progress(vpninfo, PRG_ERR, "Invalid cookie offered: %s\n", buf);
122                                 return -EINVAL;
123                         }
124                         *(equals++) = 0;
125
126                         if (*equals) {
127                                 new = malloc(sizeof(*new));
128                                 if (!new) {
129                                         vpninfo->progress(vpninfo, PRG_ERR, "No memory for allocating cookies\n");
130                                         return -ENOMEM;
131                                 }
132                                 new->next = NULL;
133                                 new->option = strdup(colon);
134                                 new->value = strdup(equals);
135                         } else {
136                                 /* Kill cookie; don't replace it */
137                                 new = NULL;
138                         }
139                         for (this = &vpninfo->cookies; *this; this = &(*this)->next) {
140                                 if (!strcmp(colon, (*this)->option)) {
141                                         /* Replace existing cookie */
142                                         if (new)
143                                                 new->next = (*this)->next;
144                                         else
145                                                 new = (*this)->next;
146
147                                         free((*this)->option);
148                                         free((*this)->value);
149                                         free(*this);
150                                         *this = new;
151                                         break;
152                                 }
153                         }
154                         if (new && !*this) {
155                                 *this = new;
156                                 new->next = NULL;
157                         }
158                 }
159                 if (!strcmp(buf, "Transfer-Encoding")) {
160                         if (!strcmp(colon, "chunked"))
161                                 bodylen = -1;
162                         else {
163                                 vpninfo->progress(vpninfo, PRG_ERR, "Unknown Transfer-Encoding: %s\n", colon);
164                                 return -EINVAL;
165                         }
166                 }
167                 if (header_cb && !strncmp(buf, "X-", 2))
168                         header_cb(vpninfo, buf, colon);
169         }
170
171         /* Handle 'HTTP/1.1 100 Continue'. Not that we should ever see it */
172         if (*result == 100)
173                 goto cont;
174
175         /* Now the body, if there is one */
176         if (!bodylen)
177                 goto fin;
178
179         if (http10) {
180                 /* HTTP 1.0 response. Just eat all we can. */
181                 while (1) {
182                         i = SSL_read(vpninfo->https_ssl, body + done, bodylen - done);
183                         if (i < 0)
184                                 goto fin;
185                         done += i;
186                 }
187         }
188         /* If we were given Content-Length, it's nice and easy... */
189         if (bodylen > 0) {
190                 while (done < bodylen) {
191                         i = SSL_read(vpninfo->https_ssl, body + done, bodylen - done);
192                         if (i < 0) {
193                                 vpninfo->progress(vpninfo, PRG_ERR, "Error reading HTTP response body\n");
194                                 return -EINVAL;
195                         }
196                         done += i;
197                 }
198                 goto fin;
199         }
200
201         /* ... else, chunked */
202         while ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
203                 int chunklen, lastchunk = 0;
204
205                 if (i < 0) {
206                         vpninfo->progress(vpninfo, PRG_ERR, "Error fetching chunk header\n");
207                         exit(1);
208                 }
209                 chunklen = strtol(buf, NULL, 16);
210                 if (!chunklen) {
211                         lastchunk = 1;
212                         goto skip;
213                 }
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);
217                         return -EINVAL;
218                 }
219                 while (chunklen) {
220                         i = SSL_read(vpninfo->https_ssl, body + done, chunklen);
221                         if (i < 0) {
222                                 vpninfo->progress(vpninfo, PRG_ERR, "Error reading HTTP response body\n");
223                                 return -EINVAL;
224                         }
225                         chunklen -= i;
226                         done += i;
227                 }
228         skip:
229                 if ((i = openconnect_SSL_gets(vpninfo->https_ssl, buf, sizeof(buf)))) {
230                         if (i < 0) {
231                                 vpninfo->progress(vpninfo, PRG_ERR, "Error fetching HTTP response body\n");
232                         } else {
233                                 vpninfo->progress(vpninfo, PRG_ERR, "Error in chunked decoding. Expected '', got: '%s'",
234                                         buf);
235                         }
236                         return -EINVAL;
237                 }
238
239                 if (lastchunk)
240                         break;
241         }
242  fin:
243         if (closeconn) {
244                 SSL_free(vpninfo->https_ssl);
245                 vpninfo->https_ssl = NULL;
246                 close(vpninfo->ssl_fd);
247                 vpninfo->ssl_fd = -1;
248         }
249         body[done] = 0;
250         return done;
251 }
252
253 static int fetch_config(struct openconnect_info *vpninfo, char *fu, char *bu,
254                         char *server_sha1)
255 {
256         struct vpn_option *opt;
257         char buf[MAX_BUF_LEN];
258         int result, buflen;
259         unsigned char local_sha1_bin[SHA_DIGEST_LENGTH];
260         char local_sha1_ascii[(SHA_DIGEST_LENGTH * 2)+1];
261         EVP_MD_CTX c;
262         int i;
263
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");
269
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");
275         }
276         sprintf(buf + strlen(buf),  "X-Transcend-Version: 1\r\n\r\n");
277
278         SSL_write(vpninfo->https_ssl, buf, strlen(buf));
279
280         buflen = process_http_response(vpninfo, &result, NULL, buf, MAX_BUF_LEN);
281         if (buflen < 0) {
282                 /* We'll already have complained about whatever offended us */
283                 return -EINVAL;
284         }
285
286         if (result != 200)
287                 return -EINVAL;
288
289
290         EVP_MD_CTX_init(&c);
291         EVP_Digest(buf, buflen, local_sha1_bin, NULL, EVP_sha1(), NULL);
292         EVP_MD_CTX_cleanup(&c);
293
294         for (i = 0; i < SHA_DIGEST_LENGTH; i++)
295                 sprintf(&local_sha1_ascii[i*2], "%02x", local_sha1_bin[i]);
296
297         if (strcasecmp(server_sha1, local_sha1_ascii)) {
298                 vpninfo->progress(vpninfo, PRG_ERR, "Downloaded config file did not match intended SHA1\n");
299                 return -EINVAL;
300         }
301
302         return vpninfo->write_new_config(vpninfo, buf, buflen);
303 }
304
305 static int run_csd_script(struct openconnect_info *vpninfo, char *buf, int buflen)
306 {
307         char fname[16];
308         int fd;
309
310         sprintf(fname, "/tmp/csdXXXXXX");
311         fd = mkstemp(fname);
312         if (fd < 0) {
313                 int err = -errno;
314                 vpninfo->progress(vpninfo, PRG_ERR, "Failed to open temporary CSD script file: %s\n",
315                                   strerror(errno));
316                 return err;
317         }
318         write(fd, buf, buflen);
319         fchmod(fd, 0700);
320         close(fd);
321         if (!fork()) {
322                 /* FIXME: Add whatever arguments we need */
323                 system(fname);
324                 vpninfo->progress(vpninfo, PRG_ERR, "Failed to exec CSD script %s\n", fname);
325         }
326         /* FIXME: Remember the filename so we can delete it later */
327
328         free(vpninfo->csd_stuburl);
329         vpninfo->csd_stuburl = NULL;
330         vpninfo->urlpath = strdup(vpninfo->csd_waiturl +
331                                   (vpninfo->csd_waiturl[0] == '/' ? 1 : 0));
332         vpninfo->csd_waiturl = NULL;
333         vpninfo->csd_scriptname = strdup(fname);
334         return 0;
335 }
336
337 /* Return value:
338  *  < 0, on error
339  *  = 0, no cookie (user cancel)
340  *  = 1, obtained cookie
341  */
342 int openconnect_obtain_cookie(struct openconnect_info *vpninfo)
343 {
344         struct vpn_option *opt, *next;
345         char buf[MAX_BUF_LEN];
346         int result, buflen;
347         char request_body[2048];
348         char *request_body_type = NULL;
349         char *method = "GET";
350
351  retry:
352         if (!vpninfo->https_ssl && openconnect_open_https(vpninfo)) {
353                 vpninfo->progress(vpninfo, PRG_ERR, "Failed to open HTTPS connection to %s\n",
354                         vpninfo->hostname);
355                 return -EINVAL;
356         }
357
358         /*
359          * It would be nice to use cURL for this, but we really need to guarantee
360          * that we'll be using OpenSSL (for the TPM stuff), and it doesn't seem
361          * to have any way to let us provide our own socket read/write functions.
362          * We can only provide a socket _open_ function. Which would require having
363          * a socketpair() and servicing the "other" end of it.
364          *
365          * So we process the HTTP for ourselves...
366          */
367         sprintf(buf, "%s /%s HTTP/1.1\r\n", method, vpninfo->urlpath ?: "");
368         sprintf(buf + strlen(buf), "Host: %s\r\n", vpninfo->hostname);
369         sprintf(buf + strlen(buf),  "User-Agent: %s\r\n", vpninfo->useragent);
370         sprintf(buf + strlen(buf),  "Accept: */*\r\n");
371         sprintf(buf + strlen(buf),  "Accept-Encoding: identity\r\n");
372
373         if (vpninfo->cookies) {
374                 sprintf(buf + strlen(buf),  "Cookie: ");
375                 for (opt = vpninfo->cookies; opt; opt = opt->next)
376                         sprintf(buf + strlen(buf),  "%s=%s%s", opt->option,
377                                       opt->value, opt->next ? "; " : "\r\n");
378         }
379         if (request_body_type) {
380                 sprintf(buf + strlen(buf),  "Content-Type: %s\r\n",
381                               request_body_type);
382                 sprintf(buf + strlen(buf),  "Content-Length: %zd\r\n",
383                               strlen(request_body));
384         }
385         sprintf(buf + strlen(buf),  "X-Transcend-Version: 1\r\n\r\n");
386         if (request_body_type)
387                 sprintf(buf + strlen(buf), "%s", request_body);
388
389         vpninfo->progress(vpninfo, PRG_INFO, "%s %s/%s\n", method,
390                           vpninfo->hostname, vpninfo->urlpath ?: "");
391
392         SSL_write(vpninfo->https_ssl, buf, strlen(buf));
393
394         buflen = process_http_response(vpninfo, &result, NULL, buf, MAX_BUF_LEN);
395         if (buflen < 0) {
396                 /* We'll already have complained about whatever offended us */
397                 exit(1);
398         }
399
400         if (result != 200 && vpninfo->redirect_url) {
401                 if (!strncmp(vpninfo->redirect_url, "https://", 8)) {
402                         /* New host. Tear down the existing connection and make a new one */
403                         char *host = vpninfo->redirect_url + 8;
404                         char *path = strchr(host, '/');
405
406                         free(vpninfo->urlpath);
407                         if (path) {
408                                 *(path++) = 0;
409                                 vpninfo->urlpath = strdup(path);
410                         } else
411                                 vpninfo->urlpath = NULL;
412
413                         if (strcmp(vpninfo->hostname, host)) {
414                                 free(vpninfo->hostname);
415                                 vpninfo->hostname = strdup(host);
416
417                                 /* Kill the existing connection, and a new one will happen */
418                                 SSL_free(vpninfo->https_ssl);
419                                 vpninfo->https_ssl = NULL;
420                                 close(vpninfo->ssl_fd);
421                                 vpninfo->ssl_fd = -1;
422
423                                 for (opt = vpninfo->cookies; opt; opt = next) {
424                                         next = opt->next;
425
426                                         free(opt->option);
427                                         free(opt->value);
428                                         free(opt);
429                                 }
430                                 vpninfo->cookies = NULL;
431                         }
432                         free(vpninfo->redirect_url);
433                         vpninfo->redirect_url = NULL;
434
435                         goto retry;
436                 } else if (vpninfo->redirect_url[0] == '/') {
437                         /* Absolute redirect within same host */
438                         free(vpninfo->urlpath);
439                         vpninfo->urlpath = strdup(vpninfo->redirect_url + 1);
440                         free(vpninfo->redirect_url);
441                         vpninfo->redirect_url = NULL;
442                         goto retry;
443                 } else {
444                         vpninfo->progress(vpninfo, PRG_ERR, "Relative redirect (to '%s') not supported\n",
445                                 vpninfo->redirect_url);
446                         return -EINVAL;
447                 }
448         }
449
450         if (vpninfo->csd_stuburl) {
451                 /* This is the CSD stub script, which we now need to run */
452                 result = run_csd_script(vpninfo, buf, buflen);
453                 if (result)
454                         return result;
455
456                 /* Now we'll be redirected to the waiturl */
457                 goto retry;
458         }
459                 
460         request_body[0] = 0;
461         result = parse_xml_response(vpninfo, buf, request_body, sizeof(request_body),
462                                     &method, &request_body_type);
463         if (!result)
464                 goto retry;
465
466         if (result != 2)
467                 return result;
468         /* A return value of 2 means the XML form indicated
469            success. We _should_ have a cookie... */
470
471         for (opt = vpninfo->cookies; opt; opt = opt->next) {
472
473                 if (!strcmp(opt->option, "webvpn"))
474                         vpninfo->cookie = opt->value;
475                 else if (vpninfo->write_new_config && !strcmp(opt->option, "webvpnc")) {
476                         char *tok = opt->value;
477                         char *bu = NULL, *fu = NULL, *sha = NULL;
478
479                         do {
480                                 if (tok != opt->value)
481                                         *(tok++) = 0;
482
483                                 if (!strncmp(tok, "bu:", 3))
484                                         bu = tok + 3;
485                                 else if (!strncmp(tok, "fu:", 3))
486                                         fu = tok + 3;
487                                 else if (!strncmp(tok, "fh:", 3)) {
488                                         if (!strncasecmp(tok+3, vpninfo->xmlsha1,
489                                                          SHA_DIGEST_LENGTH * 2))
490                                                 break;
491                                         sha = tok + 3;
492                                 }
493                         } while ((tok = strchr(tok, '&')));
494
495                         if (bu && fu && sha)
496                                 fetch_config(vpninfo, bu, fu, sha);
497                 }
498         }
499         if (vpninfo->csd_scriptname) {
500                 unlink(vpninfo->csd_scriptname);
501                 free(vpninfo->csd_scriptname);
502                 vpninfo->csd_scriptname = NULL;
503         }
504         return 0;
505 }
506
507 char *openconnect_create_useragent(char *base)
508 {
509         char *uagent = malloc(strlen(base) + 1 + strlen(openconnect_version));
510         sprintf(uagent, "%s%s", base, openconnect_version);
511         return uagent;
512 }