diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7aa6d5be52dab28dd3788ab6948a20885c55bdd1..d0da8515629923a3c61733f01931264df5f7d57f 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -37,6 +37,7 @@ Signoff: static-analyzer/GnuTLS/Fedora: stage: static-analysis + needs: [] # run immediately image: $CI_REGISTRY/$BUILD_IMAGES_PROJECT:$FEDORA_BUILD script: - ./autogen.sh @@ -55,6 +56,7 @@ static-analyzer/GnuTLS/Fedora: static-analyzer/OpenSSL/Fedora: stage: static-analysis + needs: [] # run immediately image: $CI_REGISTRY/$BUILD_IMAGES_PROJECT:$FEDORA_BUILD script: - ./autogen.sh @@ -77,13 +79,14 @@ static-analyzer/OpenSSL/Fedora: Fedora/GnuTLS: stage: test-first + needs: [] # run immediately script: - ./autogen.sh - ./configure --with-java --disable-dsa-tests --without-gnutls-version-check --enable-ppp-tests CFLAGS=-g - make tmp-distdir - mkdir build - cd build - - TMPDISTDIR=../openconnect-$(git describe --tags | sed s/^v//) + - TMPDISTDIR=../openconnect-$(git describe --tags 2>/dev/null | sed s/^v// || true) - ${TMPDISTDIR}/configure --with-java --disable-dsa-tests --without-gnutls-version-check --enable-ppp-tests CFLAGS=-g - make -j4 # For reasons that are unclear, but probably also unimportant, IPv6 is disabled by default on this CI @@ -107,6 +110,7 @@ Fedora/GnuTLS: Fedora/OpenSSL: stage: test-first + needs: [] # run immediately image: $CI_REGISTRY/$BUILD_IMAGES_PROJECT:$FEDORA_BUILD script: # Re-enable DSA since we test it @@ -116,7 +120,7 @@ Fedora/OpenSSL: - make tmp-distdir - mkdir build - cd build - - TMPDISTDIR=../openconnect-$(git describe --tags | sed s/^v//) + - TMPDISTDIR=../openconnect-$(git describe --tags 2>/dev/null | sed s/^v// || true) - ${TMPDISTDIR}/configure --without-gnutls --with-openssl --without-openssl-version-check --disable-dsa-tests --enable-ppp-tests CFLAGS=-g - make -j4 # For reasons that are unclear, but probably also unimportant, IPv6 is disabled by default on this CI @@ -321,7 +325,7 @@ Fedora/GnuTLS/ibmtss: - make tmp-distdir - mkdir build - cd build - - TMPDISTDIR=../openconnect-$(git describe --tags | sed s/^v//) + - TMPDISTDIR=../openconnect-$(git describe --tags 2>/dev/null | sed s/^v// || true) - ${TMPDISTDIR}/configure --with-java --disable-dsa-tests --without-gnutls-version-check --enable-ppp-tests --with-gnutls-tss2=ibmtss CFLAGS=-g - make -j4 # For reasons that are unclear, but probably also unimportant, IPv6 is disabled by default on this CI @@ -351,7 +355,7 @@ Fedora/GnuTLS/clang: - make tmp-distdir - mkdir build - cd build - - TMPDISTDIR=../openconnect-$(git describe --tags | sed s/^v//) + - TMPDISTDIR=../openconnect-$(git describe --tags 2>/dev/null | sed s/^v// || true) - ${TMPDISTDIR}/configure --with-java --disable-dsa-tests --without-gnutls-version-check CC=clang --enable-ppp-tests CFLAGS=-g - make -j4 # For reasons that are unclear, but probably also unimportant, IPv6 is disabled by default on this CI @@ -384,7 +388,7 @@ Fedora/OpenSSL/clang: - make tmp-distdir - mkdir build - cd build - - TMPDISTDIR=../openconnect-$(git describe --tags | sed s/^v//) + - TMPDISTDIR=../openconnect-$(git describe --tags 2>/dev/null | sed s/^v// || true) - ${TMPDISTDIR}/configure CC=clang --without-gnutls --with-openssl --without-openssl-version-check --disable-dsa-tests --enable-ppp-tests CFLAGS=-g - make -j4 # For reasons that are unclear, but probably also unimportant, IPv6 is disabled by default on this CI diff --git a/library.c b/library.c index b5e74e9b765b5b146a0af841b8198047ca8dbb7e..901c909d13c22a265d14f4fec3efa3a2909ab33a 100644 --- a/library.c +++ b/library.c @@ -101,6 +101,7 @@ struct openconnect_info *openconnect_vpninfo_new(const char *useragent, #else vpninfo->external_browser = NULL; #endif + vpninfo->external_auth_cmd = NULL; vpninfo->localname = NULL; vpninfo->unique_hostname = NULL; vpninfo->port = 443; @@ -1784,6 +1785,98 @@ void nuke_opt_values(struct oc_form_opt *opt) } } +static void parse_external_auth_cmd_output(struct openconnect_info *vpninfo, const char* line) +{ + // Trim spaces + int len = strlen(line); + while (len > 0 && line[len-1] <= ' ') + len--; + + vpn_progress(vpninfo, PRG_DEBUG, _("External auth command stdout: '%.*s'\n"), len, line); + + if (len == 0) + return; + + // Try to parse as expected output + const char hdr[] = "sso_cookie_value="; + size_t hdr_len = sizeof(hdr) - 1; + if (!strncmp(line, hdr, MIN(len, hdr_len))) { + vpninfo->sso_cookie_value = strndup(&line[hdr_len], len - hdr_len); + } +} + +#if defined(_WIN32) +static int run_external_auth_cmd(struct openconnect_info *vpninfo) +{ + vpn_progress(vpninfo, PRG_ERR, _("External auth command is not supported on Windows\n")); + return -EINVAL; +} +#else +static int run_external_auth_cmd(struct openconnect_info *vpninfo) +{ + FILE *proc; + int ret = 0; + + ret = setenv("OC_SSO_LOGIN_URL", vpninfo->sso_login, /* overwrite */ 1); + if (ret) { + goto err_setenv; + } + + ret = setenv("OC_SSO_TOKEN_COOKIE", vpninfo->sso_token_cookie, /* overwrite */ 1); + if (ret) { + goto err_setenv; + } + + ret = setenv("OC_SSO_ERROR_COOKIE", vpninfo->sso_error_cookie, /* overwrite */ 1); + if (ret) { + goto err_setenv; + } + + vpn_progress(vpninfo, PRG_DEBUG, _("Running external auth command for URL: %s\n"), + vpninfo->sso_login); + + proc = popen(vpninfo->external_auth_cmd, "r"); + if (!proc) { + ret = errno; + goto err_popen; + } + + char line[4096]; + while (fgets(line, sizeof(line), proc) != NULL) { + parse_external_auth_cmd_output(vpninfo, line); + } + + ret = pclose(proc); + if (ret < 0) { + ret = errno; + goto err_pclose; + } + + if (WEXITSTATUS(ret)) { + vpn_progress(vpninfo, PRG_ERR, _("External auth command failed: exit_code=%d\n"), WEXITSTATUS(ret)); + ret = -ECHILD; + goto err_ecode; + } + + if (!vpninfo->sso_cookie_value || !*vpninfo->sso_cookie_value) { + vpn_progress(vpninfo, PRG_ERR, _("External auth command exited without providing a cookie\n")); + ret = -ECHILD; + goto err_no_cookie; + } + +err_no_cookie: +err_ecode: +err_pclose: +err_popen: + unsetenv("OC_SSO_ERROR_COOKIE"); + unsetenv("OC_SSO_TOKEN_COOKIE"); + unsetenv("OC_SSO_LOGIN_URL"); + +err_setenv: + return ret; +} +#endif + int process_auth_form(struct openconnect_info *vpninfo, struct oc_auth_form *form) { int ret, do_sso = 0; @@ -1861,7 +1954,9 @@ retry: vpninfo->sso_username = NULL; /* Handle the special Cisco external browser mode */ - if (vpninfo->sso_browser_mode && !strcmp(vpninfo->sso_browser_mode, "external")) { + if (vpninfo->external_auth_cmd) { + ret = run_external_auth_cmd(vpninfo); + } else if (vpninfo->sso_browser_mode && !strcmp(vpninfo->sso_browser_mode, "external")) { ret = handle_external_browser(vpninfo); } else if (vpninfo->open_webview) { ret = vpninfo->open_webview(vpninfo, vpninfo->sso_login, vpninfo->cbdata); diff --git a/main.c b/main.c index 6c21796b7abf562e47353bd093db6407e73cce75..e27628b38c6949d9fbfca12c51ec056bb5d518df 100644 --- a/main.c +++ b/main.c @@ -180,6 +180,7 @@ enum { OPT_DTLS12_CIPHERS, OPT_DUMP_HTTP, OPT_EXT_BROWSER, + OPT_EXT_AUTH_CMD, OPT_FORCE_DPD, OPT_FORCE_TROJAN, OPT_GNUTLS_DEBUG, @@ -245,6 +246,7 @@ static const struct option long_options[] = { #if defined(HAVE_POSIX_SPAWN) || defined(_WIN32) OPTION("external-browser", 1, OPT_EXT_BROWSER), #endif + OPTION("external-auth-cmd", 1, OPT_EXT_AUTH_CMD), OPTION("no-external-auth", 0, OPT_NO_EXTERNAL_AUTH), OPTION("pfs", 0, OPT_PFS), OPTION("allow-insecure-crypto", 0, OPT_ALLOW_INSECURE_CRYPTO), @@ -1035,6 +1037,7 @@ static void usage(void) printf(" -e, --cert-expire-warning=DAYS %s\n", _("Warn when certificate lifetime < DAYS")); printf(" -g, --usergroup=GROUP %s\n", _("Set path of initial request URL")); printf(" -p, --key-password=PASS %s\n", _("Set key passphrase or TPM SRK PIN")); + printf(" --external-auth-cmd=CMD %s\n", _("Command to run to perform authentication. Details in `man openconnect`.")); printf(" --external-browser=BROWSER %s\n", _("Set external browser executable")); printf(" --key-password-from-fsid %s\n", _("Key passphrase is fsid of file system")); printf(" --token-mode=MODE %s\n", _("Software token type: rsa, totp, hotp or oidc")); @@ -2145,6 +2148,9 @@ int main(int argc, char *argv[]) case OPT_EXT_BROWSER: vpninfo->external_browser = dup_config_arg(); break; + case OPT_EXT_AUTH_CMD: + vpninfo->external_auth_cmd = dup_config_arg(); + break; case OPT_NO_EXTERNAL_AUTH: /* XX: Is this a workaround for a server bug, or a "normal" authentication option? */ vpninfo->no_external_auth = 1; diff --git a/openconnect-internal.h b/openconnect-internal.h index 6925d60c3bc091559547dffc9ce7963d0ef225e8..1f3f79560deb05f9a0a55849c8667ce2778aeb87 100644 --- a/openconnect-internal.h +++ b/openconnect-internal.h @@ -484,6 +484,7 @@ struct openconnect_info { struct http_auth_state proxy_auth[MAX_AUTH_TYPES]; int no_external_auth; const char *external_browser; + const char *external_auth_cmd; char *localname; diff --git a/openconnect.8.in b/openconnect.8.in index 293d3acbcf9e199315bcef786ea4aab96a1d0a6c..a3827ec493405dd877cbf75faf91d9b3b57d6fbe 100644 --- a/openconnect.8.in +++ b/openconnect.8.in @@ -20,6 +20,7 @@ openconnect \- Multi-protocol VPN client, for Cisco AnyConnect VPNs and others .OP \-g,\-\-usergroup group .OP \-h,\-\-help .OP \-\-http\-auth methods +.OP \-\-external\-auth-command command .OP \-\-external\-browser browser .OP \-i,\-\-interface ifname .OP \-l,\-\-syslog @@ -289,6 +290,16 @@ authentication in that order, if each is enabled, regardless of the order specified in the .I METHODS string. +.TAG opt-external-auth-cmd +.TP +.B \-\-external\-auth-cmd=COMMAND +Run +.I COMMAND +to handle the authentication process with +SSO gateways. +Only +.B --protocol=anyconnect +is supported for now. .TAG opt-external-browser .TP .B \-\-external\-browser=BROWSER