auth-globalprotect.c 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848
  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. * user, as opposed to the VPN administrator. The exception is
  419. * the list of gateways in policy/gateways/external/list.
  420. *
  421. * There are other fields which are worthless in terms of end-user
  422. * functionality, but are needed for compliance with the server's
  423. * security policies:
  424. * - Interval for HIP checks in policy/hip-collection/hip-report-interval
  425. * (save so that we can rerun HIP on the expected interval)
  426. * - Software version (save so we can mindlessly parrot it back)
  427. *
  428. * Potentially also useful, but currently ignored:
  429. * - welcome-page/page, help-page, and help-page-2 contents might in
  430. * principle be informative, but in practice they're either empty
  431. * or extremely verbose multi-page boilerplate in HTML format
  432. * - hip-collection/default/category/member[] might be useful
  433. * to report to the user as a diagnostic, so that they know what
  434. * HIP report entries the server expects, if their HIP report
  435. * isn't expected. In practice, servers that actually check the HIP
  436. * report contents are so nitpicky that anything less than a
  437. * capture from an officially-supported client is unlikely to help.
  438. * - root-ca/entry[]/cert is potentially useful because it contains
  439. * certs that we should allow as root-of-trust for the gateway
  440. * servers. This could prevent users from having to specify --cafile
  441. * or repeated --servercert in order to allow non-interactive
  442. * authentication to gateways whose certs aren't trusted by the
  443. * system but ARE trusted by the portal (see example at
  444. * https://github.com/dlenski/openconnect/issues/128).
  445. */
  446. if (xmlnode_is_named(xml_node, "policy")) {
  447. for (x = xml_node->children; x; x = x->next) {
  448. if (xmlnode_is_named(x, "gateways")) {
  449. for (x2 = x->children; x2; x2 = x2->next)
  450. if (xmlnode_is_named(x2, "external"))
  451. for (x3 = x2->children; x3; x3 = x3->next)
  452. if (xmlnode_is_named(x3, "list"))
  453. gateways = x3;
  454. } else if (xmlnode_is_named(x, "hip-collection")) {
  455. for (x2 = x->children; x2; x2 = x2->next) {
  456. if (!xmlnode_get_val(x2, "hip-report-interval", &hip_interval)) {
  457. int sec = atoi(hip_interval);
  458. if (vpninfo->trojan_interval)
  459. vpn_progress(vpninfo, PRG_INFO, _("Ignoring portal's HIP report interval (%d minutes), because interval is already set to %d minutes.\n"),
  460. sec/60, vpninfo->trojan_interval/60);
  461. else {
  462. vpninfo->trojan_interval = sec - 60;
  463. vpn_progress(vpninfo, PRG_INFO, _("Portal set HIP report interval to %d minutes).\n"),
  464. sec/60);
  465. }
  466. }
  467. }
  468. } else {
  469. xmlnode_get_val(x, "portal-name", &portal);
  470. if (!xmlnode_get_val(x, "portal-userauthcookie", &ctx->portal_userauthcookie)) {
  471. if (!*ctx->portal_userauthcookie || !strcmp(ctx->portal_userauthcookie, "empty")) {
  472. free(ctx->portal_userauthcookie);
  473. ctx->portal_userauthcookie = NULL;
  474. }
  475. }
  476. if (!xmlnode_get_val(x, "portal-prelogonuserauthcookie", &ctx->portal_prelogonuserauthcookie)) {
  477. if (!*ctx->portal_prelogonuserauthcookie || !strcmp(ctx->portal_prelogonuserauthcookie, "empty")) {
  478. free(ctx->portal_prelogonuserauthcookie);
  479. ctx->portal_prelogonuserauthcookie = NULL;
  480. }
  481. }
  482. }
  483. }
  484. }
  485. if (!gateways) {
  486. no_gateways:
  487. vpn_progress(vpninfo, PRG_ERR,
  488. _("GlobalProtect portal configuration lists no gateway servers.\n"));
  489. result = -EINVAL;
  490. goto out;
  491. }
  492. if (vpninfo->write_new_config) {
  493. buf = buf_alloc();
  494. buf_append(buf, "<GPPortal>\n <ServerList>\n");
  495. if (portal) {
  496. buf_append(buf, " <HostEntry><HostName>");
  497. buf_append_xmlescaped(buf, portal);
  498. buf_append(buf, "</HostName><HostAddress>%s", vpninfo->hostname);
  499. if (vpninfo->port!=443)
  500. buf_append(buf, ":%d", vpninfo->port);
  501. buf_append(buf, "/global-protect</HostAddress></HostEntry>\n");
  502. }
  503. }
  504. /* first, count the number of gateways */
  505. for (x = gateways->children; x; x = x->next)
  506. if (xmlnode_is_named(x, "entry"))
  507. max_choices++;
  508. opt->choices = calloc(max_choices, sizeof(opt->choices[0]));
  509. if (!opt->choices) {
  510. result = -ENOMEM;
  511. goto out;
  512. }
  513. /* each entry looks like <entry name="host[:443]"><description>Label</description></entry> */
  514. vpn_progress(vpninfo, PRG_INFO, _("%d gateway servers available:\n"), max_choices);
  515. for (x = gateways->children; x; x = x->next) {
  516. if (xmlnode_is_named(x, "entry")) {
  517. struct oc_choice *choice = calloc(1, sizeof(*choice));
  518. if (!choice) {
  519. result = -ENOMEM;
  520. goto out;
  521. }
  522. xmlnode_get_prop(x, "name", &choice->name);
  523. for (x2 = x->children; x2; x2=x2->next)
  524. if (!xmlnode_get_val(x2, "description", &choice->label)) {
  525. if (vpninfo->write_new_config) {
  526. buf_append(buf, " <HostEntry><HostName>");
  527. buf_append_xmlescaped(buf, choice->label);
  528. buf_append(buf, "</HostName><HostAddress>%s/ssl-vpn</HostAddress></HostEntry>\n",
  529. choice->name);
  530. }
  531. }
  532. opt->choices[opt->nr_choices++] = choice;
  533. vpn_progress(vpninfo, PRG_INFO, _(" %s (%s)\n"),
  534. choice->label, choice->name);
  535. }
  536. }
  537. if (!opt->nr_choices)
  538. goto no_gateways;
  539. if (!vpninfo->authgroup && opt->nr_choices)
  540. vpninfo->authgroup = strdup(opt->choices[0]->name);
  541. if (vpninfo->write_new_config) {
  542. buf_append(buf, " </ServerList>\n</GPPortal>\n");
  543. if ((result = buf_error(buf)))
  544. goto out;
  545. if ((result = vpninfo->write_new_config(vpninfo->cbdata, buf->data, buf->pos)))
  546. goto out;
  547. }
  548. /* process auth form to select gateway */
  549. result = process_auth_form(vpninfo, form);
  550. if (result == OC_FORM_RESULT_CANCELLED || result < 0)
  551. goto out;
  552. /* redirect to the gateway (no-op if it's the same host) */
  553. free(vpninfo->redirect_url);
  554. if (asprintf(&vpninfo->redirect_url, "https://%s", vpninfo->authgroup) == 0) {
  555. result = -ENOMEM;
  556. goto out;
  557. }
  558. result = handle_redirect(vpninfo);
  559. out:
  560. buf_free(buf);
  561. free(portal);
  562. free(hip_interval);
  563. free_auth_form(form);
  564. return result;
  565. }
  566. /* Main login entry point
  567. *
  568. * portal: 0 for gateway login, 1 for portal login
  569. * alt_secret: "alternate secret" field (see new_auth_form)
  570. *
  571. */
  572. static int gpst_login(struct openconnect_info *vpninfo, int portal, struct login_context *ctx)
  573. {
  574. int result, blind_retry = 0;
  575. struct oc_text_buf *request_body = buf_alloc();
  576. char *xml_buf = NULL, *orig_path;
  577. /* Ask the user to fill in the auth form; repeat as necessary */
  578. for (;;) {
  579. int keep_urlpath = 0;
  580. if (vpninfo->urlpath) {
  581. /* XX: If the path ends with .esp (possibly followed by a query string), leave as-is */
  582. const char *esp = strstr(vpninfo->urlpath, ".esp");
  583. if (esp && (esp[4] == '\0' || esp[4] == '?'))
  584. keep_urlpath = 1;
  585. }
  586. if (!keep_urlpath) {
  587. orig_path = vpninfo->urlpath;
  588. if (asprintf(&vpninfo->urlpath, "%s/prelogin.esp?tmp=tmp&clientVer=4100&clientos=%s",
  589. portal ? "global-protect" : "ssl-vpn", gpst_os_name(vpninfo)) < 0) {
  590. result = -ENOMEM;
  591. goto out;
  592. }
  593. }
  594. /* submit prelogin request to get form */
  595. result = do_https_request(vpninfo, "POST", NULL, NULL, &xml_buf, NULL, HTTP_REDIRECT);
  596. if (!keep_urlpath) {
  597. free(vpninfo->urlpath);
  598. vpninfo->urlpath = orig_path;
  599. }
  600. if (result >= 0)
  601. result = gpst_xml_or_error(vpninfo, xml_buf, parse_prelogin_xml, NULL, ctx);
  602. if (result)
  603. goto out;
  604. got_form:
  605. /* process auth form */
  606. result = process_auth_form(vpninfo, ctx->form);
  607. if (result)
  608. goto out;
  609. /* Coming back from SAML we might have been redirected */
  610. if (vpninfo->redirect_url) {
  611. result = handle_redirect(vpninfo);
  612. free(vpninfo->redirect_url);
  613. vpninfo->redirect_url = NULL;
  614. if (result)
  615. goto out;
  616. }
  617. replay_form:
  618. /* generate token code if specified */
  619. result = do_gen_tokencode(vpninfo, ctx->form);
  620. if (result) {
  621. vpn_progress(vpninfo, PRG_ERR, _("Failed to generate OTP tokencode; disabling token\n"));
  622. vpninfo->token_bypassed = 1;
  623. goto out;
  624. }
  625. /* submit gateway login (ssl-vpn/login.esp) or portal config (global-protect/getconfig.esp) request */
  626. buf_truncate(request_body);
  627. buf_append(request_body, "jnlpReady=jnlpReady&ok=Login&direct=yes&clientVer=4100&prot=https:&internal=no");
  628. append_opt(request_body, "ipv6-support", vpninfo->disable_ipv6 ? "no" : "yes");
  629. append_opt(request_body, "clientos", gpst_os_name(vpninfo));
  630. append_opt(request_body, "os-version", vpninfo->platname);
  631. append_opt(request_body, "server", vpninfo->hostname);
  632. append_opt(request_body, "computer", vpninfo->localname);
  633. if (ctx->portal_userauthcookie)
  634. append_opt(request_body, "portal-userauthcookie", ctx->portal_userauthcookie);
  635. if (ctx->portal_prelogonuserauthcookie)
  636. append_opt(request_body, "portal-prelogonuserauthcookie", ctx->portal_prelogonuserauthcookie);
  637. if (vpninfo->ip_info.addr)
  638. append_opt(request_body, "preferred-ip", vpninfo->ip_info.addr);
  639. if (vpninfo->ip_info.addr6)
  640. append_opt(request_body, "preferred-ipv6", vpninfo->ip_info.addr);
  641. if (ctx->form->action)
  642. append_opt(request_body, "inputStr", ctx->form->action);
  643. append_form_opts(vpninfo, ctx->form, request_body);
  644. if ((result = buf_error(request_body)))
  645. goto out;
  646. orig_path = vpninfo->urlpath;
  647. vpninfo->urlpath = strdup(portal ? "global-protect/getconfig.esp" : "ssl-vpn/login.esp");
  648. result = do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded", request_body, &xml_buf, NULL, HTTP_NO_FLAGS);
  649. free(vpninfo->urlpath);
  650. vpninfo->urlpath = orig_path;
  651. /* Result could be either a JavaScript challenge or XML */
  652. if (result >= 0)
  653. result = gpst_xml_or_error(vpninfo, xml_buf, portal ? parse_portal_xml : parse_login_xml,
  654. challenge_cb, ctx);
  655. if (result == -EACCES) {
  656. /* Invalid username/password; reuse same form, but blank,
  657. * unless we just did a blind retry.
  658. */
  659. nuke_opt_values(ctx->form->opts);
  660. if (!blind_retry)
  661. goto got_form;
  662. else
  663. blind_retry = 0;
  664. } else {
  665. /* Save successful username */
  666. if (!ctx->username)
  667. ctx->username = strdup(ctx->form->opts->_value);
  668. if (result == -EAGAIN) {
  669. /* New form is already populated from the challenge */
  670. goto got_form;
  671. } else if (portal && result == 0) {
  672. /* Portal login succeeded; blindly retry same credentials on gateway if:
  673. * (a) we received a cookie that should allow automatic retry
  674. * OR (b) portal form was neither challenge auth nor alt-secret (SAML)
  675. */
  676. portal = 0;
  677. if (ctx->portal_userauthcookie || ctx->portal_prelogonuserauthcookie ||
  678. (strcmp(ctx->form->auth_id, "_challenge") && !ctx->alt_secret)) {
  679. blind_retry = 1;
  680. goto replay_form;
  681. }
  682. } else
  683. break;
  684. }
  685. }
  686. out:
  687. buf_free(request_body);
  688. free(xml_buf);
  689. return result;
  690. }
  691. int gpst_obtain_cookie(struct openconnect_info *vpninfo)
  692. {
  693. struct login_context ctx = { .username=NULL, .alt_secret=NULL, .portal_userauthcookie=NULL, .portal_prelogonuserauthcookie=NULL, .form=NULL };
  694. int result;
  695. /* An alternate password/secret field may be specified in the "URL path" (or --usergroup).
  696. * Known possibilities are:
  697. * /portal:portal-userauthcookie
  698. * /gateway:prelogin-cookie
  699. */
  700. if (vpninfo->urlpath
  701. && (ctx.alt_secret = strrchr(vpninfo->urlpath, ':')) != NULL) {
  702. *(ctx.alt_secret) = '\0';
  703. ctx.alt_secret = strdup(ctx.alt_secret+1);
  704. }
  705. if (vpninfo->urlpath && (!strcmp(vpninfo->urlpath, "portal") || !strncmp(vpninfo->urlpath, "global-protect", 14))) {
  706. /* assume the server is a portal */
  707. result = gpst_login(vpninfo, 1, &ctx);
  708. } else if (vpninfo->urlpath && (!strcmp(vpninfo->urlpath, "gateway") || !strncmp(vpninfo->urlpath, "ssl-vpn", 7))) {
  709. /* assume the server is a gateway */
  710. result = gpst_login(vpninfo, 0, &ctx);
  711. } else {
  712. /* first try handling it as a portal, then a gateway */
  713. result = gpst_login(vpninfo, 1, &ctx);
  714. if (result == -EEXIST) {
  715. result = gpst_login(vpninfo, 0, &ctx);
  716. if (result == -EEXIST)
  717. vpn_progress(vpninfo, PRG_ERR, _("Server is neither a GlobalProtect portal nor a gateway.\n"));
  718. }
  719. }
  720. free(ctx.username);
  721. free(ctx.alt_secret);
  722. free(ctx.portal_userauthcookie);
  723. free(ctx.portal_prelogonuserauthcookie);
  724. free_auth_form(ctx.form);
  725. return result;
  726. }
  727. int gpst_bye(struct openconnect_info *vpninfo, const char *reason)
  728. {
  729. char *orig_path;
  730. int result;
  731. struct oc_text_buf *request_body = buf_alloc();
  732. char *xml_buf = NULL;
  733. /* In order to logout successfully, the client must send not only
  734. * the session's authcookie, but also the portal, user, computer,
  735. * and domain matching the values sent with the getconfig request.
  736. *
  737. * You read that right: the client must send a bunch of irrelevant
  738. * non-secret values in its logout request. If they're wrong or
  739. * missing, the logout will fail and the authcookie will remain
  740. * valid -- which is a security hole.
  741. *
  742. * Don't blame me. I didn't design this.
  743. */
  744. buf_append(request_body, "%s", vpninfo->cookie);
  745. if ((result = buf_error(request_body)))
  746. goto out;
  747. /* We need to close and reopen the HTTPS connection (to kill
  748. * the tunnel session) and submit a new HTTPS request to
  749. * logout.
  750. */
  751. orig_path = vpninfo->urlpath;
  752. vpninfo->urlpath = strdup("ssl-vpn/logout.esp");
  753. openconnect_close_https(vpninfo, 0);
  754. result = do_https_request(vpninfo, "POST", "application/x-www-form-urlencoded", request_body, &xml_buf, NULL, HTTP_NO_FLAGS);
  755. free(vpninfo->urlpath);
  756. vpninfo->urlpath = orig_path;
  757. /* logout.esp returns HTTP status 200 and <response status="success"> when
  758. * successful, and all manner of malformed junk when unsuccessful.
  759. */
  760. if (result >= 0)
  761. result = gpst_xml_or_error(vpninfo, xml_buf, NULL, NULL, NULL);
  762. if (result < 0)
  763. vpn_progress(vpninfo, PRG_ERR, _("Logout failed.\n"));
  764. else
  765. vpn_progress(vpninfo, PRG_INFO, _("Logout successful.\n"));
  766. out:
  767. buf_free(request_body);
  768. free(xml_buf);
  769. return result;
  770. }