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