auth-globalprotect.c 28 KB

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