Skip to content

Commit

Permalink
Add bash completion
Browse files Browse the repository at this point in the history
Signed-off-by: David Woodhouse <dwmw2@infradead.org>
  • Loading branch information
dwmw2 committed Apr 7, 2020
1 parent a73743d commit d7fa4b3
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 2 deletions.
5 changes: 4 additions & 1 deletion Makefile.am
Expand Up @@ -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

Expand Down
107 changes: 107 additions & 0 deletions bash/openconnect.bash
@@ -0,0 +1,107 @@
#
# Bash completion for OpenConnect
#
# Copyright © David Woodhouse <dwmw2@infradead.org>
#
# Author: David Woodhouse <dwmw2@infradead.org>
#
# 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<TAB>
#
# ... partially completes to:
#
# $ openconnect -c ./foo\ ba
#
# ... and a second <TAB> shows:
#
# foo bar.pem foo baz.crt
#
# Likewise,
#
# $ touch '"'"'".pem
# $ openconnect -c '"'<TAB>
#
# ...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
215 changes: 215 additions & 0 deletions main.c
Expand Up @@ -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;
Expand Down Expand Up @@ -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"))
Expand Down
1 change: 1 addition & 0 deletions openconnect.spec.in
Expand Up @@ -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}

Expand Down
2 changes: 1 addition & 1 deletion www/changelog.xml
Expand Up @@ -15,7 +15,7 @@
<ul>
<li><b>OpenConnect HEAD</b>
<ul>
<li><i>No changelog entries yet</i></li>
<li>Add bash completion support.</li>
</ul><br/>
</li>
<li><b><a href="ftp://ftp.infradead.org/pub/openconnect/openconnect-8.08.tar.gz">OpenConnect v8.08</a></b>
Expand Down

0 comments on commit d7fa4b3

Please sign in to comment.