1 /*
2  * OpenConnect (SSL + DTLS) VPN client
3  *
4  * Copyright © 2016-2018 Daniel Lenski
5  *
6  * Author: Dan Lenski <dlenski@gmail.com>
7  *
8  * This program is free software; you can redistribute it and/or
9  * modify it under the terms of the GNU Lesser General Public License
10  * version 2.1, as published by the Free Software Foundation.
11  *
12  * This program is distributed in the hope that it will be useful, but
13  * WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
15  * Lesser General Public License for more details.
16  */
17 
18 #include <config.h>
19 
20 #include <ctype.h>
21 #include <errno.h>
22 
23 #include <libxml/parser.h>
24 #include <libxml/tree.h>
25 
26 #include "openconnect-internal.h"
27 
28 struct login_context {
29 	char *username;				/* Username that has already succeeded in some form */
30 	char *alt_secret;			/* Alternative secret (DO NOT FREE) */
31 	struct oc_auth_form *form;
32 };
33 
gpst_common_headers(struct openconnect_info * vpninfo,struct oc_text_buf * buf)34 void gpst_common_headers(struct openconnect_info *vpninfo,
35 			 struct oc_text_buf *buf)
36 {
37 	char *orig_ua = vpninfo->useragent;
38 
39 	/* XX: more recent servers don't appear to require this specific UA value,
40 	 * but we don't have any good way to detect them.
41 	 */
42 	vpninfo->useragent = (char *)"PAN GlobalProtect";
43 	http_common_headers(vpninfo, buf);
44 	vpninfo->useragent = orig_ua;
45 }
46 
47 /* Translate platform names (derived from AnyConnect) into the values
48  * known to be emitted by GlobalProtect clients.
49  */
gpst_os_name(struct openconnect_info * vpninfo)50 const char *gpst_os_name(struct openconnect_info *vpninfo)
51 {
52 	if (!strcmp(vpninfo->platname, "mac-intel") || !strcmp(vpninfo->platname, "apple-ios"))
53 		return "Mac";
54 	else if (!strcmp(vpninfo->platname, "linux-64") || !strcmp(vpninfo->platname, "linux") || !strcmp(vpninfo->platname, "android"))
55 		return "Linux";
56 	else
57 		return "Windows";
58 }
59 
60 
61 /* Parse pre-login response ({POST,GET} /{global-protect,ssl-vpn}/pre-login.esp)
62  *
63  * Extracts the relevant arguments from the XML (username-label, password-label)
64  * and uses them to build an auth form, which always has 2-3 fields:
65  *
66  *   1) username (hidden in challenge forms, since it's simply repeated)
67  *   2) one secret value:
68  *       - normal account password
69  *       - "challenge" (2FA) password
70  *       - cookie from external authentication flow ("alternative secret" INSTEAD OF password)
71  *   3) inputStr for challenge form (shoehorned into form->action)
72  *
73  */
parse_prelogin_xml(struct openconnect_info * vpninfo,xmlNode * xml_node,void * cb_data)74 static int parse_prelogin_xml(struct openconnect_info *vpninfo, xmlNode *xml_node, void *cb_data)
75 {
76 	struct login_context *ctx = cb_data;
77 	struct oc_auth_form *form = ctx->form;
78 	struct oc_form_opt *opt, *opt2;
79 	char *prompt = NULL, *username_label = NULL, *password_label = NULL;
80 	char *saml_method = NULL, *saml_path = NULL;
81 	int result = 0;
82 
83 	if (!xmlnode_is_named(xml_node, "prelogin-response"))
84 		goto out;
85 
86 	for (xml_node = xml_node->children; xml_node; xml_node = xml_node->next) {
87 		char *s = NULL;
88 		if (!xmlnode_get_val(xml_node, "saml-request", &s)) {
89 			int len;
90 			free(saml_path);
91 			saml_path = openconnect_base64_decode(&len, s);
92 			if (len < 0) {
93 				vpn_progress(vpninfo, PRG_ERR, "Could not decode SAML request as base64: %s\n", s);
94 				free(s);
95 				result = -EINVAL;
96 				goto out;
97 			}
98 			free(s);
99 			realloc_inplace(saml_path, len+1);
100 			if (!saml_path) {
101 				result = -ENOMEM;
102 				goto out;
103 			}
104 			saml_path[len] = '\0';
105 		} else {
106 			xmlnode_get_val(xml_node, "saml-auth-method", &saml_method);
107 			xmlnode_get_val(xml_node, "authentication-message", &prompt);
108 			xmlnode_get_val(xml_node, "username-label", &username_label);
109 			xmlnode_get_val(xml_node, "password-label", &password_label);
110 			/* XX: should we save the certificate username from <ccusername/> ? */
111 		}
112 	}
113 
114 	/* XX: Alt-secret form field must be specified for SAML, because we can't autodetect it */
115 	if (saml_method || saml_path) {
116 		if (!ctx->alt_secret) {
117 			if (saml_method && !strcmp(saml_method, "REDIRECT"))
118 				vpn_progress(vpninfo, PRG_ERR,
119 				             _("SAML %s authentication is required via %s\n"),
120 				             saml_method, saml_path);
121 			else
122 				vpn_progress(vpninfo, PRG_ERR,
123 				             _("SAML %s authentication is required via external script.\n"),
124 				             saml_method);
125 			vpn_progress(vpninfo, PRG_ERR,
126 			             _("When SAML authentication is complete, specify destination form field by appending :field_name to login URL.\n"));
127 			result = -EINVAL;
128 			goto out;
129 		} else
130 			vpn_progress(vpninfo, PRG_DEBUG, _("Destination form field %s was specified; assuming SAML %s authentication is complete.\n"),
131 			             saml_method, ctx->alt_secret);
132 	}
133 
134 	/* Replace old form */
135 	free_auth_form(ctx->form);
136 	form = ctx->form = calloc(1, sizeof(*form));
137 	if (!form) {
138 	nomem:
139 		free_auth_form(form);
140 		result = -ENOMEM;
141 		goto out;
142 	}
143 	form->message = prompt ? : strdup(_("Please enter your username and password"));
144 	prompt = NULL;
145 	form->auth_id = strdup("_login");
146 
147 	/* First field (username) */
148 	opt = form->opts = calloc(1, sizeof(*opt));
149 	if (!opt)
150 		goto nomem;
151 	opt->name = strdup("user");
152 	if (asprintf(&opt->label, "%s: ", username_label ? : _("Username")) == 0)
153 		goto nomem;
154 	if (!ctx->username)
155 		opt->type = OC_FORM_OPT_TEXT;
156 	else {
157 		opt->type = OC_FORM_OPT_HIDDEN;
158 		opt->_value = ctx->username;
159 		ctx->username = NULL;
160 	}
161 
162 	/* Second field (secret) */
163 	opt2 = opt->next = calloc(1, sizeof(*opt));
164 	if (!opt2)
165 		goto nomem;
166 	opt2->name = strdup(ctx->alt_secret ? : "passwd");
167 	if (asprintf(&opt2->label, "%s: ", ctx->alt_secret ? : password_label ? : _("Password")) == 0)
168 		goto nomem;
169 
170 	/* XX: Some VPNs use a password in the first form, followed by a
171 	 * a token in the second ("challenge") form. Others use only a
172 	 * token. How can we distinguish these?
173 	 *
174 	 * Currently using the heuristic that a non-default label for the
175 	 * password in the first form means we should treat the first
176 	 * form's password as a token field.
177 	 */
178 	if (!can_gen_tokencode(vpninfo, form, opt2) && !ctx->alt_secret
179 	    && password_label && strcmp(password_label, "Password"))
180 		opt2->type = OC_FORM_OPT_TOKEN;
181 	else
182 		opt2->type = OC_FORM_OPT_PASSWORD;
183 
184 	vpn_progress(vpninfo, PRG_TRACE, "Prelogin form %s: \"%s\" %s(%s)=%s, \"%s\" %s(%s)\n",
185 	             form->auth_id,
186 	             opt->label, opt->name, opt->type == OC_FORM_OPT_TEXT ? "TEXT" : "HIDDEN", opt->_value,
187 	             opt2->label, opt2->name, opt2->type == OC_FORM_OPT_PASSWORD ? "PASSWORD" : "TOKEN");
188 
189 out:
190 	free(prompt);
191 	free(username_label);
192 	free(password_label);
193 	free(saml_method);
194 	free(saml_path);
195 	return result;
196 }
197 
198 /* Callback function to create a new form from a challenge
199  *
200  */
challenge_cb(struct openconnect_info * vpninfo,char * prompt,char * inputStr,void * cb_data)201 static int challenge_cb(struct openconnect_info *vpninfo, char *prompt, char *inputStr, void *cb_data)
202 {
203 	struct login_context *ctx = cb_data;
204 	struct oc_auth_form *form = ctx->form;
205 	struct oc_form_opt *opt = form->opts, *opt2 = form->opts->next;
206 
207 	/* Replace prompt, inputStr, and password prompt;
208 	 * clear password field, and make user field hidden.
209 	 */
210 	free(form->message);
211 	free(form->auth_id);
212 	free(form->action);
213 	free(opt2->label);
214 	free(opt2->_value);
215 	opt2->_value = NULL;
216 	opt->type = OC_FORM_OPT_HIDDEN;
217 
218 	/* XX: Some VPNs use a password in the first form, followed by a
219 	 * a token in the second ("challenge") form. Others use only a
220 	 * token. How can we distinguish these?
221 	 *
222 	 * Currently using the heuristic that if the password field in
223 	 * the preceding form wasn't treated as a token field, treat this
224 	 * as a token field.
225         */
226 	if (!can_gen_tokencode(vpninfo, form, opt2) && opt2->type == OC_FORM_OPT_PASSWORD)
227 		opt2->type = OC_FORM_OPT_TOKEN;
228 	else
229 		opt2->type = OC_FORM_OPT_PASSWORD;
230 
231 	if (    !(form->message = strdup(prompt))
232 		 || !(form->action = strdup(inputStr))
233 		 || !(form->auth_id = strdup("_challenge"))
234 		 || !(opt2->label = strdup(_("Challenge: "))) )
235 		return -ENOMEM;
236 
237 	vpn_progress(vpninfo, PRG_TRACE, "Challenge form %s: \"%s\" %s(%s)=%s, \"%s\" %s(%s), inputStr=%s\n",
238 	             form->auth_id,
239 	             opt->label, opt->name, opt->type == OC_FORM_OPT_TEXT ? "TEXT" : "HIDDEN", opt->_value,
240 	             opt2->label, opt2->name, opt2->type == OC_FORM_OPT_PASSWORD ? "PASSWORD" : "TOKEN",
241 	             inputStr);
242 
243 	return -EAGAIN;
244 }
245 
urldecode_inplace(char * p)246 static int urldecode_inplace(char *p)
247 {
248 	char *q;
249 	if (!p)
250 		return -EINVAL;
251 
252 	for (q = p; *p; p++, q++) {
253 		if (*p == '+') {
254 			*q = ' ';
255 		} else if (*p == '%' && isxdigit((int)(unsigned char)p[1]) &&
256 			   isxdigit((int)(unsigned char)p[2])) {
257 			*q = unhex(p + 1);
258 			p += 2;
259 		} else
260 			*q = *p;
261 	}
262 	*q = 0;
263 	return 0;
264 }
265 
266 /* Parse gateway login response (POST /ssl-vpn/login.esp)
267  *
268  * Extracts the relevant arguments from the XML (<jnlp><application-desc><argument>...</argument></application-desc></jnlp>)
269  * and uses them to build a query string fragment which is usable for subsequent requests.
270  * This query string fragement is saved as vpninfo->cookie.
271  *
272  */
273 struct gp_login_arg {
274 	const char *opt;
275 	unsigned save:1;
276 	unsigned show:1;
277 	unsigned warn_missing:1;
278 	unsigned err_missing:1;
279 	unsigned unknown:1;
280 	const char *check;
281 };
282 static const struct gp_login_arg gp_login_args[] = {
283 	{ .unknown=1 },                                 /* seemingly always empty */
284 	{ .opt="authcookie", .save=1, .err_missing=1 },
285 	{ .opt="persistent-cookie", .warn_missing=1 },  /* 40 hex digits; persists across sessions */
286 	{ .opt="portal", .save=1, .warn_missing=1 },
287 	{ .opt="user", .save=1, .err_missing=1 },
288 	{ .opt="authentication-source", .show=1 },      /* LDAP-auth, AUTH-RADIUS_RSA_OTP, etc. */
289 	{ .opt="configuration", .warn_missing=1 },      /* usually vsys1 (sometimes vsys2, etc.) */
290 	{ .opt="domain", .save=1, .warn_missing=1 },
291 	{ .unknown=1 },                                 /* 4 arguments, seemingly always empty */
292 	{ .unknown=1 },
293 	{ .unknown=1 },
294 	{ .unknown=1 },
295 	{ .opt="connection-type", .err_missing=1, .check="tunnel" },
296 	{ .opt="password-expiration-days", .show=1 },   /* days until password expires, if not -1 */
297 	{ .opt="clientVer", .err_missing=1, .check="4100" },
298 	{ .opt="preferred-ip", .save=1 },
299 	{ .opt="portal-userauthcookie", .show=1},
300 	{ .opt="portal-prelogonuserauthcookie", .show=1},
301 	{ .unknown=1 },
302 	{ .opt="usually-equals-4", .show=1 },           /* newer servers send "4" here, meaning unknown */
303 	{ .opt="usually-equals-unknown", .show=1 },     /* newer servers send "unknown" here */
304 };
305 static const int gp_login_nargs = (sizeof(gp_login_args)/sizeof(*gp_login_args));
306 
parse_login_xml(struct openconnect_info * vpninfo,xmlNode * xml_node,void * cb_data)307 static int parse_login_xml(struct openconnect_info *vpninfo, xmlNode *xml_node, void *cb_data)
308 {
309 	struct oc_text_buf *cookie = buf_alloc();
310 	char *value = NULL;
311 	const struct gp_login_arg *arg;
312 	int argn, unknown_args = 0, fatal_args = 0;
313 
314 	if (!xmlnode_is_named(xml_node, "jnlp"))
315 		goto err_out;
316 
317 	xml_node = xml_node->children;
318 	while (xml_node && xml_node->type != XML_ELEMENT_NODE)
319 		xml_node = xml_node->next;
320 
321 	if (!xml_node || !xmlnode_is_named(xml_node, "application-desc"))
322 		goto err_out;
323 
324 	xml_node = xml_node->children;
325 	/* XXX: Loop as long as there are EITHER more known arguments OR more XML tags,
326 	 * so that we catch both more-than-expected and fewer-than-expected arguments. */
327 	for (argn = 0; argn < gp_login_nargs || xml_node; argn++) {
328 		while (xml_node && xml_node->type != XML_ELEMENT_NODE)
329 			xml_node = xml_node->next;
330 
331 		if (!xml_node)
332 			value = NULL;
333 		else if (!xmlnode_get_val(xml_node, "argument", &value)) {
334 			if (value && (!value[0] || !strcmp(value, "(null)") || !strcmp(value, "-1"))) {
335 				free(value);
336 				value = NULL;
337 			} else {
338 				/* XX: The usage of URL encoding in the fields sent by GP servers here is
339 				 * inconsistent, but in particular the value "%28empty_domain%29" keeps popping up
340 				 * in places where the server expects "(empty_domain)" (like the stupidly redundant
341 				 * logout operation). So we do this to be safe and to ensure logout succeeds.
342 				 */
343 				urldecode_inplace(value);
344 			}
345 			xml_node = xml_node->next;
346 		} else
347 			goto err_out;
348 
349 		/* XX: argument 0 is unknown so we reuse this for extra arguments */
350 		arg = &gp_login_args[(argn < gp_login_nargs) ? argn : 0];
351 
352 		if (arg->unknown && value) {
353 			unknown_args++;
354 			vpn_progress(vpninfo, PRG_ERR,
355 						 _("GlobalProtect login returned unexpected argument value arg[%d]=%s\n"),
356 						 argn, value);
357 		} else if (arg->check && (!value || strcmp(value, arg->check))) {
358 			unknown_args++;
359 			fatal_args += arg->err_missing;
360             vpn_progress(vpninfo, PRG_ERR,
361 			             _("GlobalProtect login returned %s=%s (expected %s)\n"),
362 			             arg->opt, value, arg->check);
363 		} else if ((arg->err_missing || arg->warn_missing) && !value) {
364 			unknown_args++;
365 			fatal_args += arg->err_missing;
366 			vpn_progress(vpninfo, PRG_ERR,
367 			             _("GlobalProtect login returned empty or missing %s\n"),
368 			             arg->opt);
369 		} else if (value && arg->show) {
370 			vpn_progress(vpninfo, PRG_INFO,
371 			             _("GlobalProtect login returned %s=%s\n"),
372 			             arg->opt, value);
373 		}
374 
375 		if (value && arg->save)
376 			append_opt(cookie, arg->opt, value);
377 
378 		free(value);
379 		value = NULL;
380 	}
381 	append_opt(cookie, "computer", vpninfo->localname);
382 
383 	if (unknown_args)
384 		vpn_progress(vpninfo, PRG_ERR,
385 					 _("Please report %d unexpected values above (of which %d fatal) to <openconnect-devel@lists.infradead.org>\n"),
386 					 unknown_args, fatal_args);
387 	if (fatal_args) {
388 		buf_free(cookie);
389 		return -EPERM;
390 	}
391 
392 	if (!buf_error(cookie)) {
393 		vpninfo->cookie = cookie->data;
394 		cookie->data = NULL;
395 	}
396 	return buf_free(cookie);
397 
398 err_out:
399 	free(value);
400 	buf_free(cookie);
401 	return -EINVAL;
402 }
403 
404 /* Parse portal login/config response (POST /ssl-vpn/getconfig.esp)
405  *
406  * Extracts the list of gateways from the XML, writes them to the XML config,
407  * presents the user with a form to choose the gateway, and redirects
408  * to that gateway.
409  *
410  */
parse_portal_xml(struct openconnect_info * vpninfo,xmlNode * xml_node,void * cb_data)411 static int parse_portal_xml(struct openconnect_info *vpninfo, xmlNode *xml_node, void *cb_data)
412 {
413 	struct oc_auth_form *form;
414 	xmlNode *x, *x2, *x3, *gateways = NULL;
415 	struct oc_form_opt_select *opt;
416 	struct oc_text_buf *buf = NULL;
417 	int max_choices = 0, result;
418 	char *portal = NULL;
419 	char *hip_interval = NULL;
420 
421 	form = calloc(1, sizeof(*form));
422 	if (!form)
423 		return -ENOMEM;
424 
425 	form->message = strdup(_("Please select GlobalProtect gateway."));
426 	form->auth_id = strdup("_portal");
427 
428 	opt = form->authgroup_opt = calloc(1, sizeof(*opt));
429 	if (!opt) {
430 		result = -ENOMEM;
431 		goto out;
432 	}
433 	opt->form.type = OC_FORM_OPT_SELECT;
434 	opt->form.name = strdup("gateway");
435 	opt->form.label = strdup(_("GATEWAY:"));
436 	form->opts = (void *)opt;
437 
438 	/*
439 	 * The portal contains a ton of stuff, but basically none of it is
440 	 * useful to a VPN client that wishes to give control to the client
441 	 * user, as opposed to the VPN administrator.  The exceptions are the
442 	 * list of gateways in policy/gateways/external/list and the interval
443 	 * for HIP checks in policy/hip-collection/hip-report-interval
444 	 */
445 	if (xmlnode_is_named(xml_node, "policy")) {
446 		for (x = xml_node->children; x; x = x->next) {
447 			if (xmlnode_is_named(x, "gateways")) {
448 				for (x2 = x->children; x2; x2 = x2->next)
449 					if (xmlnode_is_named(x2, "external"))
450 						for (x3 = x2->children; x3; x3 = x3->next)
451 							if (xmlnode_is_named(x3, "list"))
452 							    gateways = x3;
453 			} else if (xmlnode_is_named(x, "hip-collection")) {
454 				for (x2 = x->children; x2; x2 = x2->next) {
455 					if (!xmlnode_get_val(x2, "hip-report-interval", &hip_interval)) {
456 						int sec = atoi(hip_interval);
457 						if (vpninfo->trojan_interval)
458 							vpn_progress(vpninfo, PRG_INFO, _("Ignoring portal's HIP report interval (%d minutes), because interval is already set to %d minutes.\n"),
459 										 sec/60, vpninfo->trojan_interval/60);
460 						else {
461 							vpninfo->trojan_interval = sec - 60;
462 							vpn_progress(vpninfo, PRG_INFO, _("Portal set HIP report interval to %d minutes).\n"),
463 										 sec/60);
464 						}
465 					}
466 				}
467 			} else
468 				xmlnode_get_val(x, "portal-name", &portal);
469 		}
470 	}
471 
472 	if (!gateways) {
473 		result = -EINVAL;
474 		goto out;
475 	}
476 
477 	if (vpninfo->write_new_config) {
478 		buf = buf_alloc();
479 		buf_append(buf, "<GPPortal>\n  <ServerList>\n");
480 		if (portal) {
481 			buf_append(buf, "      <HostEntry><HostName>");
482 			buf_append_xmlescaped(buf, portal);
483 			buf_append(buf, "</HostName><HostAddress>%s", vpninfo->hostname);
484 			if (vpninfo->port!=443)
485 				buf_append(buf, ":%d", vpninfo->port);
486 			buf_append(buf, "/global-protect</HostAddress></HostEntry>\n");
487 		}
488 	}
489 
490 	/* first, count the number of gateways */
491 	for (x = gateways->children; x; x = x->next)
492 		if (xmlnode_is_named(x, "entry"))
493 			max_choices++;
494 
495 	opt->choices = calloc(max_choices, sizeof(opt->choices[0]));
496 	if (!opt->choices) {
497 		result = -ENOMEM;
498 		goto out;
499 	}
500 
501 	/* each entry looks like <entry name="host[:443]"><description>Label</description></entry> */
502 	vpn_progress(vpninfo, PRG_INFO, _("%d gateway servers available:\n"), max_choices);
503 	for (x = gateways->children; x; x = x->next) {
504 		if (xmlnode_is_named(x, "entry")) {
505 			struct oc_choice *choice = calloc(1, sizeof(*choice));
506 			if (!choice) {
507 				result = -ENOMEM;
508 				goto out;
509 			}
510 
511 			xmlnode_get_prop(x, "name", &choice->name);
512 			for (x2 = x->children; x2; x2=x2->next)
513 				if (!xmlnode_get_val(x2, "description", &choice->label)) {
514 					if (vpninfo->write_new_config) {
515 						buf_append(buf, "      <HostEntry><HostName>");
516 						buf_append_xmlescaped(buf, choice->label);
517 						buf_append(buf, "</HostName><HostAddress>%s/ssl-vpn</HostAddress></HostEntry>\n",
518 								   choice->name);
519 					}
520 				}
521 
522 			opt->choices[opt->nr_choices++] = choice;
523 			vpn_progress(vpninfo, PRG_INFO, _("  %s (%s)\n"),
524 				     choice->label, choice->name);
525 		}
526 	}
527 	if (!opt->nr_choices) {
528 		vpn_progress(vpninfo, PRG_ERR,
529 					 _("GlobalProtect portal configuration lists no gateway servers.\n"));
530 		result = -EINVAL;
531 		goto out;
532 	}
533 	if (!vpninfo->authgroup && opt->nr_choices)
534 		vpninfo->authgroup = strdup(opt->choices[0]->name);
535 
536 	if (vpninfo->write_new_config) {
537 		buf_append(buf, "  </ServerList>\n</GPPortal>\n");
538 		if ((result = buf_error(buf)))
539 			goto out;
540 		if ((result = vpninfo->write_new_config(vpninfo->cbdata, buf->data, buf->pos)))
541 			goto out;
542 	}
543 
544 	/* process auth form to select gateway */
545 	result = process_auth_form(vpninfo, form);
546 	if (result == OC_FORM_RESULT_CANCELLED || result < 0)
547 		goto out;
548 
549 	/* redirect to the gateway (no-op if it's the same host) */
550 	free(vpninfo->redirect_url);
551 	if (asprintf(&vpninfo->redirect_url, "https://%s", vpninfo->authgroup) == 0) {
552 		result = -ENOMEM;
553 		goto out;
554 	}
555 	result = handle_redirect(vpninfo);
556 
557 out:
558 	buf_free(buf);
559 	free(portal);
560 	free(hip_interval);
561 	free_auth_form(form);
562 	return result;
563 }
564 
565 /* Main login entry point
566  *
567  * portal: 0 for gateway login, 1 for portal login
568  * alt_secret: "alternate secret" field (see new_auth_form)
569  *
570  */
gpst_login(struct openconnect_info * vpninfo,int portal,struct login_context * ctx)571 static int gpst_login(struct openconnect_info *vpninfo, int portal, struct login_context *ctx)
572 {
573 	int result, blind_retry = 0;
574 	struct oc_text_buf *request_body = buf_alloc();
575 	const char *request_body_type = "application/x-www-form-urlencoded";
576 	char *xml_buf = NULL, *orig_path;
577 
578 	/* Ask the user to fill in the auth form; repeat as necessary */
579 	for (;;) {
580 		/* submit prelogin request to get form */
581 		orig_path = vpninfo->urlpath;
582 		if (asprintf(&vpninfo->urlpath, "%s/prelogin.esp?tmp=tmp&clientVer=4100&clientos=%s",
583 			     portal ? "global-protect" : "ssl-vpn", gpst_os_name(vpninfo)) < 0) {
584 			result = -ENOMEM;
585 			goto out;
586 		}
587 		result = do_https_request(vpninfo, "POST", NULL, NULL, &xml_buf, 0);
588 		free(vpninfo->urlpath);
589 		vpninfo->urlpath = orig_path;
590 
591 		if (result >= 0)
592 			result = gpst_xml_or_error(vpninfo, xml_buf, parse_prelogin_xml, NULL, ctx);
593 		if (result)
594 			goto out;
595 
596 	got_form:
597 		/* process auth form */
598 		result = process_auth_form(vpninfo, ctx->form);
599 		if (result)
600 			goto out;
601 
602 	replay_form:
603 		/* generate token code if specified */
604 		result = do_gen_tokencode(vpninfo, ctx->form);
605 		if (result) {
606 			vpn_progress(vpninfo, PRG_ERR, _("Failed to generate OTP tokencode; disabling token\n"));
607 			vpninfo->token_bypassed = 1;
608 			goto out;
609 		}
610 
611 		/* submit gateway login (ssl-vpn/login.esp) or portal config (global-protect/getconfig.esp) request */
612 		buf_truncate(request_body);
613 		buf_append(request_body, "jnlpReady=jnlpReady&ok=Login&direct=yes&clientVer=4100&prot=https:");
614 		append_opt(request_body, "ipv6-support", vpninfo->disable_ipv6 ? "no" : "yes");
615 		append_opt(request_body, "clientos", gpst_os_name(vpninfo));
616 		append_opt(request_body, "os-version", vpninfo->platname);
617 		append_opt(request_body, "server", vpninfo->hostname);
618 		append_opt(request_body, "computer", vpninfo->localname);
619 		if (vpninfo->ip_info.addr)
620 			append_opt(request_body, "preferred-ip", vpninfo->ip_info.addr);
621 		if (ctx->form->action)
622 			append_opt(request_body, "inputStr", ctx->form->action);
623 		append_form_opts(vpninfo, ctx->form, request_body);
624 		if ((result = buf_error(request_body)))
625 			goto out;
626 
627 		orig_path = vpninfo->urlpath;
628 		vpninfo->urlpath = strdup(portal ? "global-protect/getconfig.esp" : "ssl-vpn/login.esp");
629 		result = do_https_request(vpninfo, "POST", request_body_type, request_body,
630 					  &xml_buf, 0);
631 		free(vpninfo->urlpath);
632 		vpninfo->urlpath = orig_path;
633 
634 		/* Result could be either a JavaScript challenge or XML */
635 		if (result >= 0)
636 			result = gpst_xml_or_error(vpninfo, xml_buf, portal ? parse_portal_xml : parse_login_xml,
637 									   challenge_cb, ctx);
638 		if (result == -EACCES) {
639 			/* Invalid username/password; reuse same form, but blank,
640 			 * unless we just did a blind retry.
641 			 */
642 			nuke_opt_values(ctx->form->opts);
643 			if (!blind_retry)
644 				goto got_form;
645 			else
646 				blind_retry = 0;
647 		} else {
648 			/* Save successful username */
649 			if (!ctx->username)
650 				ctx->username = strdup(ctx->form->opts->_value);
651 			if (result == -EAGAIN) {
652 				/* New form is already populated from the challenge */
653 				goto got_form;
654 			} else if (portal && result == 0) {
655 				/* Portal login succeeded; blindly retry same credentials on gateway,
656 				 * unless it was a challenge auth form or alt-secret form.
657 				 */
658 				portal = 0;
659 				if (ctx->form->auth_id[0] == '_' && !ctx->alt_secret) {
660 					blind_retry = 1;
661 					goto replay_form;
662 				}
663 			} else
664 			  break;
665 		}
666 	}
667 
668 out:
669 	buf_free(request_body);
670 	free(xml_buf);
671 	return result;
672 }
673 
gpst_obtain_cookie(struct openconnect_info * vpninfo)674 int gpst_obtain_cookie(struct openconnect_info *vpninfo)
675 {
676 	struct login_context ctx = { .username=NULL, .alt_secret=NULL, .form=NULL };
677 	int result;
678 
679 	/* An alternate password/secret field may be specified in the "URL path" (or --usergroup).
680         * Known possibilities are:
681 	 *     /portal:portal-userauthcookie
682 	 *     /gateway:prelogin-cookie
683 	 */
684 	if (vpninfo->urlpath
685 	    && (ctx.alt_secret = strrchr(vpninfo->urlpath, ':')) != NULL) {
686 		*(ctx.alt_secret) = '\0';
687 		ctx.alt_secret = strdup(ctx.alt_secret+1);
688 	}
689 
690 	if (vpninfo->urlpath && (!strcmp(vpninfo->urlpath, "portal") || !strncmp(vpninfo->urlpath, "global-protect", 14))) {
691 		/* assume the server is a portal */
692 		result = gpst_login(vpninfo, 1, &ctx);
693 	} else if (vpninfo->urlpath && (!strcmp(vpninfo->urlpath, "gateway") || !strncmp(vpninfo->urlpath, "ssl-vpn", 7))) {
694 		/* assume the server is a gateway */
695 		result = gpst_login(vpninfo, 0, &ctx);
696 	} else {
697 		/* first try handling it as a portal, then a gateway */
698 		result = gpst_login(vpninfo, 1, &ctx);
699 		if (result == -EEXIST) {
700 			result = gpst_login(vpninfo, 0, &ctx);
701 			if (result == -EEXIST)
702 				vpn_progress(vpninfo, PRG_ERR, _("Server is neither a GlobalProtect portal nor a gateway.\n"));
703 		}
704 	}
705 	free(ctx.username);
706 	free(ctx.alt_secret);
707 	free_auth_form(ctx.form);
708 	return result;
709 }
710 
gpst_bye(struct openconnect_info * vpninfo,const char * reason)711 int gpst_bye(struct openconnect_info *vpninfo, const char *reason)
712 {
713 	char *orig_path;
714 	int result;
715 	struct oc_text_buf *request_body = buf_alloc();
716 	const char *request_body_type = "application/x-www-form-urlencoded";
717 	const char *method = "POST";
718 	char *xml_buf = NULL;
719 
720 	/* In order to logout successfully, the client must send not only
721 	 * the session's authcookie, but also the portal, user, computer,
722 	 * and domain matching the values sent with the getconfig request.
723 	 *
724 	 * You read that right: the client must send a bunch of irrelevant
725 	 * non-secret values in its logout request. If they're wrong or
726 	 * missing, the logout will fail and the authcookie will remain
727 	 * valid -- which is a security hole.
728 	 *
729 	 * Don't blame me. I didn't design this.
730 	 */
731 	buf_append(request_body, "%s", vpninfo->cookie);
732 	if ((result = buf_error(request_body)))
733 		goto out;
734 
735 	/* We need to close and reopen the HTTPS connection (to kill
736 	 * the tunnel session) and submit a new HTTPS request to
737 	 * logout.
738 	 */
739 	orig_path = vpninfo->urlpath;
740 	vpninfo->urlpath = strdup("ssl-vpn/logout.esp");
741 	openconnect_close_https(vpninfo, 0);
742 	result = do_https_request(vpninfo, method, request_body_type, request_body,
743 				  &xml_buf, 0);
744 	free(vpninfo->urlpath);
745 	vpninfo->urlpath = orig_path;
746 
747 	/* logout.esp returns HTTP status 200 and <response status="success"> when
748 	 * successful, and all manner of malformed junk when unsuccessful.
749 	 */
750 	if (result >= 0)
751 		result = gpst_xml_or_error(vpninfo, xml_buf, NULL, NULL, NULL);
752 
753 	if (result < 0)
754 		vpn_progress(vpninfo, PRG_ERR, _("Logout failed.\n"));
755 	else
756 		vpn_progress(vpninfo, PRG_INFO, _("Logout successful.\n"));
757 
758 out:
759 	buf_free(request_body);
760 	free(xml_buf);
761 	return result;
762 }
763