diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000..bf2039d --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,34 @@ +--- +Checks: 'clang-diagnostic-*,clang-analyzer-*' +WarningsAsErrors: '' +HeaderFilterRegex: '' +AnalyzeTemporaryDtors: false +FormatStyle: none +User: dparrish +CheckOptions: + - key: cert-dcl16-c.NewSuffixes + value: 'L;LL;LU;LLU' + - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic + value: '1' + - key: google-readability-braces-around-statements.ShortStatementLines + value: '1' + - key: google-readability-function-size.StatementThreshold + value: '800' + - key: google-readability-namespace-comments.ShortNamespaceLines + value: '10' + - key: google-readability-namespace-comments.SpacesBeforeComments + value: '2' + - key: modernize-loop-convert.MaxCopySize + value: '16' + - key: modernize-loop-convert.MinConfidence + value: reasonable + - key: modernize-loop-convert.NamingStyle + value: CamelCase + - key: modernize-pass-by-value.IncludeStyle + value: llvm + - key: modernize-replace-auto-ptr.IncludeStyle + value: llvm + - key: modernize-use-nullptr.NullMacros + value: 'NULL' +... + diff --git a/.gitignore b/.gitignore index b0c2baf..b2e1526 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ *.so *.so.* clitest +compile_commands.json diff --git a/Makefile b/Makefile index b082edf..94a3366 100644 --- a/Makefile +++ b/Makefile @@ -10,8 +10,8 @@ DESTDIR = PREFIX = /usr/local MAJOR = 1 -MINOR = 9 -REVISION = 8 +MINOR = 10 +REVISION = 0 LIB = libcli.so LIB_STATIC = libcli.a @@ -79,9 +79,13 @@ install: $(TARGET_LIBS) rpmprep: rm -rf libcli-$(MAJOR).$(MINOR).$(REVISION) mkdir libcli-$(MAJOR).$(MINOR).$(REVISION) - cp -R libcli.{c,h} libcli.spec clitest.c Makefile COPYING README libcli-$(MAJOR).$(MINOR).$(REVISION) + cp -R libcli.{c,h} libcli.spec clitest.c Makefile COPYING README.md doc libcli-$(MAJOR).$(MINOR).$(REVISION) tar zcvf libcli-$(MAJOR).$(MINOR).$(REVISION).tar.gz --exclude CVS --exclude *.tar.gz libcli-$(MAJOR).$(MINOR).$(REVISION) rm -rf libcli-$(MAJOR).$(MINOR).$(REVISION) rpm: rpmprep rpmbuild -ta libcli-$(MAJOR).$(MINOR).$(REVISION).tar.gz --define "debug_package %{nil}" --clean + +lint: + clang-tidy -quiet -warnings-as-errors *.c *.h + diff --git a/README b/README.md similarity index 56% rename from README rename to README.md index a39e115..4e8b57b 100644 --- a/README +++ b/README.md @@ -1,112 +1,111 @@ -libcli +Libcli provides a shared C library for including a Cisco-like command-line +interface into other software. -libcli emulates a cisco style telnet command-line interface. +It’s a telnet interface which supports command-line editing, history, +authentication and callbacks for a user-definable function tree. To compile: - make - make install +```sh +$ make +$ make install +``` -This will install libcli.so into /usr/local/lib. If you want to change -the location, edit Makefile. +This will install `libcli.so` into `/usr/local/lib`. If you want to change the +location, edit Makefile. -There is a test application built called clitest. Run this and telnet -to port 8000. +There is a test application built called clitest. Run this and telnet to port +8000. By default, a single username and password combination is enabled. +``` Username: fred Password: nerk +``` -Get help by entering "help" or hitting ?. +Get help by entering `help` or hitting `?`. libcli provides support for using the arrow keys for command-line editing. Up and Down arrows will cycle through the command history, and Left & Right can be used for editing the current command line. + libcli also works out the shortest way of entering a command, so if you have a -command "show users grep foobar" defined, you can enter "sh us g foobar" if that +command `show users | grep foobar` defined, you can enter `sh us | g foobar` if that is the shortest possible way of doing it. -Enter "sh?" at the command line to get a list of commands starting with "sh" +Enter `sh?` at the command line to get a list of commands starting with `sh` A few commands are defined in every libcli program: -help -quit -exit -logout -history - - +* `help` +* `quit` +* `exit` +* `logout` +* `history` Use in your own code: -First of all, make sure you #include in your C code, and link -with -lcli. +First of all, make sure you `#include ` in your C code, and link with +`-lcli`. If you have any trouble with this, have a look at clitest.c for a demonstration. -Start your program off with a cli_init(). +Start your program off with a `cli_init()`. This sets up the internal data structures required. When a user connects, they are presented with a greeting if one is set using the -cli_set_banner(banner) function. - +`cli_set_banner(banner)` function. By default, the command-line session is not authenticated, which means users will get full access as soon as they connect. As this may not be always the best thing, 2 methods of authentication are available. First, you can add username / password combinations with the -cli_allow_user(username, password) function. When a user connects, they can +`cli_allow_user(username, password)` function. When a user connects, they can connect with any of these username / password combinations. -Secondly, you can add a callback using the cli_set_auth_callback(callback) -function. This function is passed the username and password as char *, and must -return CLI_OK if the user is to have access and CLI_ERROR if they are not. +Secondly, you can add a callback using the `cli_set_auth_callback(callback)` +function. This function is passed the username and password as `char *`, and must +return `CLI_OK` if the user is to have access and `CLI_ERROR` if they are not. The library itself will take care of prompting the user for credentials. - - - Commands are built using a tree-like structure. You define commands with the -cli_register_command(parent, command, callback, privilege, mode, help) function. +`cli_register_command(parent, command, callback, privilege, mode, help)` function. -parent is a cli_command * reference to a previously added command. Using a +`parent` is a `cli_command *` reference to a previously added command. Using a parent you can build up complex commands. -e.g. to provide commands "show users", "show sessions" and "show people", use + +e.g. to provide commands `show users`, `show sessions` and `show people`, use the following sequence: +```c cli_command *c = cli_register_command(NULL, "show", NULL, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); cli_register_command(c, "sessions", fn_sessions, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, "Show the sessions connected"); cli_register_command(c, "users", fn_users, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, "Show the users connected"); cli_register_command(c, "people", fn_people, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, "Show a list of the people I like"); +``` - -If callback is NULL, the command can be used as part of a tree, but cannot be +If callback is `NULL`, the command can be used as part of a tree, but cannot be individually run. - If you decide later that you don't want a command to be run, you can call -cli_unregister_command(command). +`cli_unregister_command(command)`. You can use this to build dynamic command trees. - It is possible to carry along a user-defined context to all command callbacks -using cli_set_context(cli, context) and cli_get_context(cli) functions. +using `cli_set_context(cli, context)` and `cli_get_context(cli)` functions. You are responsible for accepting a TCP connection, and for creating a process or thread to run the cli. Once you are ready to process the -connection, call cli_loop(cli, sock) to interact with the user on the +connection, call `cli_loop(cli, sock)` to interact with the user on the given socket. This function will return when the user exits the cli, either by breaking the -connection or entering "quit". - -Call cli_done() to free the data structures. +connection or entering `quit`. +Call `cli_done()` to free the data structures. -- David Parrish (david@dparrish.com) diff --git a/clitest.c b/clitest.c index 51cd760..dc9f8b2 100644 --- a/clitest.c +++ b/clitest.c @@ -1,3 +1,5 @@ +#include +#include #include #include #ifdef WIN32 @@ -174,22 +176,157 @@ void pc(UNUSED(struct cli_def *cli), const char *string) { printf("%s\n", string); } -int main() { - struct cli_command *c; - struct cli_def *cli; - int s, x; - struct sockaddr_in addr; - int on = 1; +#define MODE_POLYGON_TRIANGLE 20 +#define MODE_POLYGON_RECTANGLE 21 -#ifndef WIN32 - signal(SIGCHLD, SIG_IGN); -#endif -#ifdef WIN32 - if (!winsock_init()) { - printf("Error initialising winsock\n"); - return 1; +int cmd_perimeter(struct cli_def *cli, const char *command, char *argv[], int argc) { + struct cli_optarg_pair *optargs = cli_get_all_found_optargs(cli); + int i = 1, numSides = 0; + int perimeter = 0; + int verbose_count = 0; + char *verboseArg = NULL; + char *shapeName = NULL; + + cli_print(cli, "perimeter callback, with %d args", argc); + for (; optargs; optargs = optargs->next) cli_print(cli, "%d, %s=%s", i++, optargs->name, optargs->value); + + if ((verboseArg = cli_get_optarg_value(cli, "verbose", verboseArg))) { + do { + verbose_count++; + } while ((verboseArg = cli_get_optarg_value(cli, "verbose", verboseArg))); } -#endif + cli_print(cli, "verbose argument was seen %d times", verbose_count); + + shapeName = cli_get_optarg_value(cli, "shape", NULL); + if (!shapeName) { + cli_error(cli, "No shape name given"); + return CLI_ERROR; + } else if (strcmp(shapeName, "triangle") == 0) { + numSides = 3; + } else if (strcmp(shapeName, "rectangle") == 0) { + numSides = 4; + } else { + cli_error(cli, "Unrecognized shape given"); + return CLI_ERROR; + } + for (i = 1; i <= numSides; i++) { + char sidename[50], *value; + int length; + snprintf(sidename, 50, "side_%d", i); + value = cli_get_optarg_value(cli, sidename, NULL); + length = strtol(value, NULL, 10); + perimeter += length; + } + cli_print(cli, "Perimeter is %d", perimeter); + return CLI_OK; +} + +const char *KnownShapes[] = {"rectangle", "triangle", NULL}; + +int shape_completor(struct cli_def *cli, const char *name, const char *value, struct cli_comphelp *comphelp) { + const char **shape; + int rc = CLI_OK; + printf("shape_completor called with <%s>\n", value); + for (shape = KnownShapes; *shape && (rc == CLI_OK); shape++) { + if (!value || !strncmp(*shape, value, strlen(value))) { + rc = cli_add_comphelp_entry(comphelp, *shape); + } + } + return rc; +} + +int shape_validator(struct cli_def *cli, const char *name, const char *value) { + const char **shape; + + printf("shape_validator called with <%s>\n", value); + for (shape = KnownShapes; *shape; shape++) { + if (!strcmp(value, *shape)) return CLI_OK; + } + return CLI_ERROR; +} + +int verbose_validator(struct cli_def *cli, const char *name, const char *value) { + printf("verbose_validator called\n"); + return CLI_OK; +} + +int shape_transient_eval(struct cli_def *cli, const char *name, const char *value) { + printf("shape_transient_eval called with <%s>\n", value); + if (!strcmp(value, "rectangle")) { + cli_set_transient_mode(cli, MODE_POLYGON_RECTANGLE); + cli_set_optarg_value(cli, "duplicateShapeValue", value, 0); + return CLI_OK; + } else if (!strcmp(value, "triangle")) { + cli_set_transient_mode(cli, MODE_POLYGON_TRIANGLE); + cli_set_optarg_value(cli, "duplicateShapeValue", value, 0); + return CLI_OK; + } + cli_error(cli, "unrecognized value for setting %s -> %s", name, value); + return CLI_ERROR; +} + +const char *KnownColors[] = {"black", "white", "gray", "red", "blue", + "green", "lightred", "lightblue", "lightgreen", "darkred", + "darkblue", "darkgree", "lavender", "yellow", NULL}; + +int color_completor(struct cli_def *cli, const char *name, const char *word, struct cli_comphelp *comphelp) { + // Attempt to show matches against the following color strings + const char **color; + int rc = CLI_OK; + printf("color_completor called with <%s>\n", word); + for (color = KnownColors; *color && (rc == CLI_OK); color++) { + if (!word || !strncmp(*color, word, strlen(word))) { + rc = cli_add_comphelp_entry(comphelp, *color); + } + } + return rc; +} + +int color_validator(struct cli_def *cli, const char *name, const char *value) { + const char **color; + int rc = CLI_ERROR; + printf("color_validator called for %s\n", name); + for (color = KnownColors; *color; color++) { + if (!strcmp(value, *color)) return CLI_OK; + } + return rc; +} + +int side_length_validator(struct cli_def *cli, const char *name, const char *value) { + // Verify 'value' is a positive number + long len; + char *endptr; + int rc = CLI_OK; + + printf("side_length_validator called\n"); + errno = 0; + len = strtol(value, &endptr, 10); + if ((endptr == value) || (*endptr != '\0') || ((errno == ERANGE) && ((len == LONG_MIN) || (len == LONG_MAX)))) + return CLI_ERROR; + return rc; +} + +int check1_validator(struct cli_def *cli, UNUSED(const char *name), UNUSED(const char *value)) { + char *color; + char *transparent; + + printf("check1_validator called \n"); + color = cli_get_optarg_value(cli, "color", NULL); + transparent = cli_get_optarg_value(cli, "transparent", NULL); + + if (!color && !transparent) { + cli_error(cli, "\nMust supply either a color or transparent!"); + return CLI_ERROR; + } else if (color && !strcmp(color, "black") && transparent) { + cli_error(cli, "\nCan not have a transparent black object!"); + return CLI_ERROR; + } + return CLI_OK; +} + +void run_child(int x) { + struct cli_command *c; + struct cli_def *cli; // Prepare a small user context char mymessage[] = "I contain user data!"; @@ -202,8 +339,12 @@ int main() { cli_set_hostname(cli, "router"); cli_telnet_protocol(cli, 1); cli_regular(cli, regular_callback); - cli_regular_interval(cli, 5); // Defaults to 1 second - cli_set_idle_timeout_callback(cli, 60, idle_timeout); // 60 second idle timeout + + // change regular update to 5 seconds rather than default of 1 second + cli_regular_interval(cli, 5); + + // set 60 second idle timeout + cli_set_idle_timeout_callback(cli, 60, idle_timeout); cli_register_command(cli, NULL, "test", cmd_test, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); cli_register_command(cli, NULL, "simple", NULL, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); cli_register_command(cli, NULL, "simon", NULL, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); @@ -223,6 +364,36 @@ int main() { cli_register_command(cli, c, "regular", cmd_debug_regular, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, "Enable cli_regular() callback debugging"); + // Register some commands/subcommands to demonstrate opt/arg and buildmode operations + + c = cli_register_command(cli, NULL, "perimeter", cmd_perimeter, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, + "Calculate perimeter of polygon"); + cli_register_optarg(c, "transparent", CLI_CMD_OPTIONAL_FLAG, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, + "Set transparent flag", NULL, NULL, NULL); + cli_register_optarg(c, "verbose", CLI_CMD_OPTIONAL_FLAG | CLI_CMD_OPTION_MULTIPLE, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, + "Set transparent flag", NULL, NULL, NULL); + cli_register_optarg(c, "color", CLI_CMD_OPTIONAL_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, "Set color", + color_completor, color_validator, NULL); + cli_register_optarg(c, "__check1__", CLI_CMD_SPOT_CHECK, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL, NULL, + check1_validator, NULL); + cli_register_optarg(c, "shape", CLI_CMD_ARGUMENT | CLI_CMD_ALLOW_BUILDMODE, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, + "Specify shape to calclate perimeter for", shape_completor, shape_validator, + shape_transient_eval); + cli_register_optarg(c, "side_1", CLI_CMD_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_POLYGON_TRIANGLE, + "Specify side 1 length", NULL, side_length_validator, NULL); + cli_register_optarg(c, "side_1", CLI_CMD_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_POLYGON_RECTANGLE, + "Specify side 1 length", NULL, side_length_validator, NULL); + cli_register_optarg(c, "side_2", CLI_CMD_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_POLYGON_TRIANGLE, + "Specify side 2 length", NULL, side_length_validator, NULL); + cli_register_optarg(c, "side_2", CLI_CMD_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_POLYGON_RECTANGLE, + "Specify side 2 length", NULL, side_length_validator, NULL); + cli_register_optarg(c, "side_3", CLI_CMD_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_POLYGON_TRIANGLE, + "Specify side 3 length", NULL, side_length_validator, NULL); + cli_register_optarg(c, "side_3", CLI_CMD_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_POLYGON_RECTANGLE, + "Specify side 3 length", NULL, side_length_validator, NULL); + cli_register_optarg(c, "side_4", CLI_CMD_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_POLYGON_RECTANGLE, + "Specify side 4 length", NULL, side_length_validator, NULL); + // Set user context and its command cli_set_context(cli, (void *)&myctx); cli_register_command(cli, NULL, "context", cmd_context, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, @@ -242,6 +413,24 @@ int main() { fclose(fh); } } + cli_loop(cli, x); + cli_done(cli); +} + +int main() { + int s, x; + struct sockaddr_in addr; + int on = 1; + +#ifndef WIN32 + signal(SIGCHLD, SIG_IGN); +#endif +#ifdef WIN32 + if (!winsock_init()) { + printf("Error initialising winsock\n"); + return 1; + } +#endif if ((s = socket(AF_INET, SOCK_STREAM, 0)) < 0) { perror("socket"); @@ -287,15 +476,14 @@ int main() { /* child */ close(s); - cli_loop(cli, x); + run_child(x); exit(0); #else - cli_loop(cli, x); + run_child(x); shutdown(x, SD_BOTH); close(x); #endif } - cli_done(cli); return 0; } diff --git a/doc/developers-guide.md b/doc/developers-guide.md new file mode 100644 index 0000000..6f3b0de --- /dev/null +++ b/doc/developers-guide.md @@ -0,0 +1,243 @@ +## Introduction +libcli provides a telnet command-line environment which can be embedded in other programs. This environment includes useful features such as automatic authentication, history, and command-line editing. + +This guide should show you everything you need to embed libcli into your program. If you have any corrections, suggestions or modifications, please email [David Parrish](mailto:david+libcli@dparrish.com). + +## Authentication +Two methods of authentcation are supported by libcli - internal and callback. + +Internal authentication is based on a list of username / password combinations that are set up before `cli_loop()` is called. Passwords may be clear text, MD5 encrypted, or DES encrypted (requires a `{crypt}` prefix to distinguish from clear text). + +Callback based authentication calls a callback with the username and password that the user enters, and must return either `CLI_OK` or `CLI_ERROR`. This can be used for checking passwords against some other database such as LDAP. + +If neither `cli_set_auth_callback()` or `cli_allow_user()` have been called before `cli_loop()`, then authentication will be disabled and the user will not be prompted for a username / password combination. + +Authentication for the privileged state can also be defined by a static password or by a callback. Use the `cli_set_enable_callback()` or `cli_allow_enable()` functions to set the enable password. + +## Tutorial +This section will guide you through implementing libcli in a basic server. + +### Create a file libclitest.c + +```c +#include + +int main(int argc, char *argv[]) { + struct sockaddr_in servaddr; + struct cli_command *c; + struct cli_def *cli; + int on = 1, x, s; + + // Must be called first to setup data structures + cli = cli_init(); + + // Set the hostname (shown in the the prompt) + cli_set_hostname(cli, "test"); + + // Set the greeting + cli_set_banner(cli, "Welcome to the CLI test program."); + + // Enable 2 username / password combinations + cli_allow_user(cli, "fred", "nerk"); + cli_allow_user(cli, "foo", "bar"); + + // Set up a few simple one-level commands + cli_register_command(cli, NULL, "test", cmd_test, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); + cli_register_command(cli, NULL, "simple", cmd_test, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); + cli_register_command(cli, NULL, "simon", NULL, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); + + // This command takes arguments, and requires privileged mode (enable) + cli_register_command(cli, NULL, "set", cmd_set, PRIVILEGE_PRIVILEGED, MODE_EXEC, NULL); + + // Set up 2 commands "show counters" and "show junk" + c = cli_register_command(cli, NULL, "show", NULL, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); + // Note how we store the previous command and use it as the parent for this one. + cli_register_command(cli, c, "junk", cmd_test, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, NULL); + // This one has some help text + cli_register_command(cli, c, "counters", cmd_test, PRIVILEGE_UNPRIVILEGED, MODE_EXEC, "Show the counters that the system uses"); + + // Create a socket + s = socket(AF_INET, SOCK_STREAM, 0); + setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)); + + // Listen on port 12345 + memset(&servaddr, 0, sizeof(servaddr)); + servaddr.sin_family = AF_INET; + servaddr.sin_addr.s_addr = htonl(INADDR_ANY); + servaddr.sin_port = htons(12345); + bind(s, (struct sockaddr *)&servaddr, sizeof(servaddr)); + + // Wait for a connection + listen(s, 50); + + while ((x = accept(s, NULL, 0))) { + // Pass the connection off to libcli + cli_loop(cli, x); + close(x); + } + + // Free data structures + cli_done(cli); + + return 0; +} +``` + +This code snippet is all that's required to enable a libcli program. However it's not yet compilable because we haven't created the callback functions. + +A few commands have been created: + +* `test` +* `simple` +* `set` +* `show junk` +* `show counters` + +Note that `simon` isn't on this list because `callback` was `NULL` when it was registered, so the command will not be available. + +Also, the standard libcli commands `help`, `exit`, `logout`, `quit` and `history` are also available automatically. + +Make this program complete by adding the callback functions + +```c +int cmd_test(struct cli_def *cli, char *command, char *argv[], int argc) { + cli_print(cli, "called %s with %s\r\n", __FUNCTION__, command); + return CLI_OK; +} + +int cmd_set(struct cli_def *cli, char *command, char *argv[], int argc) { + if (argc < 2) { + cli_print(cli, "Specify a variable to set\r\n"); + return CLI_OK; + } + cli_print(cli, "Setting %s to %s\r\n", argv[0], argv[1]); + return CLI_OK; +} +``` + +2 callback functions are defined here, `cmd_test()` and `cmd_set()`. `cmd_test()` is called by many of the commands defined in the tutorial, although in reality you would usually use a callback for a single command. + +`cmd_test()` simply echos the command entered back to the client. Note that it shows the full expanded command, so you can enter "`te`" at the prompt and it will print back "`called with test`". + +`cmd_set()` handles the arguments given on the command line. This allows you to use a single callback to handle lots of arguments like: + +* `set colour green` +* `set name David` +* `set email "I don't have an e-mail address"` +* etc... + +Compile the code + +```sh +gcc libclitest.c -o libclitest -lcli +``` + +You can now run the program with `./libclitest` and telnet to port 12345 to see your work in action. + +## Function Reference + +### cli\_init() +This must be called before any other cli_xxx function. It sets up the internal data structures used for command-line processing. + +Returns a `struct cli_def *` which must be passed to all other `cli_xxx` functions. + +### cli\_done(struct cli\_def \*cli) +This is optional, but it's a good idea to call this when you are finished with libcli. This frees memory used by libcli. + +### cli\_register\_command(struct cli\_def \*cli, struct cli\_command *parent, char *command, int (*callback)(struct cli\_def *, char *, char **, int), int privilege, int mode, char *help) +Add a command to the internal command tree. Returns a `struct cli_command *`, which you can pass as parent to another call to `cli_register_command()`. + +When the command has been entered by the user, callback is checked. If it is not `NULL`, then the callback is called with: + +`struct cli_def *` - the handle of the cli structure. This must be passed to all cli functions, including `cli_print()`. +`char *` - the entire command which was entered. This is after command expansion. +`char **` - the list of arguments entered +`int` - the number of arguments entered +The callback must return `CLI_OK` if the command was successful, `CLI_ERROR` if processing wasn't successful and the next matching command should be tried (if any), or `CLI_QUIT` to drop the connection (e.g. on a fatal error). + +If parent is `NULL`, the command is added to the top level of commands, otherwise it is a subcommand of parent. + +privilege should be set to either PRIVILEGE\_PRIVILEGED or PRIVILEGE\_UNPRIVILEGED. If set to PRIVILEGE\_PRIVILEGED then the user must have entered enable before running this command. + +`mode` should be set to `MODE_EXEC` for no configuration mode, `MODE_CONFIG` for generic configuration commands, or your own config level. The user can enter the generic configuration level by entering configure terminal, and can return to `MODE_EXEC` by entering exit or CTRL-Z. You can define commands to enter your own configuration levels, which should call the `cli_set_configmode()` function. + +If help is provided, it is given to the user when the use the help command or press ?. + +### cli\_unregister\_command(struct cli\_def \*cli, char *command) +Remove a command command and all children. There is not provision yet for removing commands at lower than the top level. + +### cli\_loop(struct cli\_def \*cli, int sockfd) +The main loop of the command-line environment. This must be called with the FD of a socket open for bi-directional communication (sockfd). + +### cli\_loop() handles the telnet negotiation and authentication. It returns only when the connection is finished, either by a server or client disconnect. +Returns `CLI_OK`. + +### cli\_set\_auth\_callback(struct cli\_def \*cli, int (*auth\_callback)(char *, char *)) +Enables or disables callback based authentication. + +If `auth_callback` is not `NULL`, then authentication will be required on connection. `auth_callback` will be called with the username and password that the user enters. + +`auth_callback` must return a non-zero value if authentication is successful. + +If `auth_callback` is `NULL`, then callback based authentication will be disabled. + +### cli\_allow\_user(struct cli\_def \*cli, char *username, char *password) +Enables internal authentication, and adds username/password to the list of allowed users. + +The internal list of users will be checked before callback based authentication is tried. + +### cli\_deny\_user(struct cli\_def \*cli, char *username) +Removes username/password from the list of allowed users. + +If this is the last combination in the list, then internal authentication will be disabled. + +### cli\_set\_banner(struct cli\_def \*cli, char *banner) +Sets the greeting that clients will be presented with when they connect. This may be a security warning for example. + +If this function is not called or called with a `NULL` argument, no banner will be presented. + +### cli\_set\_hostname(struct cli\_def \*cli, char *hostname) +Sets the hostname to be displayed as the first part of the prompt. + +### cli\_regular(struct cli\_def \*cli, int(*callback)(struct cli\_def *)) +Adds a callback function which will be called every second that a user is connected to the cli. This can be used for regular processing such as debugging, time counting or implementing idle timeouts. + +Pass `NULL` as the callback function to disable this at runtime. + +If the callback function does not return `CLI_OK`, then the user will be disconnected. + +### cli\_file(struct cli\_def \*cli, FILE *f, int privilege, int mode) +This reads and processes every line read from f as if it were entered at the console. The privilege level will be set to privilege and mode set to mode during the processing of the file. + +### cli\_print(struct cli\_def \*cli, char *format, ...) +This function should be called for any output generated by a command callback. + +It takes a printf() style format string and a variable number of arguments. + +Be aware that any output generated by `cli_print()` will be passed through any filter currently being applied, and the output will be redirected to the `cli_print_callback()` if one has been specified. + +### cli\_error(struct cli\_def \*cli, char *format, ...) +A variant of `cli_print()` which does not have filters applied. + +### cli\_print\_callback(struct cli\_def \*cli, void (*callback)(struct cli\_def *, char *)) +Whenever `cli_print()` or `cli_error()` is called, the output generally goes to the user. If you specify a callback using this function, then the output will be sent to that callback. The function will be called once for each line, and it will be passed a single null-terminated string, without any newline characters. + +Specifying `NULL` as the callback parameter will make libcli use the default `cli_print()` function. + +### cli\_set\_enable\_callback(struct cli\_def \*cli, void (*callback)(struct cli\_def *, char *)) +Just like `cli_set_auth_callback, this takes a pointer to a callback function to authorize privileged access. However this callback only takes a single string - the password. + +### cli\_allow\_enable(struct cli\_def \*cli, char *password) +This will allow a static password to be used for the enable command. This static password will be checked before running any enable callbacks. + +Set this to `NULL` to not have a static enable password. + +### cli\_set\_configmode(struct cli\_def \*cli, int mode, char *string) +This will set the configuration mode. Once set, commands will be restricted to only ones in the selected configuration mode, plus any set to `MODE_ANY`. The previous mode value is returned. + +The string passed will be used to build the prompt in the set configuration mode. e.g. if you set the string `test`, the prompt will become: + +``` +hostname(config-test)# +``` + diff --git a/libcli.c b/libcli.c index 0b4d50b..70dd77f 100644 --- a/libcli.c +++ b/libcli.c @@ -1,3 +1,5 @@ +// vim:sw=2 tw=120 et + #ifdef WIN32 #include #include @@ -21,8 +23,6 @@ #endif #include "libcli.h" -// vim:sw=4 tw=120 et - #ifdef __GNUC__ #define UNUSED(d) d __attribute__((unused)) #else @@ -33,10 +33,7 @@ #define MATCH_INVERT 2 #ifdef WIN32 -/* - * Stupid windows has multiple namespaces for filedescriptors, with different - * read/write functions required for each .. - */ +// Stupid windows has multiple namespaces for filedescriptors, with different read/write functions required for each .. int read(int fd, void *buf, unsigned int count) { return recv(fd, buf, count, 0); } @@ -49,7 +46,7 @@ int vasprintf(char **strp, const char *fmt, va_list args) { int size; va_list argCopy; - // do initial vsnprintf on a copy of the va_list + // Do initial vsnprintf on a copy of the va_list va_copy(argCopy, args); size = vsnprintf(NULL, 0, fmt, argCopy); va_end(argCopy); @@ -90,9 +87,7 @@ int fprintf(FILE *stream, const char *fmt, ...) { return size; } -/* - * Dummy definitions to allow compilation on Windows - */ +// Dummy definitions to allow compilation on Windows int regex_dummy() { return 0; }; @@ -105,7 +100,13 @@ int regex_dummy() { #define REG_ICASE 0 #endif -enum cli_states { STATE_LOGIN, STATE_PASSWORD, STATE_NORMAL, STATE_ENABLE_PASSWORD, STATE_ENABLE }; +enum cli_states { + STATE_LOGIN, + STATE_PASSWORD, + STATE_NORMAL, + STATE_ENABLE_PASSWORD, + STATE_ENABLE, +}; struct unp { char *username; @@ -118,7 +119,7 @@ struct cli_filter_cmds { const char *help; }; -/* free and zero (to avoid double-free) */ +// Free and zero (to avoid double-free) #define free_z(p) \ do { \ if (p) { \ @@ -127,23 +128,44 @@ struct cli_filter_cmds { } \ } while (0) -int cli_match_filter_init(struct cli_def *cli, int argc, char **argv, struct cli_filter *filt); -int cli_range_filter_init(struct cli_def *cli, int argc, char **argv, struct cli_filter *filt); -int cli_count_filter_init(struct cli_def *cli, int argc, char **argv, struct cli_filter *filt); -int cli_match_filter(struct cli_def *cli, const char *string, void *data); -int cli_range_filter(struct cli_def *cli, const char *string, void *data); -int cli_count_filter(struct cli_def *cli, const char *string, void *data); - -static struct cli_filter_cmds filter_cmds[] = { - {"begin", "Begin with lines that match"}, - {"between", "Between lines that match"}, - {"count", "Count of lines"}, - {"exclude", "Exclude lines that match"}, - {"include", "Include lines that match"}, - {"grep", "Include lines that match regex (options: -v, -i, -e)"}, - {"egrep", "Include lines that match extended regex"}, - {NULL, NULL}, -}; +// Forward defines of *INTERNAL* library function as static here +static int cli_search_flags_validator(struct cli_def *cli, const char *word, const char *value); +static int cli_match_filter_init(struct cli_def *cli, int argc, char **argv, struct cli_filter *filt); +static int cli_range_filter_init(struct cli_def *cli, int argc, char **argv, struct cli_filter *filt); +static int cli_count_filter_init(struct cli_def *cli, int argc, char **argv, struct cli_filter *filt); +static int cli_match_filter(struct cli_def *cli, const char *string, void *data); +static int cli_range_filter(struct cli_def *cli, const char *string, void *data); +static int cli_count_filter(struct cli_def *cli, const char *string, void *data); +static void cli_int_parse_optargs(struct cli_def *cli, struct cli_pipeline_stage *stage, struct cli_command *cmd, + char lastchar, struct cli_comphelp *comphelp); +static int cli_int_enter_buildmode(struct cli_def *cli, struct cli_pipeline_stage *stage, char *mode_text); +static char *cli_int_buildmode_extend_cmdline(char *, char *word); +static void cli_int_free_buildmode(struct cli_def *cli); +static void cli_free_command(struct cli_def *cli, struct cli_command *cmd); +static int cli_int_unregister_command_core(struct cli_def *cli, const char *command, int command_type); +static int cli_int_unregister_buildmode_command(struct cli_def *cli, const char *command) __attribute__((unused)); +static struct cli_command *cli_int_register_buildmode_command( + struct cli_def *cli, struct cli_command *parent, const char *command, + int (*callback)(struct cli_def *cli, const char *, char **, int), int privilege, int mode, const char *help); +static int cli_int_buildmode_cmd_cback(struct cli_def *cli, const char *command, char *argv[], int argc); +static int cli_int_buildmode_flag_cback(struct cli_def *cli, const char *command, char *argv[], int argc); +static int cli_int_buildmode_flag_multiple_cback(struct cli_def *cli, const char *command, char *argv[], int argc); +static int cli_int_buildmode_cancel_cback(struct cli_def *cli, const char *command, char *argv[], int argc); +static int cli_int_buildmode_exit_cback(struct cli_def *cli, const char *command, char *argv[], int argc); +static int cli_int_buildmode_show_cback(struct cli_def *cli, const char *command, char *argv[], int argc); +static int cli_int_buildmode_unset_cback(struct cli_def *cli, const char *command, char *argv[], int argc); +static int cli_int_buildmode_unset_completor(struct cli_def *cli, const char *name, const char *word, + struct cli_comphelp *comphelp); +static int cli_int_buildmode_unset_validator(struct cli_def *cli, const char *name, const char *value); +static int cli_int_execute_buildmode(struct cli_def *cli); +static void cli_int_free_found_optargs(struct cli_optarg_pair **optarg_pair); +static void cli_int_unset_optarg_value(struct cli_def *cli, const char *name); +static struct cli_pipeline *cli_int_generate_pipeline(struct cli_def *cli, const char *command); +static int cli_int_validate_pipeline(struct cli_def *cli, struct cli_pipeline *pipeline); +static int cli_int_execute_pipeline(struct cli_def *cli, struct cli_pipeline *pipeline); +inline void cli_int_show_pipeline(struct cli_def *cli, struct cli_pipeline *pipeline); +static void cli_int_free_pipeline(struct cli_pipeline *pipeline); +static void cli_register_command_core(struct cli_def *cli, struct cli_command *parent, struct cli_command *c); static ssize_t _write(int fd, const void *buf, size_t count) { size_t written = 0; @@ -275,7 +297,7 @@ static int cli_build_shortest(struct cli_def *cli, struct cli_command *commands) c->unique_len = 1; for (p = commands; p; p = p->next) { if (c == p) continue; - + if (c->command_type != p->command_type) continue; if ((p->mode != MODE_ANY && p->mode != cli->mode) || p->privilege > cli->privilege) continue; cp = c->command; @@ -332,14 +354,54 @@ int cli_set_configmode(struct cli_def *cli, int mode, const char *config_desc) { return old; } +void cli_register_command_core(struct cli_def *cli, struct cli_command *parent, struct cli_command *c) { + struct cli_command *p = NULL; + + if (!c) return; + + c->parent = parent; + + /* + * Figure out we have a chain, or would be the first element on it. + * If we'd be the first element, assign as such. + * Otherwise find the lead element so we can trace it below. + */ + + if (parent) { + if (!parent->children) { + parent->children = c; + } else { + p = parent->children; + } + } else { + if (!cli->commands) { + cli->commands = c; + } else { + p = cli->commands; + } + } + + /* + * If we have a chain (p is not null), run down to the last element and place this command at the end + */ + for (; p && p->next; p = p->next) + ; + + if (p) { + p->next = c; + c->previous = p; + } + return; +} + struct cli_command *cli_register_command(struct cli_def *cli, struct cli_command *parent, const char *command, int (*callback)(struct cli_def *cli, const char *, char **, int), int privilege, int mode, const char *help) { - struct cli_command *c, *p; + struct cli_command *c; if (!command) return NULL; if (!(c = calloc(sizeof(struct cli_command), 1))) return NULL; - + c->command_type = CLI_REGULAR_COMMAND; c->callback = callback; c->next = NULL; if (!(c->command = strdup(command))) { @@ -347,7 +409,6 @@ struct cli_command *cli_register_command(struct cli_def *cli, struct cli_command return NULL; } - c->parent = parent; c->privilege = privilege; c->mode = mode; if (help && !(c->help = strdup(help))) { @@ -356,62 +417,75 @@ struct cli_command *cli_register_command(struct cli_def *cli, struct cli_command return NULL; } - if (parent) { - if (!parent->children) { - parent->children = c; - } else { - for (p = parent->children; p && p->next; p = p->next) - ; - if (p) p->next = c; - } - } else { - if (!cli->commands) { - cli->commands = c; - } else { - for (p = cli->commands; p && p->next; p = p->next) - ; - if (p) p->next = c; - } - } + cli_register_command_core(cli, parent, c); return c; } -static void cli_free_command(struct cli_command *cmd) { +static void cli_free_command(struct cli_def *cli, struct cli_command *cmd) { struct cli_command *c, *p; for (c = cmd->children; c;) { p = c->next; - cli_free_command(c); + cli_free_command(cli, c); c = p; } free(cmd->command); if (cmd->help) free(cmd->help); + if (cmd->optargs) cli_unregister_all_optarg(cmd); + + /* + * Ok, update the pointers of anyone who pointed to us. + * We have 3 pointers to worry about - parent, previous, and next. + * We don't have to worry about children since they've been cleared above. + * If both cli->command points to us we need to update cli->command to point to whatever command is 'next'. + * Otherwise ensure that any item before/behind us points around us. + * + * Important - there is no provision for deleting a discrete subcommand. + * For example, suppose we define foo, then bar with foo as the parent, then baz with bar as the parent. We cannot + * delete 'bar' and have a new chain of foo -> baz. + * The above freeing of children prevents this in the first place. + */ + + if (cmd == cli->commands) { + cli->commands = cmd->next; + if (cmd->next) { + cmd->next->parent = NULL; + cmd->next->previous = NULL; + } + } else { + if (cmd->previous) { + cmd->previous->next = cmd->next; + } + if (cmd->next) { + cmd->next->previous = cmd->previous; + } + } free(cmd); } -int cli_unregister_command(struct cli_def *cli, const char *command) { +int cli_int_unregister_command_core(struct cli_def *cli, const char *command, int command_type) { struct cli_command *c, *p = NULL; if (!command) return -1; if (!cli->commands) return CLI_OK; - for (c = cli->commands; c; c = c->next) { - if (strcmp(c->command, command) == 0) { - if (p) - p->next = c->next; - else - cli->commands = c->next; - - cli_free_command(c); + for (c = cli->commands; c;) { + p = c->next; + if (strcmp(c->command, command) == 0 && c->command_type == command_type) { + cli_free_command(cli, c); return CLI_OK; } - p = c; + c = p; } return CLI_OK; } +int cli_unregister_command(struct cli_def *cli, const char *command) { + return cli_int_unregister_command_core(cli, command, CLI_REGULAR_COMMAND); +} + int cli_show_help(struct cli_def *cli, struct cli_command *c) { struct cli_command *p; @@ -430,11 +504,11 @@ int cli_int_enable(struct cli_def *cli, UNUSED(const char *command), UNUSED(char if (cli->privilege == PRIVILEGE_PRIVILEGED) return CLI_OK; if (!cli->enable_password && !cli->enable_callback) { - /* no password required, set privilege immediately */ + // No password required, set privilege immediately. cli_set_privilege(cli, PRIVILEGE_PRIVILEGED); cli_set_configmode(cli, MODE_EXEC, NULL); } else { - /* require password entry */ + // Require password entry cli->state = STATE_ENABLE_PASSWORD; } @@ -519,7 +593,47 @@ struct cli_def *cli_init() { c = cli_register_command(cli, 0, "configure", 0, PRIVILEGE_PRIVILEGED, MODE_EXEC, "Enter configuration mode"); cli_register_command(cli, c, "terminal", cli_int_configure_terminal, PRIVILEGE_PRIVILEGED, MODE_EXEC, - "Configure from the terminal"); + "Conlfigure from the terminal"); + + // And now the built in filters + c = cli_register_filter(cli, "begin", cli_range_filter_init, cli_range_filter, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Begin with lines that match"); + cli_register_optarg(c, "range_start", CLI_CMD_ARGUMENT | CLI_CMD_REMAINDER_OF_LINE, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Begin showing lines that match", NULL, NULL, NULL); + + c = cli_register_filter(cli, "between", cli_range_filter_init, cli_range_filter, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Between lines that match"); + cli_register_optarg(c, "range_start", CLI_CMD_ARGUMENT, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Begin showing lines that match", NULL, NULL, NULL); + cli_register_optarg(c, "range_end", CLI_CMD_ARGUMENT | CLI_CMD_REMAINDER_OF_LINE, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Stop showing lines that match", NULL, NULL, NULL); + + cli_register_filter(cli, "count", cli_count_filter_init, cli_count_filter, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Count of lines"); + + c = cli_register_filter(cli, "exclude", cli_match_filter_init, cli_match_filter, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Exclude lines that match"); + cli_register_optarg(c, "search_pattern", CLI_CMD_ARGUMENT | CLI_CMD_REMAINDER_OF_LINE, PRIVILEGE_UNPRIVILEGED, + MODE_ANY, "Search pattern", NULL, NULL, NULL); + + c = cli_register_filter(cli, "include", cli_match_filter_init, cli_match_filter, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Include lines that match"); + cli_register_optarg(c, "search_pattern", CLI_CMD_ARGUMENT | CLI_CMD_REMAINDER_OF_LINE, PRIVILEGE_UNPRIVILEGED, + MODE_ANY, "Search pattern", NULL, NULL, NULL); + + c = cli_register_filter(cli, "grep", cli_match_filter_init, cli_match_filter, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Include lines that match regex (options: -v, -i, -e)"); + cli_register_optarg(c, "search_flags", CLI_CMD_HYPHENATED_OPTION, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Search flags (-[ivx]", NULL, cli_search_flags_validator, NULL); + cli_register_optarg(c, "search_pattern", CLI_CMD_ARGUMENT | CLI_CMD_REMAINDER_OF_LINE, PRIVILEGE_UNPRIVILEGED, + MODE_ANY, "Search pattern", NULL, NULL, NULL); + + c = cli_register_filter(cli, "egrep", cli_match_filter_init, cli_match_filter, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Include lines that match extended regex"); + cli_register_optarg(c, "search_flags", CLI_CMD_HYPHENATED_OPTION, PRIVILEGE_UNPRIVILEGED, MODE_ANY, + "Search flags (-[ivx]", NULL, cli_search_flags_validator, NULL); + cli_register_optarg(c, "search_pattern", CLI_CMD_ARGUMENT | CLI_CMD_REMAINDER_OF_LINE, PRIVILEGE_UNPRIVILEGED, + MODE_ANY, "Search pattern", NULL, NULL, NULL); cli->privilege = cli->mode = -1; cli_set_privilege(cli, PRIVILEGE_UNPRIVILEGED); @@ -534,26 +648,26 @@ struct cli_def *cli_init() { return cli; } -void cli_unregister_all(struct cli_def *cli, struct cli_command *command) { +void cli_unregister_tree(struct cli_def *cli, struct cli_command *command, int command_type) { struct cli_command *c, *p = NULL; if (!command) command = cli->commands; - if (!command) return; for (c = command; c;) { p = c->next; - - // Unregister all child commands - if (c->children) cli_unregister_all(cli, c->children); - - if (c->command) free(c->command); - if (c->help) free(c->help); - free(c); - + if (c->command_type == command_type || command_type == CLI_ANY_COMMAND) { + if (c == cli->commands) cli->commands = c->next; + // Unregister all child commands + cli_free_command(cli, c); + } c = p; } } +void cli_unregister_all(struct cli_def *cli, struct cli_command *command) { + cli_unregister_tree(cli, command, CLI_REGULAR_COMMAND); +} + int cli_done(struct cli_def *cli) { if (!cli) return CLI_OK; struct unp *u = cli->users, *n; @@ -569,9 +683,8 @@ int cli_done(struct cli_def *cli) { u = n; } - /* free all commands */ - cli_unregister_all(cli, 0); - + if (cli->buildmode) cli_int_free_buildmode(cli); + cli_unregister_tree(cli, cli->commands, CLI_ANY_COMMAND); free_z(cli->commandname); free_z(cli->modestring); free_z(cli->banner); @@ -631,7 +744,7 @@ static int cli_parse_line(const char *line, char *words[], int max_words) { if (!*p) break; - if (inquote) p++; /* skip over trailing quote */ + if (inquote) p++; // Skip over trailing quote inquote = 0; word_start = 0; @@ -676,288 +789,119 @@ static char *join_words(int argc, char **argv) { return p; } -static int cli_find_command(struct cli_def *cli, struct cli_command *commands, int num_words, char *words[], - int start_word, int filters[]) { - struct cli_command *c, *again_config = NULL, *again_any = NULL; - int c_words = num_words; - - if (filters[0]) c_words = filters[0]; - - // Deal with ? for help - if (!words[start_word]) return CLI_ERROR; - - if (words[start_word][strlen(words[start_word]) - 1] == '?') { - int l = strlen(words[start_word]) - 1; - - if (commands->parent && commands->parent->callback) - cli_error(cli, "%-20s %s", cli_command_name(cli, commands->parent), - (commands->parent->help != NULL ? commands->parent->help : "")); - - for (c = commands; c; c = c->next) { - if (strncasecmp(c->command, words[start_word], l) == 0 && (c->callback || c->children) && - cli->privilege >= c->privilege && (c->mode == cli->mode || c->mode == MODE_ANY)) - cli_error(cli, " %-20s %s", c->command, (c->help != NULL ? c->help : "")); - } - - return CLI_OK; - } - - for (c = commands; c; c = c->next) { - if (cli->privilege < c->privilege) continue; - - if (strncasecmp(c->command, words[start_word], c->unique_len)) continue; - - if (strncasecmp(c->command, words[start_word], strlen(words[start_word]))) continue; - - AGAIN: - if (c->mode == cli->mode || (c->mode == MODE_ANY && again_any != NULL)) { - int rc = CLI_OK; - int f; - struct cli_filter **filt = &cli->filters; - - // Found a word! - if (!c->children) { - // Last word - if (!c->callback) { - cli_error(cli, "No callback for \"%s\"", cli_command_name(cli, c)); - return CLI_ERROR; - } - } else { - if (start_word == c_words - 1) { - if (c->callback) goto CORRECT_CHECKS; - - cli_error(cli, "Incomplete command"); - return CLI_ERROR; - } - rc = cli_find_command(cli, c->children, num_words, words, start_word + 1, filters); - if (rc == CLI_ERROR_ARG) { - if (c->callback) { - rc = CLI_OK; - goto CORRECT_CHECKS; - } else { - cli_error(cli, "Invalid %s \"%s\"", commands->parent ? "argument" : "command", words[start_word]); - } - } - return rc; - } - - if (!c->callback) { - cli_error(cli, "Internal server error processing \"%s\"", cli_command_name(cli, c)); - return CLI_ERROR; - } - - CORRECT_CHECKS: - for (f = 0; rc == CLI_OK && filters[f]; f++) { - int n = num_words; - char **argv; - int argc; - int len; - - if (filters[f + 1]) n = filters[f + 1]; - - if (filters[f] == n - 1) { - cli_error(cli, "Missing filter"); - return CLI_ERROR; - } - - argv = words + filters[f] + 1; - argc = n - (filters[f] + 1); - len = strlen(argv[0]); - if (argv[argc - 1][strlen(argv[argc - 1]) - 1] == '?') { - if (argc == 1) { - int i; - for (i = 0; filter_cmds[i].cmd; i++) cli_error(cli, " %-20s %s", filter_cmds[i].cmd, filter_cmds[i].help); - } else { - if (argv[0][0] != 'c') // count - cli_error(cli, " WORD"); - - if (argc > 2 || argv[0][0] == 'c') // count - cli_error(cli, " "); - } - - return CLI_OK; - } - - if (argv[0][0] == 'b' && len < 3) // [beg]in, [bet]ween - { - cli_error(cli, "Ambiguous filter \"%s\" (begin, between)", argv[0]); - return CLI_ERROR; - } - *filt = calloc(sizeof(struct cli_filter), 1); - - if (!strncmp("include", argv[0], len) || !strncmp("exclude", argv[0], len) || !strncmp("grep", argv[0], len) || - !strncmp("egrep", argv[0], len)) - rc = cli_match_filter_init(cli, argc, argv, *filt); - else if (!strncmp("begin", argv[0], len) || !strncmp("between", argv[0], len)) - rc = cli_range_filter_init(cli, argc, argv, *filt); - else if (!strncmp("count", argv[0], len)) - rc = cli_count_filter_init(cli, argc, argv, *filt); - else { - cli_error(cli, "Invalid filter \"%s\"", argv[0]); - rc = CLI_ERROR; - } - - if (rc == CLI_OK) { - filt = &(*filt)->next; - } else { - free(*filt); - *filt = 0; - } - } - - if (rc == CLI_OK) - rc = c->callback(cli, cli_command_name(cli, c), words + start_word + 1, c_words - start_word - 1); - - while (cli->filters) { - struct cli_filter *filt = cli->filters; - - // call one last time to clean up - filt->filter(cli, NULL, filt->data); - cli->filters = filt->next; - free(filt); - } - - return rc; - } else if (cli->mode > MODE_CONFIG && c->mode == MODE_CONFIG) { - // command matched but from another mode, - // remember it if we fail to find correct command - again_config = c; - } else if (c->mode == MODE_ANY) { - // command matched but for any mode, - // remember it if we fail to find correct command - again_any = c; - } - } - - // drop out of config submode if we have matched command on MODE_CONFIG - if (again_config) { - c = again_config; - cli_set_configmode(cli, MODE_CONFIG, NULL); - goto AGAIN; - } - if (again_any) { - c = again_any; - goto AGAIN; - } - - if (start_word == 0) - cli_error(cli, "Invalid %s \"%s\"", commands->parent ? "argument" : "command", words[start_word]); - - return CLI_ERROR_ARG; -} - int cli_run_command(struct cli_def *cli, const char *command) { - int r; - unsigned int num_words, i, f; - char *words[CLI_MAX_LINE_WORDS] = {0}; - int filters[CLI_MAX_LINE_WORDS] = {0}; + int rc = CLI_ERROR; + struct cli_pipeline *pipeline; - if (!command) return CLI_ERROR; - while (isspace(*command)) command++; + // Split command into pipeline stages + pipeline = cli_int_generate_pipeline(cli, command); - if (!*command) return CLI_OK; + // cli_int_validate_pipeline will deal with buildmode command setup, and return CLI_BUILDMODE_START if found. + if (pipeline) rc = cli_int_validate_pipeline(cli, pipeline); - num_words = cli_parse_line(command, words, CLI_MAX_LINE_WORDS); - for (i = f = 0; i < num_words && f < CLI_MAX_LINE_WORDS - 1; i++) { - if (words[i][0] == '|') filters[f++] = i; + if (rc == CLI_OK) { + rc = cli_int_execute_pipeline(cli, pipeline); } - - filters[f] = 0; - - if (num_words) - r = cli_find_command(cli, cli->commands, num_words, words, 0, filters); - else - r = CLI_ERROR; - - for (i = 0; i < num_words; i++) free(words[i]); - - if (r == CLI_QUIT) return r; - - return CLI_OK; + cli_int_free_pipeline(pipeline); + return rc; } -static int cli_get_completions(struct cli_def *cli, const char *command, char **completions, int max_completions) { - struct cli_command *c; - struct cli_command *n; - int num_words, save_words, i, k = 0; - char *words[CLI_MAX_LINE_WORDS] = {0}; - int filter = 0; - - if (!command) return 0; - while (isspace(*command)) command++; - - save_words = num_words = cli_parse_line(command, words, sizeof(words) / sizeof(words[0])); - if (!command[0] || command[strlen(command) - 1] == ' ') num_words++; - - if (!num_words) goto out; - - for (i = 0; i < num_words; i++) { - if (words[i] && words[i][0] == '|') filter = i; - } +void cli_get_completions(struct cli_def *cli, const char *command, char lastchar, struct cli_comphelp *comphelp) { + struct cli_command *c = NULL; + struct cli_command *n = NULL; - if (filter) // complete filters - { - unsigned len = 0; + int i; + int command_type; + struct cli_pipeline *pipeline = NULL; + struct cli_pipeline_stage *stage; - if (filter < num_words - 1) // filter already completed - goto out; + if (!(pipeline = cli_int_generate_pipeline(cli, command))) goto out; - if (filter == num_words - 1) len = strlen(words[num_words - 1]); + stage = &pipeline->stage[pipeline->num_stages - 1]; - for (i = 0; filter_cmds[i].cmd && k < max_completions; i++) { - if (!len || (len < strlen(filter_cmds[i].cmd) && !strncmp(filter_cmds[i].cmd, words[num_words - 1], len))) - completions[k++] = (char *)filter_cmds[i].cmd; - } + // Check to see if either *no* input, or if the lastchar is a tab. + if ((!stage->words[0] || (command[strlen(command) - 1] == ' ')) && (stage->words[stage->num_words - 1])) + stage->num_words++; - completions[k] = NULL; - goto out; - } + if (cli->buildmode) + command_type = CLI_BUILDMODE_COMMAND; + else if (pipeline->num_stages == 1) + command_type = CLI_REGULAR_COMMAND; + else + command_type = CLI_FILTER_COMMAND; - for (c = cli->commands, i = 0; c && i < num_words && k < max_completions; c = n) { + for (c = cli->commands, i = 0; c && i < stage->num_words; c = n) { + char *strptr = NULL; n = c->next; + if (c->command_type != command_type) continue; if (cli->privilege < c->privilege) continue; - if (c->mode != cli->mode && c->mode != MODE_ANY) continue; + if (stage->words[i] && strncasecmp(c->command, stage->words[i], strlen(stage->words[i]))) continue; - if (words[i] && strncasecmp(c->command, words[i], strlen(words[i]))) continue; - - if (i < num_words - 1) { - if (strlen(words[i]) < c->unique_len) continue; + // Special case for 'buildmode' - skip if the argument for this command was seen, unless MULTIPLE flag is set + if (cli->buildmode) { + struct cli_optarg *optarg; + for (optarg = cli->buildmode->command->optargs; optarg; optarg = optarg->next) { + if (!strcmp(optarg->name, c->command)) break; + } + if (optarg && cli_find_optarg_value(cli, optarg->name, NULL) && !(optarg->flags & (CLI_CMD_OPTION_MULTIPLE))) + continue; + } + if (i < stage->num_words - 1) { + if (stage->words[i] && (strlen(stage->words[i]) < c->unique_len) && strcmp(stage->words[i], c->command)) continue; n = c->children; + + // If we have no more children, we've matched the *command* - remember this + if (!c->children) break; + i++; continue; } - completions[k++] = c->command; + if (lastchar == '?') { + if (asprintf(&strptr, " %-20s %s", c->command, (c->help) ? c->help : "") != -1) { + cli_add_comphelp_entry(comphelp, strptr); + free_z(strptr); + } + } else { + cli_add_comphelp_entry(comphelp, c->command); + } } out: - for (i = 0; i < save_words; i++) free(words[i]); + if (c) { + // Advance past first word of stage + i++; + stage->first_unmatched = i; + if (c->optargs) { + cli_int_parse_optargs(cli, stage, c, lastchar, comphelp); + } else if (lastchar == '?') { + // Special case for getting help with no defined optargs.... + comphelp->num_entries = -1; + } + } - return k; + cli_int_free_pipeline(pipeline); } static void cli_clear_line(int sockfd, char *cmd, int l, int cursor) { - // use cmd as our buffer, and overwrite contents as needed + // Use cmd as our buffer, and overwrite contents as needed. // Backspace to beginning memset((char *)cmd, '\b', cursor); _write(sockfd, cmd, cursor); - // overwrite existing cmd with spaces + // Overwrite existing cmd with spaces memset((char *)cmd, ' ', l); _write(sockfd, cmd, l); - // and backspace again to beginning + // ..and backspace again to beginning memset((char *)cmd, '\b', l); _write(sockfd, cmd, l); - // null cmd buffer + // Null cmd buffer memset((char *)cmd, 0, l); - - // reset pointers to beginning - cursor = l = 0; } void cli_reprompt(struct cli_def *cli) { @@ -976,22 +920,19 @@ void cli_regular_interval(struct cli_def *cli, int seconds) { cli->timeout_tm.tv_usec = 0; } -#define DES_PREFIX "{crypt}" /* to distinguish clear text from DES crypted */ +#define DES_PREFIX "{crypt}" // To distinguish clear text from DES crypted #define MD5_PREFIX "$1$" -static int pass_matches(const char *pass, const char *try) { +static int pass_matches(const char *pass, const char *attempt) { int des; if ((des = !strncasecmp(pass, DES_PREFIX, sizeof(DES_PREFIX) - 1))) pass += sizeof(DES_PREFIX) - 1; #ifndef WIN32 - /* - * TODO - find a small crypt(3) function for use on windows - */ - if (des || !strncmp(pass, MD5_PREFIX, sizeof(MD5_PREFIX) - 1)) try - = crypt(try, pass); + // TODO(dparrish): Find a small crypt(3) function for use on windows + if (des || !strncmp(pass, MD5_PREFIX, sizeof(MD5_PREFIX) - 1)) attempt = crypt(attempt, pass); #endif - return !strcmp(pass, try); + return !strcmp(pass, attempt); } #define CTRL(c) (c - '@') @@ -1002,12 +943,17 @@ static int show_prompt(struct cli_def *cli, int sockfd) { if (cli->hostname) len += write(sockfd, cli->hostname, strlen(cli->hostname)); if (cli->modestring) len += write(sockfd, cli->modestring, strlen(cli->modestring)); - + if (cli->buildmode) { + len += write(sockfd, "[", 1); + len += write(sockfd, cli->buildmode->cname, strlen(cli->buildmode->cname)); + len += write(sockfd, "...", 3); + if (cli->buildmode->mode_text) len += write(sockfd, cli->buildmode->mode_text, strlen(cli->buildmode->mode_text)); + len += write(sockfd, "]", 1); + } return len + write(sockfd, cli->promptchar, strlen(cli->promptchar)); } int cli_loop(struct cli_def *cli, int sockfd) { - unsigned char c; int n, l, oldl = 0, is_telnet_option = 0, skip = 0, esc = 0, cursor = 0; char *cmd = NULL, *oldcmd = 0; char *username = NULL, *password = NULL; @@ -1046,16 +992,17 @@ int cli_loop(struct cli_def *cli, int sockfd) { // Set the last action now so we don't time immediately if (cli->idle_timeout) time(&cli->last_action); - /* start off in unprivileged mode */ + // Start off in unprivileged mode cli_set_privilege(cli, PRIVILEGE_UNPRIVILEGED); cli_set_configmode(cli, MODE_EXEC, NULL); - /* no auth required? */ + // No auth required? if (!cli->users && !cli->auth_callback) cli->state = STATE_NORMAL; while (1) { signed int in_history = 0; - int lastchar = 0; + unsigned char lastchar = '\0'; + unsigned char c = '\0'; struct timeval tm; cli->showprompt = 1; @@ -1077,6 +1024,13 @@ int cli_loop(struct cli_def *cli, int sockfd) { while (1) { int sr; fd_set r; + + /* + * Ensure our transient mode is reset to the starting mode on *each* loop traversal transient mode is valid only + * while a command is being evaluated/executed. + */ + cli->transient_mode = cli->mode; + if (cli->showprompt) { if (cli->state != STATE_PASSWORD && cli->state != STATE_ENABLE_PASSWORD) _write(sockfd, "\r\n", 2); @@ -1111,16 +1065,14 @@ int cli_loop(struct cli_def *cli, int sockfd) { FD_SET(sockfd, &r); if ((sr = select(sockfd + 1, &r, NULL, NULL, &tm)) < 0) { - /* select error */ if (errno == EINTR) continue; - perror("select"); l = -1; break; } if (sr == 0) { - /* timeout every second */ + // Timeout every second if (cli->regular_callback && cli->regular_callback(cli) != CLI_OK) { l = -1; break; @@ -1185,24 +1137,24 @@ int cli_loop(struct cli_def *cli, int sockfd) { is_telnet_option = 0; } - /* handle ANSI arrows */ + // Handle ANSI arrows if (esc) { if (esc == '[') { - /* remap to readline control codes */ + // Remap to readline control codes switch (c) { - case 'A': /* Up */ + case 'A': // Up c = CTRL('P'); break; - case 'B': /* Down */ + case 'B': // Down c = CTRL('N'); break; - case 'C': /* Right */ + case 'C': // Right c = CTRL('F'); break; - case 'D': /* Left */ + case 'D': // Left c = CTRL('B'); break; @@ -1235,12 +1187,12 @@ int cli_loop(struct cli_def *cli, int sockfd) { continue; } - /* back word, backspace/delete */ + // Back word, backspace/delete if (c == CTRL('W') || c == CTRL('H') || c == 0x7f) { int back = 0; - if (c == CTRL('W')) /* word */ - { + if (c == CTRL('W')) { + // Word int nc = cursor; if (l == 0 || cursor == 0) continue; @@ -1254,8 +1206,8 @@ int cli_loop(struct cli_def *cli, int sockfd) { nc--; back++; } - } else /* char */ - { + } else { + // Char if (l == 0 || cursor == 0) { _write(sockfd, "\a", 1); continue; @@ -1272,19 +1224,18 @@ int cli_loop(struct cli_def *cli, int sockfd) { } else { int i; if (cli->state != STATE_PASSWORD && cli->state != STATE_ENABLE_PASSWORD) { - // back up one space, then write current buffer followed by a - // space + // Back up one space, then write current buffer followed by a space _write(sockfd, "\b", 1); _write(sockfd, cmd + cursor, l - cursor); _write(sockfd, " ", 1); - // move everything one char left + // Move everything one char left memmove(cmd + cursor - 1, cmd + cursor, l - cursor); - // set former last char to null + // Set former last char to null cmd[l - 1] = 0; - // and reposition cursor + // And reposition cursor for (i = l; i >= cursor; i--) _write(sockfd, "\b", 1); } cursor--; @@ -1296,7 +1247,7 @@ int cli_loop(struct cli_def *cli, int sockfd) { } } - /* redraw */ + // Redraw if (c == CTRL('L')) { int i; int cursorback = l - cursor; @@ -1312,7 +1263,7 @@ int cli_loop(struct cli_def *cli, int sockfd) { continue; } - /* clear line */ + // Clear line if (c == CTRL('U')) { if (cli->state == STATE_PASSWORD || cli->state == STATE_ENABLE_PASSWORD) memset(cmd, 0, l); @@ -1323,15 +1274,15 @@ int cli_loop(struct cli_def *cli, int sockfd) { continue; } - /* kill to EOL */ + // Kill to EOL if (c == CTRL('K')) { if (cursor == l) continue; if (cli->state != STATE_PASSWORD && cli->state != STATE_ENABLE_PASSWORD) { - int c; - for (c = cursor; c < l; c++) _write(sockfd, " ", 1); + int cptr; + for (cptr = cursor; cptr < l; cptr++) _write(sockfd, " ", 1); - for (c = cursor; c < l; c++) _write(sockfd, "\b", 1); + for (cptr = cursor; cptr < l; cptr++) _write(sockfd, "\b", 1); } memset(cmd + cursor, 0, l - cursor); @@ -1339,7 +1290,7 @@ int cli_loop(struct cli_def *cli, int sockfd) { continue; } - /* EOT */ + // EOT if (c == CTRL('D')) { if (cli->state == STATE_PASSWORD || cli->state == STATE_ENABLE_PASSWORD) break; @@ -1349,70 +1300,151 @@ int cli_loop(struct cli_def *cli, int sockfd) { break; } - /* disable */ + // Disable if (c == CTRL('Z')) { if (cli->mode != MODE_EXEC) { + if (cli->buildmode) cli_int_free_buildmode(cli); cli_clear_line(sockfd, cmd, l, cursor); cli_set_configmode(cli, MODE_EXEC, NULL); + l = cursor = 0; cli->showprompt = 1; } continue; } - /* TAB completion */ + // TAB completion if (c == CTRL('I')) { - char *completions[CLI_MAX_LINE_WORDS]; - int num_completions = 0; + struct cli_comphelp comphelp = {0}; if (cli->state == STATE_LOGIN || cli->state == STATE_PASSWORD || cli->state == STATE_ENABLE_PASSWORD) continue; - if (cursor != l) continue; - num_completions = cli_get_completions(cli, cmd, completions, CLI_MAX_LINE_WORDS); - if (num_completions == 0) { + cli_get_completions(cli, cmd, c, &comphelp); + if (comphelp.num_entries == 0) { _write(sockfd, "\a", 1); - } else if (num_completions == 1) { - // Single completion - for (; l > 0; l--, cursor--) { - if (cmd[l - 1] == ' ' || cmd[l - 1] == '|') break; - _write(sockfd, "\b", 1); - } - strcpy((cmd + l), completions[0]); - l += strlen(completions[0]); - cmd[l++] = ' '; - cursor = l; - _write(sockfd, completions[0], strlen(completions[0])); - _write(sockfd, " ", 1); } else if (lastchar == CTRL('I')) { - // double tab + // Double tab int i; - _write(sockfd, "\r\n", 2); - for (i = 0; i < num_completions; i++) { - _write(sockfd, completions[i], strlen(completions[i])); - if (i % 4 == 3) + for (i = 0; i < comphelp.num_entries; i++) { + if (i % 4 == 0) _write(sockfd, "\r\n", 2); else - _write(sockfd, " ", 1); + _write(sockfd, " ", 1); + _write(sockfd, comphelp.entries[i], strlen(comphelp.entries[i])); } - if (i % 4 != 3) _write(sockfd, "\r\n", 2); + _write(sockfd, "\r\n", 2); cli->showprompt = 1; - } else { - // More than one completion + } else if (comphelp.num_entries == 1) { + // Single completion - show *unless* the optional/required 'prefix' is present + if (comphelp.entries[0][0] != '[' && comphelp.entries[0][0] != '<') { + for (; l > 0; l--, cursor--) { + if (cmd[l - 1] == ' ' || cmd[l - 1] == '|' || (comphelp.comma_separated && cmd[l - 1] == ',')) break; + _write(sockfd, "\b", 1); + } + strcpy((cmd + l), comphelp.entries[0]); + l += strlen(comphelp.entries[0]); + cmd[l++] = ' '; + cursor = l; + _write(sockfd, comphelp.entries[0], strlen(comphelp.entries[0])); + _write(sockfd, " ", 1); + // And now forget the tab, since we just found a single match + lastchar = '\0'; + } else { + // Yes, we had a match, but it wasn't required - remember the tab in case the user double tabs.... + lastchar = CTRL('I'); + } + } else if (comphelp.num_entries > 1) { + /* + * More than one completion. + * Show as many characters as we can until the completions start to differ. + */ lastchar = c; - _write(sockfd, "\a", 1); + int i, j, k = 0; + char *tptr = comphelp.entries[0]; + + /* + * Quickly try to see where our entries differ. + * Corner cases: + * - If all entries are optional, don't show *any* options unless user has provided a letter. + * - If any entry starts with '<' then don't fill in anything. + */ + + // Skip a leading '[' + k = strlen(tptr); + if (*tptr == '[') + tptr++; + else if (*tptr == '<') + k = 0; + + for (i = 1; k != 0 && i < comphelp.num_entries; i++) { + char *wptr = comphelp.entries[i]; + + if (*wptr == '[') + wptr++; + else if (*wptr == '<') + k = 0; + + for (j = 0; (j < k) && (j < (int)strlen(wptr)); j++) { + if (strncasecmp(tptr + j, wptr + j, 1)) break; + } + k = j; + } + + // Try to show minimum match string if we have a non-zero k and the first letter of the last word is not '['. + if (k && comphelp.entries[comphelp.num_entries - 1][0] != '[') { + for (; l > 0; l--, cursor--) { + if (cmd[l - 1] == ' ' || cmd[l - 1] == '|' || (comphelp.comma_separated && cmd[l - 1] == ',')) break; + _write(sockfd, "\b", 1); + } + strncpy(cmd + l, tptr, k); + l += k; + cursor = l; + _write(sockfd, tptr, k); + + } else { + _write(sockfd, "\a", 1); + } } + cli_free_comphelp(&comphelp); continue; } - /* history */ + // '?' at end of line - generate applicable 'help' messages + if (c == '?' && cursor == l) { + struct cli_comphelp comphelp = {0}; + int i; + int show_cr = 1; + + if (cli->state == STATE_LOGIN || cli->state == STATE_PASSWORD || cli->state == STATE_ENABLE_PASSWORD) continue; + if (cursor != l) continue; + + cli_get_completions(cli, cmd, c, &comphelp); + if (comphelp.num_entries == 0) { + _write(sockfd, "\a", 1); + } else if (comphelp.num_entries > 0) { + cli->showprompt = 1; + _write(sockfd, "\r\n", 2); + for (i = 0; i < (int)comphelp.num_entries; i++) { + if (comphelp.entries[i][2] != '[') show_cr = 0; + cli_error(cli, "%s", comphelp.entries[i]); + } + if (show_cr) cli_error(cli, " "); + } + + cli_free_comphelp(&comphelp); + + if (comphelp.num_entries >= 0) continue; + } + + // History if (c == CTRL('P') || c == CTRL('N')) { int history_found = 0; if (cli->state == STATE_LOGIN || cli->state == STATE_PASSWORD || cli->state == STATE_ENABLE_PASSWORD) continue; - if (c == CTRL('P')) // Up - { + if (c == CTRL('P')) { + // Up in_history--; if (in_history < 0) { for (in_history = MAX_HISTORY - 1; in_history >= 0; in_history--) { @@ -1424,8 +1456,8 @@ int cli_loop(struct cli_def *cli, int sockfd) { } else { if (cli->history[in_history]) history_found = 1; } - } else // Down - { + } else { + // Down in_history++; if (in_history >= MAX_HISTORY || !cli->history[in_history]) { int i = 0; @@ -1452,17 +1484,17 @@ int cli_loop(struct cli_def *cli, int sockfd) { continue; } - /* left/right cursor motion */ + // Left/right cursor motion if (c == CTRL('B') || c == CTRL('F')) { - if (c == CTRL('B')) /* Left */ - { + if (c == CTRL('B')) { + // Left if (cursor) { if (cli->state != STATE_PASSWORD && cli->state != STATE_ENABLE_PASSWORD) _write(sockfd, "\b", 1); cursor--; } - } else /* Right */ - { + } else { + // Right if (cursor < l) { if (cli->state != STATE_PASSWORD && cli->state != STATE_ENABLE_PASSWORD) _write(sockfd, &cmd[cursor], 1); @@ -1473,8 +1505,8 @@ int cli_loop(struct cli_def *cli, int sockfd) { continue; } - /* start of line */ if (c == CTRL('A')) { + // Start of line if (cursor) { if (cli->state != STATE_PASSWORD && cli->state != STATE_ENABLE_PASSWORD) { _write(sockfd, "\r", 1); @@ -1487,8 +1519,8 @@ int cli_loop(struct cli_def *cli, int sockfd) { continue; } - /* end of line */ if (c == CTRL('E')) { + // End of line if (cursor < l) { if (cli->state != STATE_PASSWORD && cli->state != STATE_ENABLE_PASSWORD) _write(sockfd, &cmd[cursor], l - cursor); @@ -1499,15 +1531,15 @@ int cli_loop(struct cli_def *cli, int sockfd) { continue; } - /* normal character typed */ if (cursor == l) { - /* append to end of line if not at end-of-buffer */ + // Normal character typed. + // Append to end of line if not at end-of-buffer. if (l < CLI_MAX_LINE_LENGTH - 1) { cmd[cursor] = c; l++; cursor++; } else { - // end-of-buffer, ensure null terminated + // End-of-buffer, ensure null terminated cmd[cursor] = 0; _write(sockfd, "\a", 1); continue; @@ -1521,8 +1553,7 @@ int cli_loop(struct cli_def *cli, int sockfd) { // Insert new character cmd[cursor] = c; - // IMPORTANT - if at end of buffer, set last char to NULL and don't - // change length otherwise bump length by 1 + // IMPORTANT - if at end of buffer, set last char to NULL and don't change length, otherwise bump length by 1 if (l == CLI_MAX_LINE_LENGTH - 1) { cmd[l] = 0; } else { @@ -1556,13 +1587,13 @@ int cli_loop(struct cli_def *cli, int sockfd) { if (cli->state == STATE_LOGIN) { if (l == 0) continue; - /* require login */ + // Require login free_z(username); if (!(username = strdup(cmd))) return 0; cli->state = STATE_PASSWORD; cli->showprompt = 1; } else if (cli->state == STATE_PASSWORD) { - /* require password */ + // Require password int allowed = 0; free_z(password); @@ -1595,12 +1626,12 @@ int cli_loop(struct cli_def *cli, int sockfd) { } else if (cli->state == STATE_ENABLE_PASSWORD) { int allowed = 0; if (cli->enable_password) { - /* check stored static enable password */ + // Check stored static enable password if (pass_matches(cli->enable_password, cmd)) allowed++; } if (!allowed && cli->enable_callback) { - /* check callback */ + // Check callback if (cli->enable_callback(cmd)) allowed++; } @@ -1612,14 +1643,40 @@ int cli_loop(struct cli_def *cli, int sockfd) { cli->state = STATE_NORMAL; } } else { + int rc; if (l == 0) continue; if (cmd[l - 1] != '?' && strcasecmp(cmd, "history") != 0) cli_add_history(cli, cmd); - if (cli_run_command(cli, cmd) == CLI_QUIT) break; + rc = cli_run_command(cli, cmd); + switch (rc) { + case CLI_BUILDMODE_ERROR: + // Unable to enter buildmode successfully + cli_print(cli, "Failure entering build mode for '%s'", cli->buildmode->cname); + cli_int_free_buildmode(cli); + continue; + case CLI_BUILDMODE_CANCEL: + // Called if user enters 'cancel' + cli_print(cli, "Canceling build mode for '%s'", cli->buildmode->cname); + cli_int_free_buildmode(cli); + break; + case CLI_BUILDMODE_EXIT: + // Called when user enters exit - rebuild *entire* command line. + // Recall all located optargs + cli->found_optargs = cli->buildmode->found_optargs; + rc = cli_int_execute_buildmode(cli); + case CLI_QUIT: + break; + case CLI_BUILDMODE_START: + case CLI_BUILDMODE_EXTEND: + default: + break; + } + + // Process is done if we get a CLI_QUIT, + if (rc == CLI_QUIT) break; } - // Update the last_action time now as the last command run could take a - // long time to return + // Update the last_action time now as the last command run could take a long time to return if (cli->idle_timeout) time(&cli->last_action); } @@ -1643,7 +1700,8 @@ int cli_file(struct cli_def *cli, FILE *fh, int privilege, int mode) { char *cmd; char *end; - if (fgets(buf, CLI_MAX_LINE_LENGTH - 1, fh) == NULL) break; /* end of file */ + // End of file + if (fgets(buf, CLI_MAX_LINE_LENGTH - 1, fh) == NULL) break; if ((p = strpbrk(buf, "#\r\n"))) *p = 0; @@ -1662,7 +1720,7 @@ int cli_file(struct cli_def *cli, FILE *fh, int privilege, int mode) { } cli_set_privilege(cli, oldpriv); - cli_set_configmode(cli, oldmode, NULL /* didn't save desc */); + cli_set_configmode(cli, oldmode, NULL); return CLI_OK; } @@ -1671,7 +1729,7 @@ static void _print(struct cli_def *cli, int print_mode, const char *format, va_l int n; char *p = NULL; - if (!cli) return; // sanity check + if (!cli) return; n = vasprintf(&p, format, ap); if (n < 0) return; @@ -1746,79 +1804,66 @@ struct cli_match_filter_state { } match; }; +int cli_search_flags_validator(struct cli_def *cli, const char *word, const char *value) { + // Valid search flags starts with a hyphen, then any number of i, v, or e characters. + + if ((*value++ == '-') && (*value) && (strspn(value, "vie") == strlen(value))) return CLI_OK; + return CLI_ERROR; +} + int cli_match_filter_init(struct cli_def *cli, int argc, char **argv, struct cli_filter *filt) { struct cli_match_filter_state *state; - int rflags; - int i; - char *p; - - if (argc < 2) { - if (cli->client) fprintf(cli->client, "Match filter requires an argument\r\n"); - - return CLI_ERROR; - } + char *search_pattern = cli_get_optarg_value(cli, "search_pattern", NULL); + char *search_flags = cli_get_optarg_value(cli, "search_flags", NULL); filt->filter = cli_match_filter; filt->data = state = calloc(sizeof(struct cli_match_filter_state), 1); + if (!state) return CLI_ERROR; - if (argv[0][0] == 'i' || (argv[0][0] == 'e' && argv[0][1] == 'x')) // include/exclude - { - if (argv[0][0] == 'e') state->flags = MATCH_INVERT; - - state->match.string = join_words(argc - 1, argv + 1); - return CLI_OK; - } - -#ifdef WIN32 - /* - * No regex functions in windows, so return an error - */ - return CLI_ERROR; -#endif - - state->flags = MATCH_REGEX; - - // grep/egrep - rflags = REG_NOSUB; - if (argv[0][0] == 'e') // egrep - rflags |= REG_EXTENDED; - - i = 1; - while (i < argc - 1 && argv[i][0] == '-' && argv[i][1]) { - int last = 0; - p = &argv[i][1]; - - if (strspn(p, "vie") != strlen(p)) break; - - while (*p) { - switch (*p++) { - case 'v': - state->flags |= MATCH_INVERT; - break; + if (!strcmp(cli->pipeline->current_stage->words[0], "include")) { + state->match.string = strdup(search_pattern); + } else if (!strcmp(cli->pipeline->current_stage->words[0], "exclude")) { + state->match.string = strdup(search_pattern); + state->flags = MATCH_INVERT; +#ifndef WIN32 + } else { + int rflags = REG_NOSUB; + if (!strcmp(cli->pipeline->current_stage->words[0], "grep")) { + state->flags = MATCH_REGEX; + } else if (!strcmp(cli->pipeline->current_stage->words[0], "egrep")) { + state->flags = MATCH_REGEX; + rflags |= REG_EXTENDED; + } + if (search_flags) { + char *p = search_flags++; + while (*p) { + switch (*p++) { + case 'v': + state->flags |= MATCH_INVERT; + break; - case 'i': - rflags |= REG_ICASE; - break; + case 'i': + rflags |= REG_ICASE; + break; - case 'e': - last++; - break; + case 'e': + // Implies next term is search string, so stop processing flags + break; + } } } - - i++; - if (last) break; + if (regcomp(&state->match.re, search_pattern, rflags)) { + if (cli->client) fprintf(cli->client, "Invalid pattern \"%s\"\r\n", search_pattern); + return CLI_ERROR; + } } - - p = join_words(argc - i, argv + i); - if ((i = regcomp(&state->match.re, p, rflags))) { - if (cli->client) fprintf(cli->client, "Invalid pattern \"%s\"\r\n", p); - - free_z(p); +#else + } else { + // No regex functions in windows, so return an error. return CLI_ERROR; } +#endif - free_z(p); return CLI_OK; } @@ -1826,8 +1871,7 @@ int cli_match_filter(UNUSED(struct cli_def *cli), const char *string, void *data struct cli_match_filter_state *state = data; int r = CLI_ERROR; - if (!string) // clean up - { + if (!string) { if (state->flags & MATCH_REGEX) regfree(&state->match.re); else @@ -1861,45 +1905,28 @@ struct cli_range_filter_state { int cli_range_filter_init(struct cli_def *cli, int argc, char **argv, struct cli_filter *filt) { struct cli_range_filter_state *state; - char *from = 0; - char *to = 0; - - if (!strncmp(argv[0], "bet", 3)) // between - { - if (argc < 3) { - if (cli->client) fprintf(cli->client, "Between filter requires 2 arguments\r\n"); - - return CLI_ERROR; - } - - if (!(from = strdup(argv[1]))) return CLI_ERROR; - to = join_words(argc - 2, argv + 2); - } else // begin - { - if (argc < 2) { - if (cli->client) fprintf(cli->client, "Begin filter requires an argument\r\n"); - - return CLI_ERROR; - } - - from = join_words(argc - 1, argv + 1); - } + char *from = strdup(cli_get_optarg_value(cli, "range_start", NULL)); + char *to = strdup(cli_get_optarg_value(cli, "range_end", NULL)); + // Do not have to check from/to since we would not have gotten here if we were missing a required argument. filt->filter = cli_range_filter; filt->data = state = calloc(sizeof(struct cli_range_filter_state), 1); - - state->from = from; - state->to = to; - - return CLI_OK; + if (state) { + state->from = from; + state->to = to; + return CLI_OK; + } else { + free_z(from); + free_z(to); + return CLI_ERROR; + } } int cli_range_filter(UNUSED(struct cli_def *cli), const char *string, void *data) { struct cli_range_filter_state *state = data; int r = CLI_ERROR; - if (!string) // clean up - { + if (!string) { free_z(state->from); free_z(state->to); free_z(state); @@ -1932,9 +1959,8 @@ int cli_count_filter_init(struct cli_def *cli, int argc, UNUSED(char **argv), st int cli_count_filter(struct cli_def *cli, const char *string, void *data) { int *count = data; - if (!string) // clean up - { - // print count + if (!string) { + // Print count if (cli->client) fprintf(cli->client, "%d\r\n", *count); free(count); @@ -1943,9 +1969,10 @@ int cli_count_filter(struct cli_def *cli, const char *string, void *data) { while (isspace(*string)) string++; - if (*string) (*count)++; // only count non-blank lines + // Only count non-blank lines + if (*string) (*count)++; - return CLI_ERROR; // no output + return CLI_ERROR; } void cli_print_callback(struct cli_def *cli, void (*callback)(struct cli_def *, const char *)) { @@ -1966,7 +1993,6 @@ void cli_set_idle_timeout_callback(struct cli_def *cli, unsigned int seconds, in void cli_telnet_protocol(struct cli_def *cli, int telnet_protocol) { cli->telnet_protocol = !!telnet_protocol; } - void cli_set_context(struct cli_def *cli, void *context) { cli->user_context = context; } @@ -1974,3 +2000,1145 @@ void cli_set_context(struct cli_def *cli, void *context) { void *cli_get_context(struct cli_def *cli) { return cli->user_context; } + +struct cli_command *cli_register_filter(struct cli_def *cli, const char *command, + int (*init)(struct cli_def *cli, int, char **, struct cli_filter *), + int (*filter)(struct cli_def *, const char *, void *), int privilege, int mode, + const char *help) { + struct cli_command *c; + + if (!command) return NULL; + if (!(c = calloc(sizeof(struct cli_command), 1))) return NULL; + + c->command_type = CLI_FILTER_COMMAND; + c->init = init; + c->filter = filter; + c->next = NULL; + if (!(c->command = strdup(command))) { + free(c); + return NULL; + } + + c->privilege = privilege; + c->mode = mode; + if (help && !(c->help = strdup(help))) { + free(c->command); + free(c); + return NULL; + } + + // Filters are all registered at the top level. + cli_register_command_core(cli, NULL, c); + return c; +} + +int cli_unregister_filter(struct cli_def *cli, const char *command) { + return cli_int_unregister_command_core(cli, command, CLI_FILTER_COMMAND); +} + +void cli_int_free_found_optargs(struct cli_optarg_pair **optarg_pair) { + struct cli_optarg_pair *c; + + if (!optarg_pair || !*optarg_pair) return; + + for (c = *optarg_pair; c;) { + *optarg_pair = c->next; + free_z(c->name); + free_z(c->value); + free_z(c); + c = *optarg_pair; + } +} + +char *cli_find_optarg_value(struct cli_def *cli, char *name, char *find_after) { + char *value = NULL; + struct cli_optarg_pair *optarg_pair; + if (!name || !cli->found_optargs) return NULL; + + for (optarg_pair = cli->found_optargs; optarg_pair && !value; optarg_pair = optarg_pair->next) { + if (strcmp(optarg_pair->name, name) == 0) { + if (find_after && find_after == optarg_pair->value) { + find_after = NULL; + continue; + } + value = optarg_pair->value; + } + } + return value; +} + +static void cli_optarg_build_shortest(struct cli_optarg *optarg) { + struct cli_optarg *c, *p; + char *cp, *pp; + unsigned int len; + + for (c = optarg; c; c = c->next) { + c->unique_len = 1; + for (p = optarg; p; p = p->next) { + if (c == p) continue; + cp = c->name; + pp = p->name; + len = 1; + while (*cp && *pp && *cp++ == *pp++) len++; + if (len > c->unique_len) c->unique_len = len; + } + } +} + +void cli_free_optarg(struct cli_optarg *optarg) { + free_z(optarg->help); + free_z(optarg->name); + free_z(optarg); +} + +int cli_register_optarg(struct cli_command *cmd, const char *name, int flags, int privilege, int mode, const char *help, + int (*get_completions)(struct cli_def *cli, const char *, const char *, struct cli_comphelp *), + int (*validator)(struct cli_def *cli, const char *, const char *), + int (*transient_mode)(struct cli_def *cli, const char *, const char *)) { + struct cli_optarg *optarg; + struct cli_optarg *lastopt = NULL; + struct cli_optarg *ptr = NULL; + int retval = CLI_ERROR; + + // Name must not already exist with this priv/mode + for (ptr = cmd->optargs, lastopt = NULL; ptr; lastopt = ptr, ptr = ptr->next) { + if (!strcmp(name, ptr->name) && ptr->mode == mode && ptr->privilege == privilege) { + return CLI_ERROR; + } + } + if (!(optarg = calloc(sizeof(struct cli_optarg), 1))) goto CLEANUP; + if (!(optarg->name = strdup(name))) goto CLEANUP; + if (help && !(optarg->help = strdup(help))) goto CLEANUP; + + optarg->mode = mode; + optarg->privilege = privilege; + optarg->get_completions = get_completions; + optarg->validator = validator; + optarg->transient_mode = transient_mode; + optarg->flags = flags; + + if (lastopt) + lastopt->next = optarg; + else + cmd->optargs = optarg; + cli_optarg_build_shortest(cmd->optargs); + retval = CLI_OK; + +CLEANUP: + if (retval != CLI_OK) { + cli_free_optarg(optarg); + } + return retval; +} + +int cli_unregister_optarg(struct cli_command *cmd, const char *name) { + struct cli_optarg *ptr; + struct cli_optarg *lastptr; + int retval = CLI_ERROR; + // Iterate looking for this option name, stopping at end or if name matches + for (lastptr = NULL, ptr = cmd->optargs; ptr && strcmp(ptr->name, name); lastptr = ptr, ptr = ptr->next) + ; + + // If ptr, then we found the optarg to delete + if (ptr) { + if (lastptr) { + // Not first optarg + lastptr->next = ptr->next; + ptr->next = NULL; + } else { + // First optarg + cmd->optargs = ptr->next; + ptr->next = NULL; + } + cli_free_optarg(ptr); + cli_optarg_build_shortest(cmd->optargs); + retval = CLI_OK; + } + return retval; +} + +void cli_unregister_all_optarg(struct cli_command *c) { + struct cli_optarg *o, *p; + + for (o = c->optargs; o; o = p) { + p = o->next; + cli_free_optarg(o); + } +} + +void cli_int_unset_optarg_value(struct cli_def *cli, const char *name) { + struct cli_optarg_pair **p, *c; + for (p = &cli->found_optargs, c = *p; *p;) { + c = *p; + + if (!strcmp(c->name, name)) { + *p = c->next; + free_z(c->name); + free_z(c->value); + free_z(c); + } else { + p = &(*p)->next; + } + } +} + +int cli_set_optarg_value(struct cli_def *cli, const char *name, const char *value, int allow_multiple) { + struct cli_optarg_pair *optarg_pair, **anchor; + int rc = CLI_ERROR; + + for (optarg_pair = cli->found_optargs, anchor = &cli->found_optargs; optarg_pair; + anchor = &optarg_pair->next, optarg_pair = optarg_pair->next) { + // Break if we found this name *and* allow_multiple is false + if (!strcmp(optarg_pair->name, name) && !allow_multiple) { + break; + } + } + // If we *didn't* find this, then allocate a new entry before proceeding + if (!optarg_pair) { + optarg_pair = (struct cli_optarg_pair *)calloc(1, sizeof(struct cli_optarg_pair)); + *anchor = optarg_pair; + } + // Set the value + if (optarg_pair) { + // Name is null only if we didn't find it + if (!optarg_pair->name) optarg_pair->name = strdup(name); + + // Value may be overwritten, so free any old value. + if (optarg_pair->value) free_z(optarg_pair->value); + optarg_pair->value = strdup(value); + + rc = CLI_OK; + } + return rc; +} + +struct cli_optarg_pair *cli_get_all_found_optargs(struct cli_def *cli) { + if (cli) return cli->found_optargs; + return NULL; +} + +char *cli_get_optarg_value(struct cli_def *cli, const char *name, char *find_after) { + char *value = NULL; + struct cli_optarg_pair *optarg_pair; + + for (optarg_pair = cli->found_optargs; !value && optarg_pair; optarg_pair = optarg_pair->next) { + // Check next entry if this isn't our name + if (strcasecmp(optarg_pair->name, name)) continue; + + // Did we have a find_after, then ignore anything up until our find_after match + if (find_after && optarg_pair->value == find_after) { + find_after = NULL; + continue; + } else if (!find_after) { + value = optarg_pair->value; + } + } + return value; +} + +void cli_int_free_buildmode(struct cli_def *cli) { + if (!cli || !cli->buildmode) return; + cli_unregister_tree(cli, cli->commands, CLI_BUILDMODE_COMMAND); + cli->mode = cli->buildmode->mode; + free_z(cli->buildmode->cname); + free_z(cli->buildmode->mode_text); + cli_int_free_found_optargs(&cli->buildmode->found_optargs); + free_z(cli->buildmode); +} + +int cli_int_enter_buildmode(struct cli_def *cli, struct cli_pipeline_stage *stage, char *mode_text) { + struct cli_optarg *optarg; + struct cli_command *c; + struct cli_buildmode *buildmode; + int rc = CLI_BUILDMODE_START; + + if (!cli || !(buildmode = (struct cli_buildmode *)calloc(1, sizeof(struct cli_buildmode)))) { + cli_error(cli, "Unable to build buildmode mode for command %s", stage->command->command); + rc = CLI_BUILDMODE_ERROR; + goto out; + } + + // Clean up any shrapnel from earlier - shouldn't be any but.... + if (cli->buildmode) cli_int_free_buildmode(cli); + + // Assign it so cli_int_register_buildmode_command() has something to work with + cli->buildmode = buildmode; + cli->buildmode->mode = cli->mode; + cli->buildmode->transient_mode = cli->transient_mode; + if (mode_text) cli->buildmode->mode_text = strdup(mode_text); + + // Need this to verify we have all *required* arguments + cli->buildmode->command = stage->command; + + // Build new *limited* list of commands from this commands optargs + for (optarg = stage->command->optargs; optarg; optarg = optarg->next) { + // Don't allow anything that could redefine our mode or buildmode mode, or redefine exit/cancel/show/unset + if (!strcmp(optarg->name, "cancel") || !strcmp(optarg->name, "exit") || !strcmp(optarg->name, "show") || + !strcmp(optarg->name, "unset")) { + cli_error(cli, "Default buildmode command conflicts with optarg named %s", optarg->name); + rc = CLI_BUILDMODE_ERROR; + goto out; + } + if (optarg->flags & (CLI_CMD_ALLOW_BUILDMODE | CLI_CMD_TRANSIENT_MODE)) continue; + if (optarg->mode != cli->mode && optarg->mode != cli->transient_mode) + continue; + else if (optarg->flags & (CLI_CMD_OPTIONAL_ARGUMENT | CLI_CMD_ARGUMENT)) { + if ((c = cli_int_register_buildmode_command(cli, NULL, optarg->name, cli_int_buildmode_cmd_cback, + optarg->privilege, cli->mode, optarg->help))) { + cli_register_optarg(c, optarg->name, CLI_CMD_ARGUMENT | (optarg->flags & CLI_CMD_OPTION_MULTIPLE), + optarg->privilege, cli->mode, optarg->help, optarg->get_completions, optarg->validator, + NULL); + } else { + rc = CLI_BUILDMODE_ERROR; + goto out; + } + } else { + if (optarg->flags & CLI_CMD_OPTION_MULTIPLE) { + if (!cli_int_register_buildmode_command(cli, NULL, optarg->name, cli_int_buildmode_flag_multiple_cback, + optarg->privilege, cli->mode, optarg->help)) { + rc = CLI_BUILDMODE_ERROR; + goto out; + } + } else { + if (!cli_int_register_buildmode_command(cli, NULL, optarg->name, cli_int_buildmode_flag_cback, + optarg->privilege, cli->mode, optarg->help)) { + rc = CLI_BUILDMODE_ERROR; + goto out; + } + } + } + } + cli->buildmode->cname = strdup(cli_command_name(cli, stage->command)); + // And lastly four 'always there' commands to cancel current mode and to execute the command, show settings, and unset + cli_int_register_buildmode_command(cli, NULL, "cancel", cli_int_buildmode_cancel_cback, PRIVILEGE_UNPRIVILEGED, + cli->mode, "Cancel command"); + cli_int_register_buildmode_command(cli, NULL, "exit", cli_int_buildmode_exit_cback, PRIVILEGE_UNPRIVILEGED, cli->mode, + "Exit and execute command"); + cli_int_register_buildmode_command(cli, NULL, "show", cli_int_buildmode_show_cback, PRIVILEGE_UNPRIVILEGED, cli->mode, + "Show current settings"); + c = cli_int_register_buildmode_command(cli, NULL, "unset", cli_int_buildmode_unset_cback, PRIVILEGE_UNPRIVILEGED, + cli->mode, "Unset a setting"); + cli_register_optarg(c, "setting", CLI_CMD_ARGUMENT | CLI_CMD_DO_NOT_RECORD, PRIVILEGE_UNPRIVILEGED, cli->mode, + "setting to clear", cli_int_buildmode_unset_completor, cli_int_buildmode_unset_validator, NULL); + +out: + if (rc != CLI_BUILDMODE_START) cli_int_free_buildmode(cli); + return rc; +} + +int cli_int_unregister_buildmode_command(struct cli_def *cli, const char *command) { + return cli_int_unregister_command_core(cli, command, CLI_BUILDMODE_COMMAND); +} + +struct cli_command *cli_int_register_buildmode_command(struct cli_def *cli, struct cli_command *parent, + const char *command, + int (*callback)(struct cli_def *cli, const char *, char **, int), + int privilege, int mode, const char *help) { + struct cli_command *c; + + if (!command) return NULL; + if (!(c = calloc(sizeof(struct cli_command), 1))) return NULL; + + c->callback = callback; + c->next = NULL; + if (!(c->command = strdup(command))) { + free(c); + return NULL; + } + + c->command_type = CLI_BUILDMODE_COMMAND; + c->privilege = privilege; + c->mode = mode; + if (help && !(c->help = strdup(help))) { + free(c->command); + free(c); + return NULL; + } + + // Buildmode commmands are all registered at the top level + cli_register_command_core(cli, NULL, c); + return c; +} + +int cli_int_execute_buildmode(struct cli_def *cli) { + struct cli_optarg *optarg = NULL; + int rc = CLI_OK; + char *cmdline; + char *value = NULL; + + cmdline = strdup(cli_command_name(cli, cli->buildmode->command)); + for (optarg = cli->buildmode->command->optargs; rc == CLI_OK && optarg; optarg = optarg->next) { + value = NULL; + do { + if (cli->privilege < optarg->privilege) continue; + if ((optarg->mode != cli->buildmode->mode) && (optarg->mode != cli->buildmode->transient_mode) && + (optarg->mode != MODE_ANY)) + continue; + + value = cli_get_optarg_value(cli, optarg->name, value); + if (!value && optarg->flags & CLI_CMD_ARGUMENT) { + cli_error(cli, "Missing required argument %s", optarg->name); + rc = CLI_MISSING_ARGUMENT; + } else if (value) { + if (optarg->flags & (CLI_CMD_OPTIONAL_FLAG | CLI_CMD_ARGUMENT)) { + if (!(cmdline = cli_int_buildmode_extend_cmdline(cmdline, value))) { + cli_error(cli, "Unable to append to building commandlne"); + rc = CLI_ERROR; + } + } else { + if (!(cmdline = cli_int_buildmode_extend_cmdline(cmdline, optarg->name))) { + cli_error(cli, "Unable to append to building commandlne"); + rc = CLI_ERROR; + } + if (!(cmdline = cli_int_buildmode_extend_cmdline(cmdline, value))) { + cli_error(cli, "Unable to append to building commandlne"); + rc = CLI_ERROR; + } + } + } + } while (rc == CLI_OK && value && optarg->flags & CLI_CMD_OPTION_MULTIPLE); + } + + if (rc == CLI_OK) { + cli_int_free_buildmode(cli); + cli_add_history(cli, cmdline); + rc = cli_run_command(cli, cmdline); + } + free_z(cmdline); + cli_int_free_buildmode(cli); + return rc; +} + +char *cli_int_buildmode_extend_cmdline(char *cmdline, char *word) { + char *tptr = NULL; + char *cptr = NULL; + size_t oldlen = strlen(cmdline); + size_t wordlen = strlen(word); + int add_quotes = 0; + + // Allocate enough space to hold the old string, a space, and the new string (including null terminator). + // Also include enough space for a quote around the string if it contains a whitespace character + if ((tptr = (char *)realloc(cmdline, oldlen + 1 + wordlen + 1 + 2))) { + strcat(tptr, " "); + for (cptr = word; *cptr; cptr++) { + if (isspace(*cptr)) { + add_quotes = 1; + break; + } + } + if (add_quotes) strcat(tptr, "'"); + strcat(tptr, word); + if (add_quotes) strcat(tptr, "'"); + } + return tptr; +} + +int cli_int_buildmode_cmd_cback(struct cli_def *cli, const char *command, char *argv[], int argc) { + int rc = CLI_BUILDMODE_EXTEND; + + if (argc) { + cli_error(cli, "Extra arguments on command line, command ignored."); + rc = CLI_ERROR; + } + return rc; +} + +// A 'flag' callback has no optargs, so we need to set it ourself based on *this* command +int cli_int_buildmode_flag_cback(struct cli_def *cli, const char *command, char *argv[], int argc) { + int rc = CLI_BUILDMODE_EXTEND; + + if (argc) { + cli_error(cli, "Extra arguments on command line, command ignored."); + rc = CLI_ERROR; + } + if (cli_set_optarg_value(cli, command, command, 0)) { + cli_error(cli, "Problem setting value for optional flag %s", command); + rc = CLI_ERROR; + } + return rc; +} + +// A 'flag' callback has no optargs, so we need to set it ourself based on *this* command +int cli_int_buildmode_flag_multiple_cback(struct cli_def *cli, const char *command, char *argv[], int argc) { + int rc = CLI_BUILDMODE_EXTEND; + + if (argc) { + cli_error(cli, "Extra arguments on command line, command ignored."); + rc = CLI_ERROR; + } + if (cli_set_optarg_value(cli, command, command, CLI_CMD_OPTION_MULTIPLE)) { + cli_error(cli, "Problem setting value for optional flag %s", command); + rc = CLI_ERROR; + } + + return rc; +} + +int cli_int_buildmode_cancel_cback(struct cli_def *cli, const char *command, char *argv[], int argc) { + int rc = CLI_BUILDMODE_CANCEL; + + if (argc > 0) { + cli_error(cli, "Extra arguments on command line, command ignored."); + rc = CLI_ERROR; + } + return rc; +} + +int cli_int_buildmode_exit_cback(struct cli_def *cli, const char *command, char *argv[], int argc) { + int rc = CLI_BUILDMODE_EXIT; + + if (argc > 0) { + cli_error(cli, "Extra arguments on command line, command ignored."); + rc = CLI_ERROR; + } + return rc; +} + +int cli_int_buildmode_show_cback(struct cli_def *cli, const char *command, char *argv[], int argc) { + struct cli_optarg_pair *optarg_pair; + if (cli && cli->buildmode) { + for (optarg_pair = cli->found_optargs; optarg_pair; optarg_pair = optarg_pair->next) { + // Only show vars that are also current 'commands' + struct cli_command *c = cli->commands; + for (; c; c = c->next) { + if (c->command_type != CLI_BUILDMODE_COMMAND) continue; + if (!strcmp(c->command, optarg_pair->name)) { + cli_print(cli, " %-20s = %s", optarg_pair->name, optarg_pair->value); + break; + } + } + } + } + return CLI_OK; +} + +int cli_int_buildmode_unset_cback(struct cli_def *cli, const char *command, char *argv[], int argc) { + // Iterate over our 'set' variables to see if that variable is also a 'valid' command right now + struct cli_command *c; + + // Is this 'optarg' to remove one of the current commands? + for (c = cli->commands; c; c = c->next) { + if (c->command_type != CLI_BUILDMODE_COMMAND) continue; + if (cli->privilege < c->privilege) continue; + if ((cli->buildmode->mode != c->mode) && (cli->buildmode->transient_mode != c->mode) && (c->mode != MODE_ANY)) + continue; + if (strcmp(c->command, argv[0])) continue; + // Go fry anything by this name + + cli_int_unset_optarg_value(cli, argv[0]); + break; + } + + return CLI_OK; +} + +// Generate a list of variables that *have* been set +int cli_int_buildmode_unset_completor(struct cli_def *cli, const char *name, const char *word, + struct cli_comphelp *comphelp) { + return CLI_OK; +} + +int cli_int_buildmode_unset_validator(struct cli_def *cli, const char *name, const char *value) { + return CLI_OK; +} + +void cli_set_transient_mode(struct cli_def *cli, int transient_mode) { + cli->transient_mode = transient_mode; +} + +int cli_add_comphelp_entry(struct cli_comphelp *comphelp, const char *entry) { + int retval = CLI_ERROR; + if (comphelp && entry) { + char *dupelement = strdup(entry); + char **duparray = (char **)realloc((void *)comphelp->entries, sizeof(char *) * (comphelp->num_entries + 1)); + if (dupelement && duparray) { + comphelp->entries = duparray; + comphelp->entries[comphelp->num_entries++] = dupelement; + retval = CLI_OK; + } else { + free_z(dupelement); + free_z(duparray); + } + } + return retval; +} + +void cli_free_comphelp(struct cli_comphelp *comphelp) { + if (comphelp) { + int idx; + + for (idx = 0; idx < comphelp->num_entries; idx++) free_z(comphelp->entries[idx]); + free_z(comphelp->entries); + } +} + +static int cli_int_locate_command(struct cli_def *cli, struct cli_command *commands, int command_type, int start_word, + struct cli_pipeline_stage *stage) { + struct cli_command *c, *again_config = NULL, *again_any = NULL; + int c_words = stage->num_words; + + for (c = commands; c; c = c->next) { + if (c->command_type != command_type) continue; + if (cli->privilege < c->privilege) continue; + + if (strncasecmp(c->command, stage->words[start_word], c->unique_len)) continue; + if (strncasecmp(c->command, stage->words[start_word], strlen(stage->words[start_word]))) continue; + + AGAIN: + if (c->mode == cli->mode || (c->mode == MODE_ANY && again_any != NULL)) { + int rc = CLI_OK; + + // Found a word! + if (!c->children) { + // Last word + if (!c->callback && !c->filter) { + cli_error(cli, "No callback for \"%s\"", cli_command_name(cli, c)); + return CLI_ERROR; + } + } else { + if (start_word == c_words - 1) { + if (c->callback) goto CORRECT_CHECKS; + + cli_error(cli, "Incomplete command"); + return CLI_ERROR; + } + rc = cli_int_locate_command(cli, c->children, command_type, start_word + 1, stage); + if (rc == CLI_ERROR_ARG) { + if (c->callback) { + rc = CLI_OK; + goto CORRECT_CHECKS; + } else { + cli_error(cli, "Invalid %s \"%s\"", commands->parent ? "argument" : "command", stage->words[start_word]); + } + } + return rc; + } + + if (!c->callback && !c->filter) { + cli_error(cli, "Internal server error processing \"%s\"", cli_command_name(cli, c)); + return CLI_ERROR; + } + + CORRECT_CHECKS: + + if (rc == CLI_OK) { + stage->command = c; + stage->first_unmatched = start_word + 1; + stage->first_optarg = stage->first_unmatched; + cli_int_parse_optargs(cli, stage, c, '\0', NULL); + rc = stage->status; + } + return rc; + } else if (cli->mode > MODE_CONFIG && c->mode == MODE_CONFIG) { + // Command matched but from another mode, remember it if we fail to find correct command + again_config = c; + } else if (c->mode == MODE_ANY) { + // Command matched but for any mode, remember it if we fail to find correct command + again_any = c; + } + } + + // Drop out of config submode if we have matched command on MODE_CONFIG + if (again_config) { + c = again_config; + goto AGAIN; + } + if (again_any) { + c = again_any; + goto AGAIN; + } + + if (start_word == 0) + cli_error(cli, "Invalid %s \"%s\"", commands->parent ? "argument" : "command", stage->words[start_word]); + + return CLI_ERROR_ARG; +} + +int cli_int_validate_pipeline(struct cli_def *cli, struct cli_pipeline *pipeline) { + int i; + int rc = CLI_OK; + int command_type; + + if (!pipeline) return CLI_ERROR; + cli->pipeline = pipeline; + + cli->found_optargs = NULL; + for (i = 0; i < pipeline->num_stages; i++) { + // In 'buildmode' we only have one pipeline, but we need to recall if we had started with any optargs + + if (cli->buildmode && i == 0) + command_type = CLI_BUILDMODE_COMMAND; + else if (i > 0) + command_type = CLI_FILTER_COMMAND; + else + command_type = CLI_REGULAR_COMMAND; + + cli->pipeline->current_stage = &pipeline->stage[i]; + if (cli->buildmode) + cli->found_optargs = cli->buildmode->found_optargs; + else + cli->found_optargs = NULL; + rc = cli_int_locate_command(cli, cli->commands, command_type, 0, &pipeline->stage[i]); + + // And save our found optargs for later use + if (cli->buildmode) + cli->buildmode->found_optargs = cli->found_optargs; + else + pipeline->stage[i].found_optargs = cli->found_optargs; + + if (rc != CLI_OK) break; + } + cli->pipeline = NULL; + + return rc; +} + +void cli_int_free_pipeline(struct cli_pipeline *pipeline) { + int i; + if (!pipeline) return; + for (i = 0; i < pipeline->num_stages; i++) cli_int_free_found_optargs(&pipeline->stage[i].found_optargs); + for (i = 0; i < pipeline->num_words; i++) free_z(pipeline->words[i]); + free_z(pipeline->cmdline); + free_z(pipeline); + pipeline = NULL; +} + +void cli_int_show_pipeline(struct cli_def *cli, struct cli_pipeline *pipeline) { + int i, j; + struct cli_pipeline_stage *stage; + char **word; + struct cli_optarg_pair *optarg_pair; + + for (i = 0, word = pipeline->words; i < pipeline->num_words; i++, word++) printf("[%s] ", *word); + fprintf(stderr, "\n"); + fprintf(stderr, "#stages=%d, #words=%d\n", pipeline->num_stages, pipeline->num_words); + + for (i = 0; i < pipeline->num_stages; i++) { + stage = &(pipeline->stage[i]); + fprintf(stderr, " #%d(%d words) first_unmatched=%d: ", i, stage->num_words, stage->first_unmatched); + for (j = 0; j < stage->num_words; j++) { + fprintf(stderr, " [%s]", stage->words[j]); + } + fprintf(stderr, "\n"); + + if (stage->command) { + fprintf(stderr, " Command: %s\n", stage->command->command); + } + for (optarg_pair = stage->found_optargs; optarg_pair; optarg_pair = optarg_pair->next) { + fprintf(stderr, " %s: %s\n", optarg_pair->name, optarg_pair->value); + } + } +} + +// Take an array of words and return a pipeline, using '|' to split command into different 'stages'. +// Pipeline is broken down by '|' characters and within each p. +struct cli_pipeline *cli_int_generate_pipeline(struct cli_def *cli, const char *command) { + int i; + struct cli_pipeline_stage *stage; + char **word; + struct cli_pipeline *pipeline = NULL; + + cli->found_optargs = NULL; + if (cli->buildmode) cli->found_optargs = cli->buildmode->found_optargs; + if (!command) return NULL; + while (*command && isspace(*command)) command++; + + if (!(pipeline = (struct cli_pipeline *)calloc(1, sizeof(struct cli_pipeline)))) return NULL; + pipeline->cmdline = (char *)strdup(command); + + pipeline->num_words = cli_parse_line(command, pipeline->words, CLI_MAX_LINE_WORDS); + + pipeline->stage[0].num_words = 0; + stage = &pipeline->stage[0]; + word = pipeline->words; + stage->words = word; + for (i = 0; i < pipeline->num_words; i++, word++) { + if (*word[0] == '|') { + if (cli->buildmode) { + // Can't allow filters in buildmode commands + cli_int_free_pipeline(pipeline); + return NULL; + } + stage->stage_num = pipeline->num_stages; + stage++; + stage->num_words = 0; + pipeline->num_stages++; + stage->words = word + 1; // First word of the next stage is one past where we are (possibly NULL) + } else { + stage->num_words++; + } + } + stage->stage_num = pipeline->num_stages; + pipeline->num_stages++; + return pipeline; +} + +int cli_int_execute_pipeline(struct cli_def *cli, struct cli_pipeline *pipeline) { + int stage_num; + int rc = CLI_OK; + struct cli_filter **filt = &cli->filters; + + if (!pipeline | !cli) return CLI_ERROR; + + cli->pipeline = pipeline; + for (stage_num = 1; stage_num < pipeline->num_stages; stage_num++) { + struct cli_pipeline_stage *stage = &pipeline->stage[stage_num]; + pipeline->current_stage = stage; + cli->found_optargs = stage->found_optargs; + *filt = calloc(sizeof(struct cli_filter), 1); + if (*filt) { + if ((rc = stage->command->init(cli, stage->num_words, stage->words, *filt) != CLI_OK)) { + break; + } + filt = &(*filt)->next; + } + } + pipeline->current_stage = NULL; + + // Did everything init? If so, execute, otherwise skip execution + if ((rc == CLI_OK) && pipeline->stage[0].command->callback) { + struct cli_pipeline_stage *stage = &pipeline->stage[0]; + + pipeline->current_stage = &pipeline->stage[0]; + if (pipeline->current_stage->command->command_type == CLI_BUILDMODE_COMMAND) + cli->found_optargs = cli->buildmode->found_optargs; + else + cli->found_optargs = pipeline->stage[0].found_optargs; + rc = stage->command->callback(cli, cli_command_name(cli, stage->command), stage->words + stage->first_unmatched, + stage->num_words - stage->first_unmatched); + if (pipeline->current_stage->command->command_type == CLI_BUILDMODE_COMMAND) + cli->buildmode->found_optargs = cli->found_optargs; + pipeline->current_stage = NULL; + } + + // Now teardown any filters + while (cli->filters) { + struct cli_filter *filt = cli->filters; + if (filt->filter) filt->filter(cli, NULL, cli->filters->data); + cli->filters = filt->next; + free_z(filt); + } + cli->found_optargs = NULL; + cli->pipeline = NULL; + return rc; +} + +static char DELIM_OPT_START[] = "["; +static char DELIM_OPT_END[] = "]"; +static char DELIM_ARG_START[] = "<"; +static char DELIM_ARG_END[] = ">"; +static char DELIM_NONE[] = ""; +static char BUILDMODE_YES[] = " (enter buildmode)"; +static char BUILDMODE_NO[] = ""; + +static void cli_get_optarg_comphelp(struct cli_def *cli, struct cli_optarg *optarg, struct cli_comphelp *comphelp, + int num_candidates, const char lastchar, const char *anchor_word, + const char *next_word) { + int help_insert = 0; + char *delim_start = DELIM_NONE; + + char *delim_end = DELIM_NONE; + char *allow_buildmode = BUILDMODE_NO; + int (*get_completions)(struct cli_def *, const char *, const char *, struct cli_comphelp *) = NULL; + char *tptr = NULL; + char *tname = NULL; + + // If we've already seen a value by this exact name, skip it, unless the multiple flag is set + if (cli_find_optarg_value(cli, optarg->name, NULL) && !(optarg->flags & (CLI_CMD_OPTION_MULTIPLE))) return; + + get_completions = optarg->get_completions; + if (optarg->flags & CLI_CMD_OPTIONAL_FLAG) { + if (!(anchor_word && !strncmp(anchor_word, optarg->name, strlen(anchor_word)))) { + delim_start = DELIM_OPT_START; + delim_end = DELIM_OPT_END; + get_completions = NULL; // No point, completor of field is the name itself + } + } else if (optarg->flags & CLI_CMD_HYPHENATED_OPTION) { + delim_start = DELIM_OPT_START; + delim_end = DELIM_OPT_END; + } else if (optarg->flags & CLI_CMD_ARGUMENT) { + delim_start = DELIM_ARG_START; + delim_end = DELIM_ARG_END; + } else if (optarg->flags & CLI_CMD_OPTIONAL_ARGUMENT) { + /* + * Optional args can match against the name the value. + * Here 'anchor_word' is the name, and 'next_word' is what we're matching against. + * So if anchor_word==next_word we're looking at the 'name' of the optarg, otherwise we know the name and are going + * against the value. + */ + if (anchor_word != next_word) { + // Matching against optional argument 'value' + help_insert = 0; + if (!get_completions) { + delim_start = DELIM_ARG_START; + delim_end = DELIM_ARG_END; + } + } else { + // Matching against optional argument 'name' + help_insert = 1; + get_completions = NULL; // Matching against the name, not the following field value + if (!(anchor_word && !strncmp(anchor_word, optarg->name, strlen(anchor_word)))) { + delim_start = DELIM_OPT_START; + delim_end = DELIM_OPT_END; + } + } + } + + // Fill in with help text or completor value(s) as indicated + if (lastchar == '?' && asprintf(&tname, "%s%s%s", delim_start, optarg->name, delim_end) != -1) { + if (optarg->flags & CLI_CMD_ALLOW_BUILDMODE) allow_buildmode = BUILDMODE_YES; + if (help_insert && (asprintf(&tptr, " %-20s enter '%s' to %s%s", tname, optarg->name, + (optarg->help) ? optarg->help : "", allow_buildmode) != -1)) { + cli_add_comphelp_entry(comphelp, tptr); + free_z(tptr); + } else if (asprintf(&tptr, " %-20s %s%s", tname, (optarg->help) ? optarg->help : "", allow_buildmode) != -1) { + cli_add_comphelp_entry(comphelp, tptr); + free_z(tptr); + } + free_z(tname); + } else if (lastchar == CTRL('I')) { + if (get_completions) { + (*get_completions)(cli, optarg->name, next_word, comphelp); + } else if ((!anchor_word || !strncmp(anchor_word, optarg->name, strlen(anchor_word))) && + (asprintf(&tptr, "%s%s%s", delim_start, optarg->name, delim_end) != -1)) { + cli_add_comphelp_entry(comphelp, tptr); + free_z(tptr); + } + } +} + +static void cli_int_parse_optargs(struct cli_def *cli, struct cli_pipeline_stage *stage, struct cli_command *cmd, + char lastchar, struct cli_comphelp *comphelp) { + struct cli_optarg *optarg = NULL, *oaptr = NULL; + int word_idx, word_incr, candidate_idx; + struct cli_optarg *candidates[CLI_MAX_LINE_WORDS]; + char *value; + int num_candidates = 0; + int is_last_word = 0; + int (*validator)(struct cli_def *, const char *name, const char *value); + + if (cli->buildmode) cli->found_optargs = cli->buildmode->found_optargs; + else cli->found_optargs = stage->found_optargs; + /* + * Tab completion and help are *only* allowed at end of string, but we need to process the entire command to know what + * has already been found. There should be no ambiguities before the 'last' word. + * Note specifically that for tab completions and help the *last* word can be a null pointer. + */ + stage->error_word = NULL; + + /* Start our optarg and word pointers at the beginning. + * optarg will be incremented *only* when an argument is identified. + * word_idx will be incremented either by 1 (optflag or argument) or 2 (optional argument). + */ + word_idx = stage->first_unmatched; + optarg = cmd->optargs; + num_candidates = 0; + while (optarg && word_idx < stage->num_words && num_candidates <= 1) { + num_candidates = 0; + word_incr = 1; // Assume we're only incrementing by a word - if we match an optional argument bump to 2 + + /* + * The initial loop here is to identify candidates based matching *this* word in order against: + * - An exact match of the word to the optinal flag/argument name (yield exactly one match and exit the loop) + * - A partial match for optional flag/argument name + * - Candidate an argument. + */ + + for (oaptr = optarg; oaptr; oaptr = oaptr->next) { + // Skip this option unless it matches privileges, MODE_ANY, the current mode, or the transient_mode + if (cli->privilege < oaptr->privilege) continue; + if ((oaptr->mode != cli->mode) && (oaptr->mode != cli->transient_mode) && (oaptr->mode != MODE_ANY)) continue; + + /* + * Two special cases - a hphenated option and an 'exact' match optional flag or optional argument. + * If our word starts with a '-' and we have a CMD_CLI_HYPHENATED_OPTION or an exact match for an optional + * flag/argument name trumps anything and will be the *only* candidate. + * Otherwise if the word is 'blank', could be an argument, or matches 'enough' of an option/flag it is a + * candidate. + * Once we accept an argument as a candidate, we're done looking for candidates as straight arguments are + * required. + */ + if (oaptr->flags & CLI_CMD_SPOT_CHECK && num_candidates == 0) { + stage->status = (*oaptr->validator)(cli, NULL, NULL); + if (stage->status != CLI_OK) { + stage->error_word = stage->words[word_idx]; + cli_reprompt(cli); + goto done; + } + } else if (stage->words[word_idx] && (oaptr->flags & (CLI_CMD_OPTIONAL_FLAG | CLI_CMD_OPTIONAL_ARGUMENT)) && + !strcmp(oaptr->name, stage->words[word_idx])) { + candidates[0] = oaptr; + num_candidates = 1; + break; + } else if (stage->words[word_idx] && stage->words[word_idx][0] == '-' && + (oaptr->flags & (CLI_CMD_HYPHENATED_OPTION))) { + candidates[0] = oaptr; + num_candidates = 1; + break; + } else if (!stage->words[word_idx] || (oaptr->flags & CLI_CMD_ARGUMENT) || + !strncasecmp(oaptr->name, stage->words[word_idx], strlen(stage->words[word_idx]))) { + candidates[num_candidates++] = oaptr; + } + if (oaptr->flags & CLI_CMD_ARGUMENT) { + break; + } + } + + /* + * Iterate over the list of candidates for this word. There are several early exit cases to consider: + * - If we have no candidates then we're done - any remaining words must be processed by the command callback + * - If we have more than one candidate evaluating for execution punt hard after complaining. + * - If we have more than one candidate and we're not at end-of-line ( + */ + if (num_candidates == 0) break; + if (num_candidates > 1 && (lastchar == '\0' || word_idx < (stage->num_words - 1))) { + stage->error_word = stage->words[word_idx]; + stage->status = CLI_AMBIGUOUS; + cli_error(cli, "Ambiguous option/argument for command %s", stage->command->command); + goto done; + } + + /* + * So now we could have one or more candidates. We need to call get help/completions *only* if this is the + * 'last-word'. + * Remember that last word for optional arguments is last or next to last.... + */ + if (lastchar != '\0') { + int called_comphelp = 0; + for (candidate_idx = 0; candidate_idx < num_candidates; candidate_idx++) { + oaptr = candidates[candidate_idx]; + + // Need to know *which* word we're trying to complete for optional_args, hence the difference calls + if (((oaptr->flags & (CLI_CMD_OPTIONAL_FLAG | CLI_CMD_ARGUMENT)) && (word_idx == (stage->num_words - 1))) || + (oaptr->flags & (CLI_CMD_OPTIONAL_ARGUMENT | CLI_CMD_HYPHENATED_OPTION) && + word_idx == (stage->num_words - 1))) { + cli_get_optarg_comphelp(cli, oaptr, comphelp, num_candidates, lastchar, stage->words[word_idx], + stage->words[word_idx]); + called_comphelp = 1; + } else if (oaptr->flags & CLI_CMD_OPTIONAL_ARGUMENT && word_idx == (stage->num_words - 2)) { + cli_get_optarg_comphelp(cli, oaptr, comphelp, num_candidates, lastchar, stage->words[word_idx], + stage->words[word_idx + 1]); + called_comphelp = 1; + } + } + // If we were 'end-of-word' and looked for completions/help, return to user + if (called_comphelp) { + stage->status = CLI_OK; + goto done; + } + } + + // Set some values for use later - makes code much easier to read + value = stage->words[word_idx]; + oaptr = candidates[0]; + validator = oaptr->validator; + if ((oaptr->flags & (CLI_CMD_OPTIONAL_FLAG | CLI_CMD_ARGUMENT) && word_idx == (stage->num_words - 1)) || + (oaptr->flags & CLI_CMD_OPTIONAL_ARGUMENT && word_idx == (stage->num_words - 2))) { + is_last_word = 1; + } + + if (oaptr->flags & CLI_CMD_OPTIONAL_ARGUMENT) { + word_incr = 2; + if (!stage->words[word_idx + 1] && lastchar == '\0') { + // Hit an optional argument that does not have a value with it + cli_error(cli, "Optional argument %s requires a value", stage->words[word_idx]); + stage->error_word = stage->words[word_idx]; + stage->status = CLI_MISSING_VALUE; + goto done; + } + value = stage->words[word_idx + 1]; + } + + /* + * We're not at end of string and doing help/completions. + * So see if our value is 'valid', to save it, and see if we have any extra processing to do such as a transient + * mode check or enter build mode. + */ + + if (!validator || (*validator)(cli, oaptr->name, value) == CLI_OK) { + if (oaptr->flags & CLI_CMD_DO_NOT_RECORD) { + // We want completion and validation, but then leave this 'value' to be seen - used *only* by buildmode as + // argv[0] with argc=1 + break; + } else { + // Need to combine remaining words if the CLI_CMD_REMAINDER_OF_LINE flag it set, then we're done processing + int set_value_return = 0; + + if (oaptr->flags & CLI_CMD_REMAINDER_OF_LINE) { + char *combined = NULL; + combined = join_words(stage->num_words - word_idx, stage->words + word_idx); + set_value_return = cli_set_optarg_value(cli, oaptr->name, combined, 0); + free_z(combined); + } else { + set_value_return = cli_set_optarg_value(cli, oaptr->name, value, oaptr->flags & CLI_CMD_OPTION_MULTIPLE); + } + + if (set_value_return != CLI_OK) { + cli_error(cli, "%sProblem setting value for command argument %s", lastchar == '\0' ? "" : "\n", + stage->words[word_idx]); + cli_reprompt(cli); + stage->error_word = stage->words[word_idx]; + stage->status = CLI_ERROR; + goto done; + } + } + } else { + cli_error(cli, "%sProblem parsing command setting %s with value %s", lastchar == '\0' ? "" : "\n", oaptr->name, + stage->words[word_idx]); + cli_reprompt(cli); + stage->error_word = stage->words[word_idx]; + stage->status = CLI_ERROR; + goto done; + } + + // If this optarg can set the transient mode, then evaluate it if we're not at last word + if (oaptr->transient_mode && oaptr->transient_mode(cli, oaptr->name, value)) { + stage->error_word = stage->words[word_idx]; + stage->status = CLI_ERROR; + goto done; + } + + // Only do buildmode optargs if we're a executing a command, parsing command (stage 0), and this is the last word + if ((stage->status == CLI_OK) && (oaptr->flags & CLI_CMD_ALLOW_BUILDMODE) && is_last_word) { + stage->status = cli_int_enter_buildmode(cli, stage, value); + goto done; + } + + // Optional flags and arguments can appear multiple times, but true arguments only once. Advance our optarg + // starting point when we see a true argument + if (oaptr->flags & CLI_CMD_ARGUMENT) { + // Advance past this argument entry + optarg = oaptr->next; + } + + word_idx += word_incr; + stage->first_unmatched = word_idx; + } + + // If we're evaluating the command for execution, ensure we have all required arguments. + if (lastchar == '\0') { + for (; optarg; optarg = optarg->next) { + if (cli->privilege < optarg->privilege) continue; + if ((optarg->mode != cli->mode) && (optarg->mode != cli->transient_mode) && (optarg->mode != MODE_ANY)) continue; + if (optarg->flags & CLI_CMD_DO_NOT_RECORD) continue; + if (optarg->flags & CLI_CMD_ARGUMENT) { + cli_error(cli, "Incomplete command, missing required argument '%s'", optarg->name); + stage->status = CLI_MISSING_ARGUMENT; + goto done; + } + } + } + +done: + if (cli->buildmode) cli->buildmode->found_optargs = cli->found_optargs; + else stage->found_optargs = cli->found_optargs; + return; +} + +void cli_unregister_all_commands(struct cli_def *cli) { + cli_unregister_tree(cli, cli->commands, CLI_REGULAR_COMMAND); +} + +void cli_unregister_all_filters(struct cli_def *cli) { + cli_unregister_tree(cli, cli->commands, CLI_FILTER_COMMAND); +} diff --git a/libcli.h b/libcli.h index 6ee68e5..3361744 100644 --- a/libcli.h +++ b/libcli.h @@ -12,14 +12,23 @@ extern "C" { #include #define LIBCLI_VERSION_MAJOR 1 -#define LIBCLI_VERISON_MINOR 9 -#define LIBCLI_VERISON_REVISION 8 +#define LIBCLI_VERISON_MINOR 10 +#define LIBCLI_VERISON_REVISION 0 #define LIBCLI_VERSION ((LIBCLI_VERSION_MAJOR << 16) | (LIBCLI_VERSION_MINOR << 8) | LIBCLI_VERSION_REVISION) #define CLI_OK 0 #define CLI_ERROR -1 #define CLI_QUIT -2 #define CLI_ERROR_ARG -3 +#define CLI_AMBIGUOUS -4 +#define CLI_UNRECOGNIZED -5 +#define CLI_MISSING_ARGUMENT -6 +#define CLI_MISSING_VALUE -7 +#define CLI_BUILDMODE_START -8 +#define CLI_BUILDMODE_ERROR -9 +#define CLI_BUILDMODE_EXTEND -10 +#define CLI_BUILDMODE_CANCEL -11 +#define CLI_BUILDMODE_EXIT -12 #define MAX_HISTORY 256 @@ -70,6 +79,10 @@ struct cli_def { time_t last_action; int telnet_protocol; void *user_context; + struct cli_optarg_pair *found_optargs; + int transient_mode; + struct cli_pipeline *pipeline; + struct cli_buildmode *buildmode; }; struct cli_filter { @@ -78,6 +91,13 @@ struct cli_filter { struct cli_filter *next; }; +enum command_types { + CLI_ANY_COMMAND, + CLI_REGULAR_COMMAND, + CLI_FILTER_COMMAND, + CLI_BUILDMODE_COMMAND, +}; + struct cli_command { char *command; int (*callback)(struct cli_def *, const char *, char **, int); @@ -85,9 +105,83 @@ struct cli_command { char *help; int privilege; int mode; + struct cli_command *previous; struct cli_command *next; struct cli_command *children; struct cli_command *parent; + struct cli_optarg *optargs; + int (*filter)(struct cli_def *cli, const char *string, void *data); + int (*init)(struct cli_def *cli, int, char **, struct cli_filter *filt); + int command_type; +}; + +struct cli_comphelp { + int comma_separated; + char **entries; + int num_entries; +}; + +enum optarg_flags { + CLI_CMD_OPTIONAL_FLAG = 1 << 0, + CLI_CMD_OPTIONAL_ARGUMENT = 1 << 1, + CLI_CMD_ARGUMENT = 1 << 2, + CLI_CMD_ALLOW_BUILDMODE = 1 << 3, + CLI_CMD_OPTION_MULTIPLE = 1 << 4, + CLI_CMD_OPTION_SEEN = 1 << 5, + CLI_CMD_TRANSIENT_MODE = 1 << 6, + CLI_CMD_DO_NOT_RECORD = 1 << 7, + CLI_CMD_REMAINDER_OF_LINE = 1 << 8, + CLI_CMD_HYPHENATED_OPTION = 1 << 9, + CLI_CMD_SPOT_CHECK = 1 << 10, +}; + +struct cli_optarg { + char *name; + int flags; + char *help; + int mode; + int privilege; + unsigned int unique_len; + int (*get_completions)(struct cli_def *, const char *, const char *, struct cli_comphelp *); + int (*validator)(struct cli_def *, const char *, const char *); + int (*transient_mode)(struct cli_def *, const char *, const char *); + struct cli_optarg *next; +}; + +struct cli_optarg_pair { + char *name; + char *value; + struct cli_optarg_pair *next; +}; + +struct cli_pipeline_stage { + struct cli_command *command; + struct cli_optarg_pair *found_optargs; + char **words; + int num_words; + int status; + int first_unmatched; + int first_optarg; + int stage_num; + char *error_word; +}; + +struct cli_pipeline { + char *cmdline; + char *words[CLI_MAX_LINE_WORDS]; + int num_words; + int num_stages; + struct cli_pipeline_stage stage[CLI_MAX_LINE_WORDS]; + struct cli_pipeline_stage *current_stage; +}; + +struct cli_buildmode { + struct cli_command *command; + struct cli_optarg_pair *found_optargs; + char *cname; + int mode; + int transient_mode; + char *mode_text; }; struct cli_def *cli_init(); @@ -130,6 +224,29 @@ void cli_telnet_protocol(struct cli_def *cli, int telnet_protocol); void cli_set_context(struct cli_def *cli, void *context); void *cli_get_context(struct cli_def *cli); +void cli_free_comphelp(struct cli_comphelp *comphelp); +int cli_add_comphelp_entry(struct cli_comphelp *comphelp, const char *entry); +void cli_set_transient_mode(struct cli_def *cli, int transient_mode); +struct cli_command *cli_register_filter(struct cli_def *cli, const char *command, + int (*init)(struct cli_def *cli, int, char **, struct cli_filter *filt), + int (*filter)(struct cli_def *, const char *, void *), int privilege, int mode, + const char *help); +int cli_unregister_filter(struct cli_def *cli, const char *command); +int cli_register_optarg(struct cli_command *cmd, const char *name, int flags, int priviledge, int mode, + const char *help, + int (*get_completions)(struct cli_def *cli, const char *, const char *, struct cli_comphelp *), + int (*validator)(struct cli_def *cli, const char *, const char *), + int (*transient_mode)(struct cli_def *, const char *, const char *)); +char *cli_find_optarg_value(struct cli_def *cli, char *name, char *find_after); +struct cli_optarg_pair *cli_get_all_found_optargs(struct cli_def *cli); +int cli_unregister_optarg(struct cli_command *cmd, const char *name); +char *cli_get_optarg_value(struct cli_def *cli, const char *name, char *find_after); +int cli_set_optarg_value(struct cli_def *cli, const char *name, const char *value, int allow_multiple); +void cli_unregister_all_optarg(struct cli_command *c); +void cli_unregister_all_filters(struct cli_def *cli); +void cli_unregister_all_commands(struct cli_def *cli); +void cli_unregister_all(struct cli_def *cli, struct cli_command *command); + #ifdef __cplusplus } #endif diff --git a/libcli.spec b/libcli.spec index fd3daa5..88e6de1 100644 --- a/libcli.spec +++ b/libcli.spec @@ -1,7 +1,7 @@ -Version: 1.9.8 +Version: 1.10.0 Summary: Cisco-like telnet command-line library Name: libcli -Release: 4 +Release: 2 License: LGPL Group: Library/Communication Source: %{name}-%{version}.tar.gz @@ -60,13 +60,31 @@ rm -rf $RPM_BUILD_ROOT %defattr(-, root, root) %files devel -%doc README +%doc README.md doc/developers-guide.md %{_libdir}/*.so* %{_libdir}/*.a %{_includedir}/*.h %defattr(-, root, root) %changelog +* Tue Jul 23 2019 Rob Sanders 1.10.0-2 +- Fix spec file and rpm build issues +- Fix 2 memory leaks (tab completion and help formatting) +- Expose cli_set_optarg_value() for external use + +* Tue Jul 16 2019 Rob Sanders 1.10.0-1 +- Add support for named arguments, optional flags, and optional arguments +- Support help and tab complete for options/arguments +- Enable users to add custom 'filters', including support for options/arguments +- Replaced current interal filters with equivalent 'user filters' +- Added examples of options/arguments to clitest +- Added support for 'building' longer commands one option/argument at a time (buildmode) +- Additional minor Coverity/valgrind related fixes +- Tab completion will show up until an ambiguity, or entire entry if only one found +- Tab completion for options/arguments will show '[]' around optional items +- Tab completion for options/arguments will show '<>' around required items +- Restructured clitest.c so 'cli_init()' is done in child thread + * Wed Sep 19 2018 Rob Sanders 1.9.8-4 - Update spac file to use relative links for libcli.so symlinks