From d7fa4b375d1a84ea1d2c3daf3456baf3fa315b2a Mon Sep 17 00:00:00 2001 From: David Woodhouse Date: Tue, 7 Apr 2020 14:12:27 +0100 Subject: [PATCH] Add bash completion Signed-off-by: David Woodhouse --- Makefile.am | 5 +- bash/openconnect.bash | 107 +++++++++++++++++++++ main.c | 215 ++++++++++++++++++++++++++++++++++++++++++ openconnect.spec.in | 1 + www/changelog.xml | 2 +- 5 files changed, 328 insertions(+), 2 deletions(-) create mode 100644 bash/openconnect.bash diff --git a/Makefile.am b/Makefile.am index e8ec3825..ac155aab 100644 --- a/Makefile.am +++ b/Makefile.am @@ -135,13 +135,16 @@ pkgconfig_DATA = openconnect.pc EXTRA_DIST = AUTHORS version.sh COPYING.LGPL $(lib_srcs_openssl) $(lib_srcs_gnutls) EXTRA_DIST += $(shell cd "$(top_srcdir)" && \ - git ls-tree HEAD -r --name-only -- android/ java/ trojans/ 2>/dev/null) + git ls-tree HEAD -r --name-only -- android/ java/ trojans/ bash/ 2>/dev/null) DISTCLEANFILES = $(pkgconfig_DATA) pkglibexec_SCRIPTS = trojans/csd-post.sh trojans/csd-wrapper.sh trojans/tncc-wrapper.py \ trojans/hipreport.sh trojans/hipreport-android.sh +bashcompletiondir = $(sysconfdir)/bash_completion.d +bashcompletion_DATA = bash/openconnect.bash + # main.c includes version.c openconnect-main.$(OBJEXT): version.c diff --git a/bash/openconnect.bash b/bash/openconnect.bash new file mode 100644 index 00000000..89d55ee7 --- /dev/null +++ b/bash/openconnect.bash @@ -0,0 +1,107 @@ +# +# Bash completion for OpenConnect +# +# Copyright © David Woodhouse +# +# Author: David Woodhouse +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public License +# version 2.1, as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. + + +# Consider a command line like the following: +# +# openconnect -c --authenticate\ -k -k "'"'"'.pem --authgroup 'foo +# bar' --o\s linux-64 myserver +# +# There is absolutely no way I want to attempt parsing that in C and +# attempting to come up with the correct results as bash would do. +# That is just designing for failure; we'll never get it right. +# +# Yet if we use 'complete -C openconnect openconnect' and allow the +# program to do completions all by itself, that's what bash expects +# it to do. All that's passed into the program is $COMP_LINE and +# some other metadata. +# +# So instead we use bash to help us. In a completion *function* we +# are given the ${COMP_WORDS[]} array which has actually been parsed +# correctly. We still want openconnect itself to be able to do the +# result generation, so just prepend --autocomplete to the args. +# +# For special cases like filenames and hostnames, we want to invoke +# compgen, again to avoid reinventing the wheel badly. So define +# special cases HOSTNAME, FILENAME as the autocomplete results, +# to be handled as special cases. In those cases we also use +# ${COMP_WORDS[$COMP_CWORD]}) as the string to bew completed, +# pristine from bash instead of having been passed through the +# program itself. Thus, we see correct completions along the lines +# of +# +# $ ls foo\ * +# 'foo bar.pem' 'foo bar.xml' 'foo baz.crt' +# $ openconnect -c ./fo +# +# ... partially completes to: +# +# $ openconnect -c ./foo\ ba +# +# ... and a second shows: +# +# foo bar.pem foo baz.crt +# +# Likewise, +# +# $ touch '"'"'".pem +# $ openconnect -c '"' +# +# ...completes to: +# +# $ openconnect -c \"\'.pem +# +# This does fall down if I create a filename with a newline in it, +# but even tab-completion for 'ls' falls over in that case. +# +# The main problem with this approach is that we can't easily map +# $COMP_POINT to the precise character on the line at which TAB was +# being pressed, which may not be the *end*. + + +_complete_openconnect () { + export COMP_LINE COMP_POINT COMP_CWORD COMP_KEY COMP_TYPE + COMP_WORDS[0]="--autocomplete" + local IFS=$'\n' + COMPREPLY=( $(/home/dwmw/git/openconnect/gtls-ibm/openconnect "${COMP_WORDS[@]}") ) + case "${COMPREPLY[0]}" in + FILENAME) + if [ "${COMPREPLY[1]}" != "" ]; then + COMPREPLY=( $( compgen -f -o filenames -o plusdirs -X ${COMPREPLY[1]} ${COMP_WORDS[$COMP_CWORD]}) ) + else + COMPREPLY=( $( compgen -f -o filenames -o plusdirs ${COMP_WORDS[$COMP_CWORD]}) ) + fi + ;; + + FILENAMEAT) + COMPREPLY=( $( compgen -P @ -f -o filenames -o plusdirs ${COMP_WORDS[$COMP_CWORD]#@}) ) + ;; + + EXECUTABLE) + COMPREPLY=( $( compgen -c -o plusdirs ${COMP_WORDS[$COMP_CWORD]}) ) + ;; + + HOSTNAME) + COMPREPLY=( $( compgen -A hostname ${COMP_WORDS[$COMP_CWORD]}) ) + ;; + + USERNAME) + COMPREPLY=( $( compgen -A user ${COMP_WORDS[$COMP_CWORD]}) ) + ;; + esac +} + +complete -F _complete_openconnect -o filenames openconnect diff --git a/main.c b/main.c index 32823586..366c5770 100644 --- a/main.c +++ b/main.c @@ -1100,6 +1100,218 @@ static void get_uids(const char *config_arg, uid_t *uid, gid_t *gid) } #endif +static int complete_words(char *partial, ...) +{ + int partlen = strlen(partial); + va_list vl; + char *check; + + va_start(vl, partial); + while ( (check = va_arg(vl, char *)) ) { + if (!strncmp(partial, check, partlen)) + printf("%s\n", check); + } + va_end(vl); + return 0; +} + +static int autocomplete(int argc, char **argv) +{ + int opt; + const char *comp_cword = getenv("COMP_CWORD"); + char *comp_opt; + int cword, longidx; + + /* Skip over the --autocomplete */ + argc--; + argv++; + + if (!comp_cword) + return -EINVAL; + + cword = atoi(comp_cword); + if (cword <= 0 || cword > argc) + return -EINVAL; + + comp_opt = argv[cword]; + if (!comp_opt) + return -EINVAL; + + opterr = 0; + + while (1) { + int match_opt = (argv[optind] == comp_opt); + + /* Don't let getopt_long() assume it's a separator; instead + * assume they want to tab-complete to a real long option. */ + if (match_opt && !strcmp(comp_opt, "--")) + goto empty_opt; + + opt = getopt_long(argc, argv, +#ifdef _WIN32 + "C:c:Dde:F:g:hi:k:m:P:p:Q:qs:u:Vvx:", +#else + "bC:c:Dde:F:g:hi:k:lm:P:p:Q:qSs:U:u:Vvx:", +#endif + long_options, &longidx); + + if (opt == -1) + break; + + if (match_opt) { + empty_opt: + /* No autocompletion for short options */ + if (!strncmp(comp_opt, "--", 2)) { + int complen = strlen(comp_opt + 2); + const struct option *p = long_options; + + while (p->name) { + if (!strncmp(comp_opt + 2, p->name, complen)) + printf("--%s\n", p->name); + p++; + } + } + return 0; + } + + + if (optarg == comp_opt) { + switch (opt) { + case 'k': /* --sslkey */ + case 'c': /* --certificate */ + if (!strncmp(comp_opt, "pkcs11:", 7)) { + /* We could do clever things here... */ + return 0; /* .. but we don't. */ + } + printf("FILENAME\n!*.@(pem|der|p12|crt)\n"); + break; + + case OPT_CAFILE: /* --cafile */ + printf("FILENAME\n!*.@(pem|der|crt)\n"); + break; + + case 'x': /* --xmlconfig */ + printf("FILENAME\n!*.xml\n"); + break; + + case OPT_CONFIGFILE: /* --config */ + case OPT_PIDFILE: /* --pid-file */ + printf("FILENAME\n"); + break; + + case 's': /* --script */ + case OPT_CSD_WRAPPER: /* --csd-wrapper */ + printf("EXECUTABLE\n"); + break; + + case OPT_LOCAL_HOSTNAME: /* --local-hostname */ + printf("HOSTNAME\n"); + break; + + case OPT_CSD_USER: /* --csd-user */ + case 'U': /* --setuid */ + printf("USERNAME\n"); + break; + + case OPT_OS: /* --os */ + complete_words(comp_opt, "mac-intel", "android", + "linux-64", "linux", "apple-ios", + "win", NULL); + break; + + case OPT_COMPRESSION: /* --compression */ + complete_words(comp_opt, "none", "off", "all", + "stateless", NULL); + break; + + case OPT_PROTOCOL: /* --protocol */ + { + struct oc_vpn_proto *protos, *p; + int partlen = strlen(comp_opt); + + if (openconnect_get_supported_protocols(&protos) >= 0) { + for (p = protos; p->name; p++) { + if(!strncmp(comp_opt, p->name, partlen)) + printf("%s\n", p->name); + } + free(protos); + } + break; + } + + case OPT_HTTP_AUTH: /* --http-auth */ + case OPT_PROXY_AUTH: /* --proxy-auth */ + /* FIXME: Expand latest list item */ + break; + + case OPT_TOKEN_MODE: /* --token-mode */ + complete_words(comp_opt, "totp", "hotp", "oidc", NULL); + if (openconnect_has_stoken_support()) + complete_words(comp_opt, "rsa", NULL); + if (openconnect_has_yubioath_support()) + complete_words(comp_opt, "yubioath", NULL); + break; + + case OPT_TOKEN_SECRET: /* --token-secret */ + if (!comp_opt[0] || comp_opt[0] == '/') + printf("FILENAME\n"); + else if (comp_opt[0] == '@') + printf("FILENAMEAT\n"); + break; + + case 'i': /* --interface */ + /* FIXME: Enumerate available tun devices */ + break; + + case OPT_SERVERCERT: /* --servercert */ + /* We could do something really evil here and actually + * connect, then return the result? */ + break; + + /* No autocmplete for these but handle them explicitly so that + * we can have automatic checking for *accidentally* unhandled + * options. Right after we do automated checking of man page + * entries and --help output for all supported options too. */ + + case 'e': /* --cert-expire-warning */ + case 'C': /* --cookie */ + case 'g': /* --usergroup */ + case 'm': /* --mtu */ + case OPT_BASEMTU: /* --base-mtu */ + case 'p': /* --key-password */ + case 'P': /* --proxy */ + case 'u': /* --user */ + case 'Q': /* --queue-len */ + case OPT_RECONNECT_TIMEOUT: /* --reconnect-timeout */ + case OPT_AUTHGROUP: /* --authgroup */ + case OPT_RESOLVE: /* --resolve */ + case OPT_USERAGENT: /* --useragent */ + case OPT_VERSION: /* --version-string */ + case OPT_FORCE_DPD: /* --force-dpd */ + case OPT_FORCE_TROJAN: /* --force-trojan */ + case OPT_DTLS_LOCAL_PORT: /* --dtls-local-port */ + case 'F': /* --form-entry */ + case OPT_GNUTLS_DEBUG: /* --gnutls-debug */ + case OPT_CIPHERSUITES: /* --gnutls-priority */ + case OPT_DTLS_CIPHERS: /* --dtls-ciphers */ + case OPT_DTLS12_CIPHERS: /* --dtls12-ciphers */ + break; + + default: + fprintf(stderr, _("Unhandled autocomplete for option %d '--%s'. Please report.\n"), + opt, long_options[longidx].name); + return -ENOENT; + } + + return 0; + } + } + + /* Ths only non-option argument we accept as a hostname */ + printf("HOSTNAME\n"); + return 0; +} + int main(int argc, char **argv) { struct openconnect_info *vpninfo; @@ -1137,6 +1349,9 @@ int main(int argc, char **argv) fprintf(stderr, _("WARNING: Cannot set locale: %s\n"), strerror(errno)); + if (argc > 2 && !strcmp(argv[1], "--autocomplete")) + return autocomplete(argc, argv); + #ifdef HAVE_NL_LANGINFO charset = nl_langinfo(CODESET); if (charset && strcmp(charset, "UTF-8")) diff --git a/openconnect.spec.in b/openconnect.spec.in index 03be1ea6..f5a0ba8b 100644 --- a/openconnect.spec.in +++ b/openconnect.spec.in @@ -145,6 +145,7 @@ make VERBOSE=1 XFAIL_TESTS="auth-nonascii bad_dtls_test" check %{_sbindir}/openconnect %{_libexecdir}/openconnect/ %{_mandir}/man8/* +%{_sysconfdir}/bash_completion.d %doc TODO COPYING.LGPL %doc %{_pkgdocdir} diff --git a/www/changelog.xml b/www/changelog.xml index c44324d3..fb1b9a17 100644 --- a/www/changelog.xml +++ b/www/changelog.xml @@ -15,7 +15,7 @@
  • OpenConnect HEAD
      -
    • No changelog entries yet
    • +
    • Add bash completion support.

  • OpenConnect v8.08 -- 2.50.1