/* -*- mode: c; c-file-style: "openbsd" -*- */
/*
* Copyright (c) 2012 Vincent Bernat <bernat@luffy.cx>
*
* Permission to use, copy, modify, and/or distribute this software for any
* purpose with or without fee is hereby granted, provided that the above
* copyright notice and this permission notice appear in all copies.
*
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
* WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
* ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
* WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
* ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
* OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
#include "client.h"
#include <string.h>
#include <sys/queue.h>
/**
* An element of the environment (a key and a value).
*/
struct cmd_env_el {
TAILQ_ENTRY(cmd_env_el) next; /**< Next environment element */
const char *key; /**< Key for this element */
const char *value; /**< Value for this element */
};
/**
* A stack element.
*/
struct cmd_env_stack {
TAILQ_ENTRY(cmd_env_stack) next; /**< Next element, down the stack */
struct cmd_node *el; /**< Stored element */
};
/**
* Structure representing an environment for the current command.
*
* An environment is a list of values stored for use for the function executing
* as well as the current command, the current position in the command and a
* stack for cmd_node
*/
struct cmd_env {
TAILQ_HEAD(, cmd_env_el) elements; /**< List of environment variables */
TAILQ_HEAD(, cmd_env_stack) stack; /**< Stack */
int argc; /**< Number of argument in the command */
int argp; /**< Current argument */
const char **argv; /**< Arguments */
};
/**
* Structure representing a command node.
*
* Such a node contains a token accepted to enter the node (or @c NULL if there
* is no token needed), a documentation string to present the user, a function
* to validate the user input (or @c NULL if no function is needed) and a
* function to execute when entering the node. Because we can enter a node just
* by completing, the execution part should have no other effect than modifying
* the environment, with the exception of execution on @c NEWLINE (which cannot
* happen when completing).
*/
struct cmd_node {
TAILQ_ENTRY(cmd_node) next; /**< Next sibling */
const char *token; /**< Token to enter this cnode */
const char *doc; /**< Documentation string */
int privileged; /**< Privileged command? */
int hidden; /**< Hidden command? */
/**
* Function validating entry in this node. Can be @c NULL.
*/
int(*validate)(struct cmd_env*, void *);
/**
* Function to execute when entering this node. May be @c NULL.
*
* This function can alter the environment
*/
int(*execute)(struct lldpctl_conn_t*, struct writer*,
struct cmd_env*, void *);
void *arg; /**< Magic argument for the previous two functions */
/* List of possible subentries */
TAILQ_HEAD(, cmd_node) subentries; /* List of subnodes */
};
/**
* Create a root node.
*
* @return the root node.
*/
struct cmd_node*
commands_root(void)
{
return commands_new(NULL, NULL, NULL, NULL, NULL, NULL);
}
/**
* Make a node accessible only to privileged users.
*
* @param node node to change privileges
* @return the modified node
*
* The node is modified. It is returned to ease chaining.
*/
struct cmd_node*
commands_privileged(struct cmd_node *node)
{
if (node) node->privileged = 1;
return node;
}
/**
* Hide a node from help or completion.
*
* @param node node to hide
* @return the modified node
*
* The node is modified. It is returned to ease chaining.
*/
struct cmd_node*
commands_hidden(struct cmd_node *node)
{
if (node) node->hidden = 1;
return node;
}
/**
* Create a new node acessible by any user.
*
* @param root The node we want to attach this node.
* @param token Token to enter this node. Or @c NULL if no token is needed.
* @param doc Documentation for this node.
* @param validate Function that should return 1 if we can enter the node.
* @param execute Function that should return 1 on successful execution of this node.
* @param arg Magic argument for precedent functions.
* @return the newly created node
*/
struct cmd_node*
commands_new(struct cmd_node *root,
const char *token, const char *doc,
int(*validate)(struct cmd_env*, void *),
int(*execute)(struct lldpctl_conn_t*, struct writer*,
struct cmd_env*, void *),
void *arg)
{
struct cmd_node *new = calloc(1, sizeof(struct cmd_node));
if (new == NULL) {
log_warn("lldpctl", "unable to allocate memory for new command node");
return NULL;
}
new->token = token;
new->doc = doc;
new->validate = validate;
new->execute = execute;
new->arg = arg;
TAILQ_INIT(&new->subentries);
if (root != NULL)
TAILQ_INSERT_TAIL(&root->subentries, new, next);
return new;
}
/**
* Free a command tree.
*
* @param root The node we want to free.
*/
void
commands_free(struct cmd_node *root)
{
struct cmd_node *subcmd, *subcmd_next;
for (subcmd = TAILQ_FIRST(&root->subentries); subcmd != NULL;
subcmd = subcmd_next) {
subcmd_next = TAILQ_NEXT(subcmd, next);
TAILQ_REMOVE(&root->subentries, subcmd, next);
commands_free(subcmd);
}
free(root);
}
/**
* Return the current argument in the environment. This can be @c NEWLINE or
* @c NULL.
*
* @param env The environment.
* @return current argument.
*/
const char*
cmdenv_arg(struct cmd_env *env)
{
if (env->argp < env->argc)
return env->argv[env->argp];
if (env->argp == env->argc)
return NEWLINE;
return NULL;
}
/**
* Get a value from the environment.
*
* @param env The environment.
* @param key The key for the requested value.
* @return @c NULL if not found or the requested value otherwise. If no value is
* associated, return the key.
*/
const char*
cmdenv_get(struct cmd_env *env, const char *key)
{
struct cmd_env_el *el;
TAILQ_FOREACH(el, &env->elements, next)
if (!strcmp(el->key, key))
return el->value ? el->value : el->key;
return NULL;
}
/**
* Put a value in the environment.
*
* @param env The environment.
* @param key The key for the value.
* @param value The value.
* @return 0 on success, -1 otherwise.
*/
int
cmdenv_put(struct cmd_env *env,
const char *key, const char *value)
{
struct cmd_env_el *el = malloc(sizeof(struct cmd_env_el));
if (el == NULL) {
log_warn("lldpctl", "unable to allocate memory for new environment variable");
return -1;
}
el->key = key;
el->value = value;
TAILQ_INSERT_TAIL(&env->elements, el, next);
return 0;
}
/**
* Pop some node from the execution stack.
*
* This allows to resume parsing on a previous state. Useful to call after
* parsing optional arguments.
*
* @param env The environment.
* @param n How many element we want to pop.
* @return 0 on success, -1 otherwise.
*/
int
cmdenv_pop(struct cmd_env *env, int n)
{
while (n-- > 0) {
if (TAILQ_EMPTY(&env->stack)) {
log_warnx("lldpctl", "environment stack is empty");
return -1;
}
struct cmd_env_stack *first = TAILQ_FIRST(&env->stack);
TAILQ_REMOVE(&env->stack,
first, next);
free(first);
}
return 0;
}
/**
* Push some node on the execution stack.
*
* @param env The environment.
* @param node The node to push.
* @return 0 on success, -1 on error.
*/
static int
cmdenv_push(struct cmd_env *env, struct cmd_node *node)
{
struct cmd_env_stack *el = malloc(sizeof(struct cmd_env_stack));
if (el == NULL) {
log_warn("lldpctl", "not enough memory to allocate a stack element");
return -1;
}
el->el = node;
TAILQ_INSERT_HEAD(&env->stack, el, next);
return 0;
}
/**
* Return the top of the stack, without poping it.
*
* @param env The environment.
* @return the top element or @c NULL is the stack is empty.
*/
static struct cmd_node*
cmdenv_top(struct cmd_env *env)
{
if (TAILQ_EMPTY(&env->stack)) return NULL;
return TAILQ_FIRST(&env->stack)->el;
}
/**
* Free execution environment.
*
* @param env The environment.
*/
static void
cmdenv_free(struct cmd_env *env)
{
while (!TAILQ_EMPTY(&env->stack)) cmdenv_pop(env, 1);
struct cmd_env_el *first;
while (!TAILQ_EMPTY(&env->elements)) {
first = TAILQ_FIRST(&env->elements);
TAILQ_REMOVE(&env->elements, first, next);
free(first);
}
}
struct candidate_word {
TAILQ_ENTRY(candidate_word) next;
const char *word;
const char *doc;
int hidden;
};
/**
* Execute or complete a command from the given node.
*
* @param conn Connection to lldpd.
* @param w Writer for output.
* @param root Root node we want to start from.
* @param argc Number of arguments.
* @param argv Array of arguments.
* @param word Completed word. Or NULL when no completion is required.
* @param all When completing, display possible completions even if only one choice is possible.
* @param priv Is the current user privileged?
* @return 0 on success, -1 otherwise.
*/
static int
_commands_execute(struct lldpctl_conn_t *conn, struct writer *w,
struct cmd_node *root, int argc, const char **argv,
char **word, int all, int priv)
{
int n, rc = 0, completion = (word != NULL);
int help = 0; /* Are we asking for help? */
int complete = 0; /* Are we asking for possible completions? */
struct cmd_env env = {
.elements = TAILQ_HEAD_INITIALIZER(env.elements),
.stack = TAILQ_HEAD_INITIALIZER(env.stack),
.argc = argc,
.argv = argv,
.argp = 0
};
cmdenv_push(&env, root);
if (!completion)
for (n = 0; n < argc; n++)
log_debug("lldpctl", "argument %02d: `%s`", n, argv[n]);
if (completion) *word = NULL;
#define CAN_EXECUTE(candidate) \
((!candidate->privileged || priv || complete) && \
(!candidate->validate || \
candidate->validate(&env, candidate->arg) == 1))
/* When completion is in progress, we use the same algorithm than for
* execution until we reach the cursor position. */
struct cmd_node *current = NULL;
while ((current = cmdenv_top(&env))) {
if (!completion) {
help = !!cmdenv_get(&env, "help"); /* Are we asking for help? */
complete = !!cmdenv_get(&env, "complete"); /* Or completion? */
}
struct cmd_node *candidate, *best = NULL;
const char *token = (env.argp < env.argc) ? env.argv[env.argp] :
(env.argp == env.argc && !help && !complete) ? NEWLINE : NULL;
if (token == NULL ||
(completion && env.argp == env.argc - 1))
goto end;
if (!completion)
log_debug("lldpctl", "process argument %02d: `%s`",
env.argp, token);
TAILQ_FOREACH(candidate, ¤t->subentries, next) {
if (candidate->token &&
!strncmp(candidate->token, token, strlen(token)) &&
CAN_EXECUTE(candidate)) {
if (candidate->token &&
!strcmp(candidate->token, token)) {
/* Exact match */
best = candidate;
break;
}
if (!best) best = candidate;
else {
if (!completion)
log_warnx("lldpctl", "ambiguous token: %s (%s or %s)",
token, candidate->token, best->token);
rc = -1;
goto end;
}
}
}
if (!best) {
/* Take first that validate */
TAILQ_FOREACH(candidate, ¤t->subentries, next) {
if (!candidate->token &&
CAN_EXECUTE(candidate)) {
best = candidate;
break;
}
}
}
if (!best && env.argp == env.argc) goto end;
if (!best) {
if (!completion)
log_warnx("lldpctl", "unknown command from argument %d: `%s`",
env.argp + 1, token);
rc = -1;
goto end;
}
/* Push and execute */
cmdenv_push(&env, best);
if (best->execute && best->execute(conn, w, &env, best->arg) != 1) {
rc = -1;
goto end;
}
env.argp++;
}
end:
if (!completion && !help && !complete) {
if (rc == 0 && env.argp != env.argc + 1) {
log_warnx("lldpctl", "incomplete command");
rc = -1;
}
} else if (rc == 0 && (env.argp == env.argc - 1 || help || complete)) {
/* We need to complete. Let's build the list of candidate words. */
struct cmd_node *candidate = NULL;
size_t maxl = 10; /* Max length of a word */
TAILQ_HEAD(, candidate_word) words; /* List of subnodes */
TAILQ_INIT(&words);
current = cmdenv_top(&env);
if (!TAILQ_EMPTY(¤t->subentries)) {
TAILQ_FOREACH(candidate, ¤t->subentries, next) {
if ((!candidate->token || help || complete ||
!strncmp(env.argv[env.argc - 1], candidate->token,
strlen(env.argv[env.argc -1 ]))) &&
CAN_EXECUTE(candidate)) {
struct candidate_word *cword =
malloc(sizeof(struct candidate_word));
if (!cword) break;
cword->word = candidate->token;
cword->doc = candidate->doc;
cword->hidden = candidate->hidden;
if (cword->word && strlen(cword->word) > maxl)
maxl = strlen(cword->word);
TAILQ_INSERT_TAIL(&words, cword, next);
}
}
}
if (!TAILQ_EMPTY(&words)) {
/* Search if there is a common prefix, then return it. */
char prefix[maxl + 2]; /* Extra space may be added at the end */
struct candidate_word *cword, *cword_next;
memset(prefix, 0, maxl+2);
for (size_t n = 0; n < maxl; n++) {
int c = 1; /* Set to 0 to exit outer loop */
TAILQ_FOREACH(cword, &words, next) {
c = 0;
if (cword->hidden) continue;
if (cword->word == NULL) break;
if (!strcmp(cword->word, NEWLINE)) break;
if (strlen(cword->word) == n) break;
if (prefix[n] == '\0') prefix[n] = cword->word[n];
else if (prefix[n] != cword->word[n]) break;
c = 1;
}
if (c == 0) {
prefix[n] = '\0';
break;
}
}
/* If the prefix is complete, add a space, otherwise,
* just return it as is. */
if (!all && !help && !complete && strcmp(prefix, NEWLINE) &&
strlen(prefix) > 0 &&
strlen(env.argv[env.argc-1]) < strlen(prefix)) {
TAILQ_FOREACH(cword, &words, next) {
if (cword->word && !strcmp(prefix, cword->word)) {
prefix[strlen(prefix)] = ' ';
break;
}
}
*word = strdup(prefix);
} else {
/* No common prefix, print possible completions */
if (!complete)
fprintf(stderr, "\n-- \033[1;34m%s\033[0m\n",
current->doc ? current->doc : "Help");
TAILQ_FOREACH(cword, &words, next) {
if (cword->hidden) continue;
char fmt[100];
if (!complete) {
snprintf(fmt, sizeof(fmt),
"%s%%%ds%s %%s\n",
"\033[1;30m", (int)maxl, "\033[0m");
fprintf(stderr, fmt,
cword->word ? cword->word : "WORD",
cword->doc ? cword->doc : "...");
} else {
if (!cword->word || !strcmp(cword->word, NEWLINE))
continue;
fprintf(stdout, "%s %s\n",
cword->word ? cword->word : "WORD",
cword->doc ? cword->doc : "...");
}
}
}
for (cword = TAILQ_FIRST(&words); cword != NULL;
cword = cword_next) {
cword_next = TAILQ_NEXT(cword, next);
TAILQ_REMOVE(&words, cword, next);
free(cword);
}
}
}
cmdenv_free(&env);
return rc;
}
/**
* Complete the given command.
*/
char *
commands_complete(struct cmd_node *root, int argc, const char **argv,
int all, int privileged)
{
char *word = NULL;
if (_commands_execute(NULL, NULL, root, argc, argv,
&word, all, privileged) == 0)
return word;
return NULL;
}
/**
* Execute the given commands.
*/
int
commands_execute(struct lldpctl_conn_t *conn, struct writer *w,
struct cmd_node *root, int argc, const char **argv, int privileged)
{
return _commands_execute(conn, w, root, argc, argv, NULL, 0, privileged);
}
/**
* Check if the environment does not contain the given key.
*
* @param env The environment.
* @param key The key to search for.
* @return 1 if the environment does not contain the key. 0 otherwise.
*/
int
cmd_check_no_env(struct cmd_env *env, void *key)
{
return cmdenv_get(env, (const char*)key) == NULL;
}
/**
* Check if the environment does contain the given key.
*
* @param env The environment.
* @param key The key to search for. Can be a comma separated list.
* @return 1 if the environment does contain the key. 0 otherwise.
*/
int
cmd_check_env(struct cmd_env *env, void *key)
{
struct cmd_env_el *el;
const char *list = key;
int count = 0;
TAILQ_FOREACH(el, &env->elements, next) {
if (contains(list, el->key))
count++;
}
while ((list = strchr(list, ','))) { list++; count--; }
return (count == 1);
}
/**
* Store the given key in the environment.
*
* @param conn The connection.
* @param w The writer (not used).
* @param env The environment.
* @param key The key to store.
* @return 1 if the key was stored
*/
int
cmd_store_env(struct lldpctl_conn_t *conn, struct writer *w,
struct cmd_env *env, void *key)
{
return cmdenv_put(env, key, NULL) != -1;
}
/**
* Store the given key in the environment and pop one element from the stack.
*
* @param conn The connection.
* @param w The writer (not used).
* @param env The environment.
* @param key The key to store.
* @return 1 if the key was stored
*/
int
cmd_store_env_and_pop(struct lldpctl_conn_t *conn, struct writer *w,
struct cmd_env *env, void *key)
{
return (cmd_store_env(conn, w, env, key) != -1 &&
cmdenv_pop(env, 1) != -1);
}
/**
* Store the given key with a value being the current keyword in the environment
* and pop X elements from the stack.
*
* @param conn The connection.
* @param w The writer (not used).
* @param env The environment.
* @param key The key to store.
* @return 1 if the key was stored
*/
int
cmd_store_env_value_and_pop(struct lldpctl_conn_t *conn, struct writer *w,
struct cmd_env *env, void *key)
{
return (cmdenv_put(env, key, cmdenv_arg(env)) != -1 &&
cmdenv_pop(env, 1) != -1);
}
int
cmd_store_env_value_and_pop2(struct lldpctl_conn_t *conn, struct writer *w,
struct cmd_env *env, void *key)
{
return (cmdenv_put(env, key, cmdenv_arg(env)) != -1 &&
cmdenv_pop(env, 2) != -1);
}
int
cmd_store_env_value(struct lldpctl_conn_t *conn, struct writer *w,
struct cmd_env *env, void *key)
{
return (cmdenv_put(env, key, cmdenv_arg(env)) != -1);
}
int
cmd_store_env_value_and_pop3(struct lldpctl_conn_t *conn, struct writer *w,
struct cmd_env *env, void *key)
{
return (cmdenv_put(env, key, cmdenv_arg(env)) != -1 &&
cmdenv_pop(env, 3) != -1);
}
int
cmd_store_something_env_value_and_pop2(const char *what,
struct cmd_env *env, void *value)
{
return (cmdenv_put(env, what, value) != -1 &&
cmdenv_pop(env, 2) != -1);
}
int
cmd_store_something_env_value(const char *what,
struct cmd_env *env, void *value)
{
return (cmdenv_put(env, what, value) != -1);
}
/**
* Provide an iterator on all interfaces contained in "ports".
*
* @warning This function is not reentrant. It uses static variables to keep
* track of ports that have already been provided. Moreover, to release all
* resources, the iterator should be used until its end.
*
* @param conn The connection.
* @param env The environment.
* @return The next interface in the set of ports (or in all ports if no `ports`
* variable is present in the environment)
*/
lldpctl_atom_t*
cmd_iterate_on_interfaces(struct lldpctl_conn_t *conn, struct cmd_env *env)
{
static lldpctl_atom_iter_t *iter = NULL;
static lldpctl_atom_t *iface_list = NULL;
static lldpctl_atom_t *iface = NULL;
const char *interfaces = cmdenv_get(env, "ports");
do {
if (iter == NULL) {
iface_list = lldpctl_get_interfaces(conn);
if (!iface_list) {
log_warnx("lldpctl", "not able to get the list of interfaces. %s",
lldpctl_last_strerror(conn));
return NULL;
}
iter = lldpctl_atom_iter(iface_list);
if (!iter) return NULL;
} else {
iter = lldpctl_atom_iter_next(iface_list, iter);
if (iface) {
lldpctl_atom_dec_ref(iface);
iface = NULL;
}
if (!iter) {
lldpctl_atom_dec_ref(iface_list);
return NULL;
}
}
iface = lldpctl_atom_iter_value(iface_list, iter);
} while (interfaces && !contains(interfaces,
lldpctl_atom_get_str(iface, lldpctl_k_interface_name)));
return iface;
}
/**
* Provide an iterator on all ports contained in "ports", as well as the
* default port.
*
* @warning This function is not reentrant. It uses static variables to keep
* track of ports that have already been provided. Moreover, to release all
* resources, the iterator should be used until its end.
*
* @param conn The connection.
* @param env The environment.
* @param name Name of the interface (for logging purpose)
* @return The next interface in the set of ports (or in all ports if no `ports`
* variable is present in the environment), including the default port
* if no `ports` variable is present in the environment.
*/
lldpctl_atom_t*
cmd_iterate_on_ports(struct lldpctl_conn_t *conn, struct cmd_env *env, const char **name)
{
static int put_default = 0;
static lldpctl_atom_t *last_port = NULL;
const char *interfaces = cmdenv_get(env, "ports");
if (last_port) {
lldpctl_atom_dec_ref(last_port);
last_port = NULL;
}
if (!put_default) {
lldpctl_atom_t *iface = cmd_iterate_on_interfaces(conn, env);
if (iface) {
*name = lldpctl_atom_get_str(iface, lldpctl_k_interface_name);
last_port = lldpctl_get_port(iface);
return last_port;
}
if (!interfaces) {
put_default = 1;
*name = "(default)";
last_port = lldpctl_get_default_port(conn);
return last_port;
}
return NULL;
} else {
put_default = 0;
return NULL;
}
}
/**
* Restrict the command to some ports.
*/
void
cmd_restrict_ports(struct cmd_node *root)
{
/* Restrict to some ports. */
commands_new(
commands_new(root,
"ports",
"Restrict configuration to some ports",
cmd_check_no_env, NULL, "ports"),
NULL,
"Restrict configuration to the specified ports (comma-separated list)",
NULL, cmd_store_env_value_and_pop2, "ports");
}
/**
* Restrict the command to specific protocol
*/
void
cmd_restrict_protocol(struct cmd_node *root)
{
/* Restrict to some ports. */
commands_new(
commands_new(root,
"protocol",
"Restrict to specific protocol",
cmd_check_no_env, NULL, "protocol"),
NULL,
"Restrict display to the specified protocol",
NULL, cmd_store_env_value_and_pop2, "protocol");
}