From 2ff2029c42999902e66b8c6a0224884383000e6c Mon Sep 17 00:00:00 2001 From: "Roger A. Light" Date: Sat, 9 Dec 2023 10:36:05 +0000 Subject: [PATCH] mosquitto_ctrl interactive shell mode --- .github/workflows/coverage.yml | 7 + .github/workflows/coverity-scan-develop.yml | 1 + .github/workflows/coverity-scan-fixes.yml | 1 + .github/workflows/mosquitto-cmake.yml | 2 + .github/workflows/mosquitto-make-asan.yml | 2 + .github/workflows/mosquitto-make.yml | 2 + .gitignore | 8 + CMakeLists.txt | 1 + ChangeLog.txt | 1 + apps/mosquitto_ctrl/CMakeLists.txt | 42 +- apps/mosquitto_ctrl/Makefile | 21 +- apps/mosquitto_ctrl/ctrl_shell.c | 818 +++++ apps/mosquitto_ctrl/ctrl_shell.h | 110 + apps/mosquitto_ctrl/ctrl_shell_broker.c | 197 ++ apps/mosquitto_ctrl/ctrl_shell_client.c | 93 + .../ctrl_shell_completion_tree.c | 139 + apps/mosquitto_ctrl/ctrl_shell_dynsec.c | 1016 ++++++ apps/mosquitto_ctrl/ctrl_shell_internal.h | 105 + apps/mosquitto_ctrl/ctrl_shell_io.c | 31 + apps/mosquitto_ctrl/ctrl_shell_post_connect.c | 143 + apps/mosquitto_ctrl/ctrl_shell_pre_connect.c | 210 ++ apps/mosquitto_ctrl/ctrl_shell_printf.c | 39 + apps/mosquitto_ctrl/dynsec.c | 2 +- apps/mosquitto_ctrl/dynsec_client.c | 2 +- apps/mosquitto_ctrl/mosquitto_ctrl.c | 24 +- apps/mosquitto_ctrl/mosquitto_ctrl.h | 7 +- apps/mosquitto_ctrl/test/Makefile | 124 + cmake/Findedit.cmake | 52 + config.mk | 8 + fuzzing/broker/Makefile | 3 +- fuzzing/scripts/oss-fuzz-build.sh | 2 +- fuzzing/scripts/oss-fuzz-dependencies.sh | 2 +- lib/connect.c | 37 + man/CMakeLists.txt | 2 + man/Makefile | 4 + man/mosquitto_ctrl.1.xml | 17 + man/mosquitto_ctrl_shell.1.meta | 5 + man/mosquitto_ctrl_shell.1.xml | 437 +++ plugins/dynamic-security/test.conf | 1 + test/CMakeLists.txt | 3 +- test/apps/Makefile | 1 + test/apps/ctrl/CMakeLists.txt | 73 + test/apps/ctrl/Makefile | 154 +- test/apps/ctrl/ctrl-args.py | 96 +- test/apps/ctrl/ctrl_shell_broker_test.cpp | 363 +++ test/apps/ctrl/ctrl_shell_completion_test.cpp | 103 + test/apps/ctrl/ctrl_shell_dynsec_test.cpp | 2768 +++++++++++++++++ test/apps/ctrl/ctrl_shell_help_test.cpp | 533 ++++ test/apps/ctrl/ctrl_shell_options_test.cpp | 368 +++ .../apps/ctrl/ctrl_shell_pre_connect_test.cpp | 167 + test/apps/ctrl/ctrl_shell_test.cpp | 171 + test/mock/CMakeLists.txt | 13 +- test/mock/Makefile | 1 + test/mock/apps/CMakeLists.txt | 1 + test/mock/apps/mosquitto_ctrl/CMakeLists.txt | 16 + test/mock/apps/mosquitto_ctrl/Makefile | 34 + .../apps/mosquitto_ctrl/ctrl_shell_mock.cpp | 14 + .../apps/mosquitto_ctrl/ctrl_shell_mock.hpp | 17 + 58 files changed, 8536 insertions(+), 78 deletions(-) create mode 100644 apps/mosquitto_ctrl/ctrl_shell.c create mode 100644 apps/mosquitto_ctrl/ctrl_shell.h create mode 100644 apps/mosquitto_ctrl/ctrl_shell_broker.c create mode 100644 apps/mosquitto_ctrl/ctrl_shell_client.c create mode 100644 apps/mosquitto_ctrl/ctrl_shell_completion_tree.c create mode 100644 apps/mosquitto_ctrl/ctrl_shell_dynsec.c create mode 100644 apps/mosquitto_ctrl/ctrl_shell_internal.h create mode 100644 apps/mosquitto_ctrl/ctrl_shell_io.c create mode 100644 apps/mosquitto_ctrl/ctrl_shell_post_connect.c create mode 100644 apps/mosquitto_ctrl/ctrl_shell_pre_connect.c create mode 100644 apps/mosquitto_ctrl/ctrl_shell_printf.c create mode 100644 apps/mosquitto_ctrl/test/Makefile create mode 100644 cmake/Findedit.cmake create mode 100644 man/mosquitto_ctrl_shell.1.meta create mode 100644 man/mosquitto_ctrl_shell.1.xml create mode 100644 test/apps/ctrl/ctrl_shell_broker_test.cpp create mode 100644 test/apps/ctrl/ctrl_shell_completion_test.cpp create mode 100644 test/apps/ctrl/ctrl_shell_dynsec_test.cpp create mode 100644 test/apps/ctrl/ctrl_shell_help_test.cpp create mode 100644 test/apps/ctrl/ctrl_shell_options_test.cpp create mode 100644 test/apps/ctrl/ctrl_shell_pre_connect_test.cpp create mode 100644 test/apps/ctrl/ctrl_shell_test.cpp create mode 100644 test/mock/apps/CMakeLists.txt create mode 100644 test/mock/apps/mosquitto_ctrl/CMakeLists.txt create mode 100644 test/mock/apps/mosquitto_ctrl/Makefile create mode 100644 test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.cpp create mode 100644 test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.hpp diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7f719ea6..7cabbe28 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -26,6 +26,8 @@ jobs: libcjson-dev \ libcjson1 \ libcunit1-dev \ + libedit-dev \ + libgmock-dev \ libssl-dev \ libwrap0-dev \ microsocks \ @@ -43,6 +45,11 @@ jobs: CFLAGS="-O0 -Wall -ggdb -fprofile-arcs" \ -j $(nproc) \ binary + make \ + WITH_COVERAGE=yes \ + CFLAGS="-O0 -Wall -ggdb -fprofile-arcs" \ + -j $(nproc) \ + test-compile - run: | make -C test test diff --git a/.github/workflows/coverity-scan-develop.yml b/.github/workflows/coverity-scan-develop.yml index 348ae6d9..f1b1e6e9 100644 --- a/.github/workflows/coverity-scan-develop.yml +++ b/.github/workflows/coverity-scan-develop.yml @@ -23,6 +23,7 @@ jobs: libcjson-dev \ libcjson1 \ libcunit1-dev \ + libedit-dev \ libssl-dev \ libwrap0-dev \ microsocks \ diff --git a/.github/workflows/coverity-scan-fixes.yml b/.github/workflows/coverity-scan-fixes.yml index 013729ce..44fe2b2b 100644 --- a/.github/workflows/coverity-scan-fixes.yml +++ b/.github/workflows/coverity-scan-fixes.yml @@ -23,6 +23,7 @@ jobs: libcjson-dev \ libcjson1 \ libcunit1-dev \ + libedit-dev \ libssl-dev \ libwrap0-dev \ microsocks \ diff --git a/.github/workflows/mosquitto-cmake.yml b/.github/workflows/mosquitto-cmake.yml index fa963db9..3d4ded90 100644 --- a/.github/workflows/mosquitto-cmake.yml +++ b/.github/workflows/mosquitto-cmake.yml @@ -26,6 +26,8 @@ jobs: libcjson-dev \ libcjson1 \ libcunit1-dev \ + libedit-dev \ + libgmock-dev \ libssl-dev \ libwrap0-dev \ microsocks \ diff --git a/.github/workflows/mosquitto-make-asan.yml b/.github/workflows/mosquitto-make-asan.yml index 1ed6acfa..e68a496a 100644 --- a/.github/workflows/mosquitto-make-asan.yml +++ b/.github/workflows/mosquitto-make-asan.yml @@ -27,6 +27,8 @@ jobs: libcjson-dev \ libcjson1 \ libcunit1-dev \ + libedit-dev \ + libgmock-dev \ libssl-dev \ libwrap0-dev \ microsocks \ diff --git a/.github/workflows/mosquitto-make.yml b/.github/workflows/mosquitto-make.yml index 0cfba5c7..0153c461 100644 --- a/.github/workflows/mosquitto-make.yml +++ b/.github/workflows/mosquitto-make.yml @@ -26,6 +26,8 @@ jobs: libcjson-dev \ libcjson1 \ libcunit1-dev \ + libedit-dev \ + libgmock-dev \ libssl-dev \ libwrap0-dev \ microsocks \ diff --git a/.gitignore b/.gitignore index 6728d03a..dbe1996f 100644 --- a/.gitignore +++ b/.gitignore @@ -89,6 +89,7 @@ man/mosquitto.conf.5 man/libmosquitto.3 man/mosquitto_ctrl.1 man/mosquitto_ctrl_dynsec.1 +man/mosquitto_ctrl_shell.1 man/mosquitto_passwd.1 man/mosquitto_pub.1 man/mosquitto_rr.1 @@ -101,6 +102,13 @@ out/ src/mosquitto src/mosquitto_broker.a +test/apps/ctrl/ctrl_shell_broker_test +test/apps/ctrl/ctrl_shell_completion_test +test/apps/ctrl/ctrl_shell_dynsec_test +test/apps/ctrl/ctrl_shell_help_test +test/apps/ctrl/ctrl_shell_options_test +test/apps/ctrl/ctrl_shell_pre_connect_test +test/apps/ctrl/ctrl_shell_test test/broker/broker.pid test/test_client test/fake_user diff --git a/CMakeLists.txt b/CMakeLists.txt index 7ce42084..b4bc8875 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,6 +98,7 @@ endif() find_package(cJSON REQUIRED) find_package(argon2) +find_package(edit) if(ARGON2_FOUND) # Disable until separate password handling thread is implemented #add_definitions("-DWITH_ARGON2") diff --git a/ChangeLog.txt b/ChangeLog.txt index b8c1cd79..460b4ef9 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -201,6 +201,7 @@ DB Dump: Apps: - Add `mosquitto_signal` for helping send signals to mosquitto on Windows. +- Add interactive shell mode to mosquitto_ctrl. Build: - Increased CMake minimal required version to 3.14, which is required for the diff --git a/apps/mosquitto_ctrl/CMakeLists.txt b/apps/mosquitto_ctrl/CMakeLists.txt index 91a0f508..28cfd4ca 100644 --- a/apps/mosquitto_ctrl/CMakeLists.txt +++ b/apps/mosquitto_ctrl/CMakeLists.txt @@ -1,5 +1,5 @@ if(WITH_TLS) - add_executable(mosquitto_ctrl + set(SRC mosquitto_ctrl.c mosquitto_ctrl.h broker.c client.c @@ -12,12 +12,43 @@ if(WITH_TLS) ../../common/json_help.c ../../common/json_help.h ) + if(EDITLINE_FOUND) + add_library(ctrl_shell OBJECT + ctrl_shell.c + ctrl_shell.h + ctrl_shell_broker.c + ctrl_shell_client.c + ctrl_shell_completion_tree.c + ctrl_shell_dynsec.c + ctrl_shell_post_connect.c + ctrl_shell_pre_connect.c + ctrl_shell_printf.c + ) + target_compile_definitions(ctrl_shell PRIVATE WITH_EDITLINE) + target_include_directories(ctrl_shell PRIVATE + "${mosquitto_SOURCE_DIR}" + "${mosquitto_SOURCE_DIR}/common" + "${mosquitto_SOURCE_DIR}/include" + ) + add_library(ctrl_shell_io OBJECT + ctrl_shell_io.c + ) + target_compile_definitions(ctrl_shell_io PRIVATE WITH_EDITLINE) + target_include_directories(ctrl_shell_io PRIVATE + "${mosquitto_SOURCE_DIR}" + "${mosquitto_SOURCE_DIR}/common" + "${mosquitto_SOURCE_DIR}/include" + ) + endif() + add_executable(mosquitto_ctrl ${SRC}) + target_include_directories(mosquitto_ctrl PRIVATE "${mosquitto_SOURCE_DIR}" "${mosquitto_SOURCE_DIR}/apps/mosquitto_passwd" "${mosquitto_SOURCE_DIR}/common" "${mosquitto_SOURCE_DIR}/plugins/common" "${mosquitto_SOURCE_DIR}/plugins/dynamic-security" + "${ARGON2_INCLUDE_DIRS}" ) if(WITH_BUNDLED_DEPS) target_include_directories(mosquitto_ctrl PRIVATE @@ -25,6 +56,15 @@ if(WITH_TLS) ) endif() + if(EDITLINE_FOUND) + target_compile_definitions(mosquitto_ctrl PRIVATE WITH_EDITLINE) + target_link_libraries(mosquitto_ctrl PRIVATE + "${EDITLINE_LIBRARIES}" + ctrl_shell + ctrl_shell_io + ) + endif() + if(WITH_STATIC_LIBRARIES) target_link_libraries(mosquitto_ctrl PRIVATE libmosquitto_static) else() diff --git a/apps/mosquitto_ctrl/Makefile b/apps/mosquitto_ctrl/Makefile index 911edd11..ffda4f8d 100644 --- a/apps/mosquitto_ctrl/Makefile +++ b/apps/mosquitto_ctrl/Makefile @@ -21,6 +21,10 @@ LOCAL_LDFLAGS+=-pthread LOCAL_LDADD+=-lssl -lcrypto endif +ifeq ($(WITH_EDITLINE),yes) +LOCAL_LDADD+=-ledit +LOCAL_CPPFLAGS+=-DWITH_EDITLINE +endif OBJS= \ mosquitto_ctrl.o \ @@ -30,7 +34,20 @@ OBJS= \ dynsec_client.o \ dynsec_group.o \ dynsec_role.o \ - options.o \ + options.o + +ifeq ($(WITH_EDITLINE),yes) +OBJS+= \ + ctrl_shell.o \ + ctrl_shell_broker.o \ + ctrl_shell_client.o \ + ctrl_shell_completion_tree.o \ + ctrl_shell_dynsec.o \ + ctrl_shell_io.o \ + ctrl_shell_post_connect.o \ + ctrl_shell_pre_connect.o \ + ctrl_shell_printf.o +endif OBJS_EXTERNAL= \ get_password.o \ @@ -46,7 +63,7 @@ endif all : ${TARGET} -mosquitto_ctrl : ${OBJS} ${OBJS_EXTERNAL} ${LIBMOSQ} +mosquitto_ctrl : ${OBJS} ${OBJS_EXTERNAL} ${CROSS_COMPILE}${CC} $^ -o $@ $(LOCAL_LDFLAGS) $(LOCAL_LDADD) mosquitto_ctrl_example.so : ${EXAMPLE_OBJS} diff --git a/apps/mosquitto_ctrl/ctrl_shell.c b/apps/mosquitto_ctrl/ctrl_shell.c new file mode 100644 index 00000000..2417653c --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell.c @@ -0,0 +1,818 @@ +/* +Copyright (c) 2023 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ctrl_shell.h" +#include "ctrl_shell_internal.h" + +#define UNUSED(A) (void)(A) + +#ifdef WITH_EDITLINE + +#define FREE(A) do{free(A); A = NULL;}while(0) + +const char *ANSI_URL = NULL; +const char *ANSI_MODULE = NULL; +const char *ANSI_INPUT = NULL; +const char *ANSI_ERROR = NULL; +const char **ANSI_LABEL = NULL; +const char *ANSI_RESET = "\001\e[0m\002"; + +const char *ANSI_LABEL_none[] = {"", ""}; + +const char ANSI_URL_dark[] = "\001\e[38;5;155m\002"; +const char ANSI_MODULE_dark[] = "\001\e[38;5;214m\002"; +const char ANSI_INPUT_dark[] = "\001\e[38;5;80m\002"; +const char ANSI_ERROR_dark[] = "\001\e[38;5;198m\002"; +const char *ANSI_LABEL_dark[] = { + "\001\e[38;5;207m\002", + "\001\e[38;5;219m\002", +}; + +const char ANSI_URL_light[] = "\001\e[38;5;23m\002"; +const char ANSI_MODULE_light[] = "\001\e[38;5;130m\002"; +const char ANSI_INPUT_light[] = "\001\e[38;5;27m\002"; +const char ANSI_ERROR_light[] = "\001\e[38;5;196m\002"; +const char *ANSI_LABEL_light[] = { + "\001\e[38;5;165m\002", + "\001\e[38;5;171m\002", +}; + + +char prompt[200]; +struct ctrl_shell data; +static int generator_arg = -1; +struct completion_tree_cmd *current_cmd_match = NULL; + +static void signal_winch(int signal) +{ + UNUSED(signal); + rl_resize_terminal(); +} + +static void signal_term(int signal) +{ + UNUSED(signal); + data.run = 0; +} + + +static void term_set_flag(bool set, unsigned int flag) +{ + struct termios ts; + + tcgetattr(0, &ts); + if(set){ + ts.c_lflag |= (flag); + }else{ + ts.c_lflag &= (unsigned int)(~flag); + } + tcsetattr(0, TCSANOW, &ts); +} + +static void term_set_echo(bool echo) +{ + term_set_flag(echo, ECHO); +} + +static void term_set_canon(bool canon) +{ + term_set_flag(canon, ICANON); +} + +void ctrl_shell_rtrim(char *buf) +{ + size_t slen = strlen(buf); + while(slen > 0 && isspace(buf[slen-1])){ + buf[slen-1] = '\0'; + slen = strlen(buf); + } +} + + +bool ctrl_shell_get_password(char *buf, size_t len) +{ + ctrl_shell_printf("%spassword:%s", ANSI_INPUT, ANSI_RESET); + term_set_echo(false); + if(ctrl_shell_fgets(buf, (int)len, stdin) == NULL){ + term_set_echo(true); + return false; + } + term_set_echo(true); + ctrl_shell_printf("\n"); + + ctrl_shell_rtrim(buf); + return true; +} + +static int response_wait(bool unlock) +{ + struct timespec timeout; + int rc = 0; + + clock_gettime(CLOCK_REALTIME, &timeout); + timeout.tv_sec += 2; + if(pthread_cond_timedwait(&data.response_cond, &data.response_mutex, &timeout) == ETIMEDOUT){ + ctrl_shell_printf("Timed out with no response.\n"); + rc = 1; + } + if(unlock){ + pthread_mutex_unlock(&data.response_mutex); + } + + return rc; +} + + +int ctrl_shell_publish_blocking(cJSON *j_command) +{ + int rc = 0; + + cJSON *j_commands = cJSON_CreateObject(); + cJSON *j_array = cJSON_AddArrayToObject(j_commands, "commands"); + cJSON_AddItemToArray(j_array, j_command); + + char *payload = cJSON_PrintUnformatted(j_commands); + cJSON_Delete(j_commands); + + pthread_mutex_lock(&data.response_mutex); + + mosquitto_publish(data.mosq, NULL, data.request_topic, (int)strlen(payload), payload, 1, false); + FREE(payload); + + /* Check for publish callback */ + rc = response_wait(false); + if(rc){ + pthread_mutex_unlock(&data.response_mutex); + return rc; + } + + if(data.publish_rc >= 128){ + pthread_mutex_unlock(&data.response_mutex); + return 1; + } + + /* Check for message callback */ + return response_wait(true); +} + +void ctrl_shell__connect_blocking(const char *hostname, int port) +{ + pthread_mutex_lock(&data.response_mutex); + + int rc = mosquitto_connect(data.mosq, hostname, port, 60); + rc = mosquitto_loop_start(data.mosq); + + /* FIXME - do something with the error */ + + response_wait(true); +} + +void ctrl_shell_line_callback_set(void (*callback)(char *line)) +{ + data.line_callback = callback; +} + +int ctrl_shell_command_generic_arg0(const char *command) +{ + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", command); + + return ctrl_shell_publish_blocking(j_command); +} + +int ctrl_shell_command_generic_arg1(const char *command, const char *itemlabel, char **saveptr) +{ + const char *item; + + item = strtok_r(NULL, " ", saveptr); + if(!item){ + ctrl_shell_printf("%s %s\n", command, itemlabel); + return MOSQ_ERR_INVAL; + } + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", command); + cJSON_AddStringToObject(j_command, itemlabel, item); + + return ctrl_shell_publish_blocking(j_command); +} + +int ctrl_shell_command_generic_int_arg1(const char *command, const char *itemlabel, char **saveptr) +{ + const char *item; + + item = strtok_r(NULL, " ", saveptr); + if(!item){ + ctrl_shell_printf("%s %s\n", command, itemlabel); + return MOSQ_ERR_INVAL; + } + int intval = atoi(item); + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", command); + cJSON_AddNumberToObject(j_command, itemlabel, intval); + + return ctrl_shell_publish_blocking(j_command); +} + +int ctrl_shell_command_generic_arg2(const char *command, const char *itemlabel1, const char *itemlabel2, char **saveptr) +{ + const char *item1, *item2; + + item1 = strtok_r(NULL, " ", saveptr); + item2 = strtok_r(NULL, " ", saveptr); + if(!item1 || !item2){ + ctrl_shell_printf("%s %s %s\n", command, itemlabel1, itemlabel2); + return MOSQ_ERR_INVAL; + } + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", command); + cJSON_AddStringToObject(j_command, itemlabel1, item1); + cJSON_AddStringToObject(j_command, itemlabel2, item2); + + return ctrl_shell_publish_blocking(j_command); +} + +static int ctrl_shell__subscribe_blocking(const char *topic, void (*module_on_subscribe)(void)) +{ + int rc = 0; + + for(int i=0; i= 128){ + rc = 1; + }else{ + if(module_on_subscribe){ + module_on_subscribe(); + } + } + + return rc; +} + + +bool ctrl_shell_callback_final(char *line) +{ + if(!line || !strcasecmp(line, "exit")){ + data.run = 0; + }else if(!strcasecmp(line, "disconnect")){ + if(data.mosq){ + ctrl_shell__disconnect(); + }else{ + return false; + } + }else if(!strcasecmp(line, "return")){ + if(data.mod_cleanup) data.mod_cleanup(); + ctrl_shell__post_connect_init(); + }else{ + return false; + } + return true; +} + + +void ctrl_shell_print_help_final(const char *command, const char *modul) +{ + if(!strcasecmp(command, "disconnect")){ + ctrl_shell_print_help_command("disconnect"); + ctrl_shell_printf("\nDisconnect from the broker\n"); + }else if(!strcasecmp(command, "exit")){ + ctrl_shell_print_help_command("exit"); + ctrl_shell_printf("\nQuit the program\n"); + }else if(!strcasecmp(command, "help")){ + ctrl_shell_print_help_command("help "); + ctrl_shell_printf("\nFind help on a command using 'help '\n"); + ctrl_shell_printf("Press tab multiple times to find currently available commands.\n"); + }else if(modul && !strcasecmp(command, "return")){ + ctrl_shell_print_help_command("return"); + ctrl_shell_printf("\nLeave %s mode.\n", modul); + }else{ + ctrl_shell_printf("Unknown command '%s'\n", command); + } +} + + +static void calc_generator_arg(int start) +{ + char *text_heap; + char *text_arg, *saveptr = NULL; + int ga = 0; + + if(start == 0){ + generator_arg = -1; + return; + } + + text_heap = strdup(rl_line_buffer); + if(!text_heap) return; + text_heap[start] = '\0'; + text_arg = strtok_r(text_heap, " ", &saveptr); + while(text_arg){ + ga++; + text_arg = strtok_r(NULL, " ", &saveptr); + } + FREE(text_heap); + + generator_arg = ga-1; +} + + +char *completion_generator(const char *text, int state) +{ + static size_t len; + static struct completion_tree_cmd *cmd, *cmd_prev; + static struct completion_tree_arg *arg; + + if(!data.commands){ + return NULL; + } + if(!state){ + len = strlen(text); + if(generator_arg < 0){ + cmd = data.commands->commands; + }else if(current_cmd_match && generator_arg < current_cmd_match->arg_list_count){ + arg = current_cmd_match->arg_lists[generator_arg]->args; + }else{ + return NULL; + } + } + + if(generator_arg < 0){ + while(cmd){ + char *name = cmd->name; + cmd_prev = cmd; + cmd = cmd->next; + + if(strncasecmp(name, text, len) == 0){ + current_cmd_match = cmd_prev; + return strdup(name); + } + } + }else{ + while(arg){ + char *name = arg->name; + arg = arg->next; + + if(strncasecmp(name, text, len) == 0){ + return strdup(name); + } + } + } + + return NULL; +} + + +void ctrl_shell_completion_commands_set(struct completion_tree_root *new_commands) +{ + data.commands = new_commands; +} + + +char **completion_matcher(const char *text, int start, int end) +{ + char **matches; + + UNUSED(end); + + rl_attempted_completion_over = 1; + calc_generator_arg(start); + matches = rl_completion_matches(text, completion_generator); + return matches; +} + +int my_get_address(int sock, char *buf, size_t len, uint16_t *remote_port) +{ + struct sockaddr_storage addr; + socklen_t addrlen; + + memset(&addr, 0, sizeof(struct sockaddr_storage)); + addrlen = sizeof(addr); + if(!getpeername(sock, (struct sockaddr *)&addr, &addrlen)){ + if(addr.ss_family == AF_INET){ + if(remote_port){ + *remote_port = ntohs(((struct sockaddr_in *)&addr)->sin_port); + } + if(inet_ntop(AF_INET, &((struct sockaddr_in *)&addr)->sin_addr.s_addr, buf, (socklen_t)len)){ + return 0; + } + }else if(addr.ss_family == AF_INET6){ + if(remote_port){ + *remote_port = ntohs(((struct sockaddr_in6 *)&addr)->sin6_port); + } + if(inet_ntop(AF_INET6, &((struct sockaddr_in6 *)&addr)->sin6_addr.s6_addr, buf, (socklen_t)len)){ + return 0; + } + } + } + return 1; +} + +static void on_connect_reconnect(struct mosquitto *mosq, void *userdata, int rc) +{ + UNUSED(userdata); + UNUSED(rc); + + for(int i=0; ipayload); + cJSON *j_responses = cJSON_GetObjectItem(j_tree, "responses"); + cJSON *j_command_obj = cJSON_GetArrayItem(j_responses, 0); + cJSON *j_command = cJSON_GetObjectItem(j_command_obj, "command"); + cJSON *j_error = cJSON_GetObjectItem(j_command_obj, "error"); + cJSON *j_data = cJSON_GetObjectItem(j_command_obj, "data"); + if(j_error){ + ctrl_shell_printf("%s%s%s\n", ANSI_ERROR, j_error->valuestring, ANSI_RESET); + }else if(j_data && data.response_callback){ + data.response_callback(j_command->valuestring, j_data, msg->payload); + }else if(j_command){ + ctrl_shell_printf("OK\n"); + }else{ + ctrl_shell_printf("Invalid response from broker.\n"); + } + cJSON_Delete(j_tree); + + pthread_mutex_unlock(&data.response_mutex); + pthread_cond_signal(&data.response_cond); +} + +void ctrl_shell__on_publish(struct mosquitto *mosq, void *userdata, int mid, int reason_code, const mosquitto_property *props) +{ + UNUSED(mosq); + UNUSED(userdata); + UNUSED(mid); + UNUSED(props); + + if(reason_code >= 128){ + ctrl_shell_printf("Publish failed, check you have permission to access this module.\n"); + data.publish_rc = reason_code; + } + + pthread_mutex_unlock(&data.response_mutex); + pthread_cond_signal(&data.response_cond); +} + +void ctrl_shell__on_subscribe(struct mosquitto *mosq, void *userdata, int mid, int qos_count, const int *granted_qos) +{ + UNUSED(mosq); + UNUSED(userdata); + UNUSED(mid); + + if(qos_count == 1 && granted_qos[0] >= 128){ + ctrl_shell_printf("Subscribe failed, check you have permission to access this module.\n"); + data.subscribe_rc = granted_qos[0]; + } + + pthread_mutex_unlock(&data.response_mutex); + pthread_cond_signal(&data.response_cond); +} + +void ctrl_shell__load_module(void (*mod_init)(struct ctrl_shell__module *mod)) +{ + struct ctrl_shell__module mod; + memset(&mod, 0, sizeof(mod)); + mod_init(&mod); + + data.request_topic = mod.request_topic; + data.response_callback = mod.response_callback; + data.mod_cleanup = mod.cleanup; + + ctrl_shell_completion_commands_set(mod.completion_commands); + ctrl_shell_line_callback_set(mod.line_callback); + ctrl_shell__subscribe_blocking(mod.response_topic, mod.on_subscribe); +} + + +void set_no_colour(void) +{ + ANSI_URL = ""; + ANSI_MODULE = ""; + ANSI_INPUT = ""; + ANSI_ERROR = ""; + ANSI_LABEL = ANSI_LABEL_none; + ANSI_RESET = ""; +} + +static void set_bg_light(void) +{ + ANSI_URL = ANSI_URL_light; + ANSI_MODULE = ANSI_MODULE_light; + ANSI_INPUT = ANSI_INPUT_light; + ANSI_ERROR = ANSI_ERROR_light; + ANSI_LABEL = ANSI_LABEL_light; +} + +static void set_bg_dark(void) +{ + ANSI_URL = ANSI_URL_dark; + ANSI_MODULE = ANSI_MODULE_dark; + ANSI_INPUT = ANSI_INPUT_dark; + ANSI_ERROR = ANSI_ERROR_dark; + ANSI_LABEL = ANSI_LABEL_dark; +} + + +static void get_bg(void) +{ + int opt; + + set_bg_dark(); + + /* Set non-blocking */ + opt = fcntl(STDIN_FILENO, F_GETFL, 0); + fcntl(STDIN_FILENO, F_SETFL, opt | O_NONBLOCK); + + term_set_echo(false); + term_set_canon(false); + ctrl_shell_printf("\e]10;?\a\e]11;?\a"); + fflush(stdout); + + char buf[50] = {0}; + ssize_t rl; + usleep(100000); + do{ + rl = read(STDIN_FILENO, buf, sizeof(buf)); + }while(rl == 0); + fcntl(STDIN_FILENO, F_SETFL, opt); + term_set_echo(true); + term_set_canon(true); + + for(int i=0; i bg){ + set_bg_dark(); + }else{ + set_bg_light(); + } +} + + +void ctrl_shell__cleanup(void) +{ + FREE(data.hostname); + for(int i=0; ino_colour){ + set_no_colour(); + }else{ + get_bg(); + } + + if(config){ + if(config->host){ + data.hostname = strdup(config->host); + } + if(config->port != PORT_UNDEFINED){ + data.port = config->port; + } + if(config->username){ + data.username = strdup(config->username); + } + if(config->password){ + data.password = strdup(config->password); + } + if(config->id){ + data.clientid = strdup(config->id); + } + if(config->cafile){ + data.tls_cafile = strdup(config->cafile); + } + if(config->capath){ + data.tls_capath = strdup(config->capath); + } + if(config->certfile){ + data.tls_certfile = strdup(config->certfile); + } + if(config->keyfile){ + data.tls_keyfile = strdup(config->keyfile); + } + + } + + pthread_mutex_init(&data.response_mutex, NULL); + pthread_cond_init(&data.response_cond, NULL); + + rl_readline_name = "mosquitto_ctrl"; + rl_completion_entry_function = completion_generator; + rl_attempted_completion_function = completion_matcher; + rl_bind_key('\t', rl_complete); + + signal(SIGWINCH, signal_winch); + signal(SIGTERM, signal_term); + signal(SIGINT, signal_term); + + ctrl_shell_printf("mosquitto_ctrl shell v" VERSION "\n"); + + if(data.hostname){ + if(ctrl_shell__connect()){ + ctrl_shell__cleanup(); + return; + } + }else{ + ctrl_shell__pre_connect_init(); + } + + while(data.run){ + current_cmd_match = NULL; + char *line = readline(prompt); + if(data.line_callback){ + data.line_callback(line); + } + } + clear_history(); + + ctrl_shell_printf("\n"); + + ctrl_shell__cleanup(); +} + +static void print_label(unsigned int level, const char *label) +{ + char *str = calloc(1, level*2 + strlen(label) + 30); + + for(unsigned int i=0; i + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#ifndef CTRL_SHELL_H +#define CTRL_SHELL_H + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +extern const char *ANSI_URL; +extern const char *ANSI_MODULE; +extern const char *ANSI_INPUT; +extern const char *ANSI_ERROR; +extern const char **ANSI_LABEL; +extern const char *ANSI_RESET; + +/* This relatively complex structure is used to store the command tree. It + * allows for commands to be shared between different parts of the tree, and + * for commands to have multiple arguments. The completion_tree_arg_list + * members exist to have a fixed memory location that never changes, so that + * we can reallocate the list for where we have dynamically updated arguments, + * for e.g. dynsec clients, where we want many commands to use the same list. + */ + +struct completion_tree_arg { + struct completion_tree_arg *next; + char name[]; +}; + +struct completion_tree_arg_list { + struct completion_tree_arg *args; + bool is_shared; +}; + +struct completion_tree_cmd { + struct completion_tree_cmd *next; + struct completion_tree_arg_list **arg_lists; + int arg_list_count; + char name[]; +}; + +struct completion_tree_root { + struct completion_tree_cmd *commands; +}; + +extern char prompt[200]; + +/* Helper functions for sending commands to the broker. */ +int ctrl_shell_command_generic_arg0(const char *command); +int ctrl_shell_command_generic_arg1(const char *command, const char *itemlabel, char **saveptr); +int ctrl_shell_command_generic_arg2(const char *command, const char *itemlabel1, const char *itemlabel2, char **saveptr); +int ctrl_shell_command_generic_int_arg1(const char *command, const char *itemlabel, char **saveptr); + + +void ctrl_shell_completion_commands_set(struct completion_tree_root *new_commands); +void ctrl_shell_line_callback_set(void (*callback)(char *line)); + +/* Helper functions for building the command tree. */ +void completion_tree_arg_list_free(struct completion_tree_arg_list *arg_list); +void completion_tree_arg_list_args_free(struct completion_tree_arg_list *arg_list); +void completion_tree_cmd_free(struct completion_tree_cmd *cmd); +void completion_tree_free(struct completion_tree_root *tree); +struct completion_tree_cmd *completion_tree_cmd_add(struct completion_tree_root *root, const char *name); +struct completion_tree_arg_list *completion_tree_cmd_new_arg_list(void); +void completion_tree_cmd_append_arg_list(struct completion_tree_cmd *cmd, struct completion_tree_arg_list *new_list); +struct completion_tree_arg_list *completion_tree_cmd_add_arg_list(struct completion_tree_cmd *cmd); +void completion_tree_arg_list_add_arg(struct completion_tree_arg_list *arg_list, const char *name); + +void ctrl_shell_pre_connect_init(void); +void ctrl_shell_post_connect_init(void); + +int ctrl_shell_publish_blocking(cJSON *j_command); +bool ctrl_shell_callback_final(char *line); + +char *ctrl_shell_fgets(char *s, int size, FILE *stream); +bool ctrl_shell_get_password(char *buf, size_t len); +void ctrl_shell_rtrim(char *buf); + +void ctrl_shell_printf(const char *fmt, ...); +void ctrl_shell_vprintf(const char *fmt, va_list va); +void ctrl_shell_print_label(unsigned int level, const char *label); +void ctrl_shell_print_label_value(unsigned int level, const char *label, int align, const char *fmt, ...); +void ctrl_shell_print_value(unsigned int level, const char *fmt, ...); +void ctrl_shell_print_help_command(const char *cmd); +void ctrl_shell_print_help_desc(const char *desc); +void ctrl_shell_print_help_final(const char *command, const char *modul); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/apps/mosquitto_ctrl/ctrl_shell_broker.c b/apps/mosquitto_ctrl/ctrl_shell_broker.c new file mode 100644 index 00000000..1b00d7c4 --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell_broker.c @@ -0,0 +1,197 @@ +/* +Copyright (c) 2023 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include + +#include +#include + +#include "ctrl_shell_internal.h" +#include "json_help.h" + +#ifdef WITH_EDITLINE + +#define UNUSED(A) (void)(A) + +static struct completion_tree_root *commands_broker = NULL; + +static void command_tree_create(void) +{ + if(commands_broker) return; + + commands_broker = calloc(1, sizeof(struct completion_tree_root)); + + completion_tree_cmd_add(commands_broker, "listPlugins"); + completion_tree_cmd_add(commands_broker, "listListeners"); + completion_tree_cmd_add(commands_broker, "disconnect"); + completion_tree_cmd_add(commands_broker, "help"); + completion_tree_cmd_add(commands_broker, "return"); + completion_tree_cmd_add(commands_broker, "exit"); +} + + +static void print_help(char **saveptr) +{ + char *command = strtok_r(NULL, " ", saveptr); + if(command){ + if(!strcasecmp(command, "listPlugins")){ + ctrl_shell_print_help_command("listPlugins"); + ctrl_shell_printf("\nLists currently loaded plugins.\n"); + }else if(!strcasecmp(command, "listListeners")){ + ctrl_shell_print_help_command("listListeners"); + ctrl_shell_printf("\nLists current listeners.\n"); + }else{ + ctrl_shell_print_help_final(command, "broker"); + } + }else{ + ctrl_shell_printf("This is the mosquitto_ctrl interactive shell, for controlling aspects of a mosquitto broker.\n"); + ctrl_shell_printf("You are in broker mode, for controlling some core broker functionality.\n"); + ctrl_shell_printf("Use '%sreturn%s' to leave this mode.\n", ANSI_INPUT, ANSI_RESET); + + ctrl_shell_printf("Find help on a command using '%shelp %s'\n", ANSI_INPUT, ANSI_RESET); + ctrl_shell_printf("Press tab multiple times to find currently available commands.\n\n"); + } +} + +static void line_callback(char *line) +{ + if(!line){ + ctrl_shell_callback_final(NULL); + return; + } + ctrl_shell_rtrim(line); + if(strlen(line) > 0){ + add_history(line); + }else{ + free(line); + return; + } + + char *saveptr = NULL; + char *command = strtok_r(line, " ", &saveptr); + + if(!strcasecmp(command, "listPlugins")){ + ctrl_shell_command_generic_arg0("listPlugins"); + }else if(!strcasecmp(command, "listListeners")){ + ctrl_shell_command_generic_arg0("listListeners"); + }else if(!strcasecmp(command, "help")){ + print_help(&saveptr); + }else{ + if(!ctrl_shell_callback_final(line)){ + ctrl_shell_printf("Unknown command '%s'\n", command); + } + } + + free(line); +} + +static void print_plugins(cJSON *j_data) +{ + cJSON *j_plugins, *j_plugin; + + j_plugins = cJSON_GetObjectItem(j_data, "plugins"); + + cJSON_ArrayForEach(j_plugin, j_plugins){ + const char *name; + if(json_get_string(j_plugin, "name", &name, false) != MOSQ_ERR_SUCCESS){ + ctrl_shell_printf("Invalid response from broker.\n"); + return; + } + + ctrl_shell_print_label(0, "Plugin:"); + ctrl_shell_print_value(1, "%s\n", name); + + cJSON *j_endpoints, *j_endpoint; + j_endpoints = cJSON_GetObjectItem(j_plugin, "control-endpoints"); + if(j_endpoints){ + ctrl_shell_print_label(0, "Control endpoints:"); + cJSON_ArrayForEach(j_endpoint, j_endpoints){ + ctrl_shell_print_value(1, "%s\n", j_endpoint->valuestring); + } + } + ctrl_shell_print_value(0, "\n"); + } +} + +static void print_listeners(cJSON *j_data) +{ + cJSON *j_listeners, *j_listener; + + j_listeners = cJSON_GetObjectItem(j_data, "listeners"); + + cJSON_ArrayForEach(j_listener, j_listeners){ + int port; + const char *protocol; + bool tls; + + if(json_get_int(j_listener, "port", &port, false, -1) != MOSQ_ERR_SUCCESS + || json_get_string(j_listener, "protocol", &protocol, false) != MOSQ_ERR_SUCCESS + || json_get_bool(j_listener, "tls", &tls, false, false) != MOSQ_ERR_SUCCESS){ + + ctrl_shell_printf("Invalid response from broker.\n"); + return; + } + + ctrl_shell_print_label(0, "Listener:"); + ctrl_shell_print_label(1, "Port:"); + ctrl_shell_print_value(2, "%d\n", port); + ctrl_shell_print_label(1, "Protocol:"); + ctrl_shell_print_value(2, "%s\n", protocol); + ctrl_shell_print_label(1, "TLS:"); + ctrl_shell_print_value(2, "%s\n\n", tls?"true":"false"); + } + +} + +static void handle_response(const char *command, cJSON *j_data, const char *payload) +{ + if(!strcmp(command, "listPlugins")){ + print_plugins(j_data); + }else if(!strcmp(command, "listListeners")){ + print_listeners(j_data); + }else{ + ctrl_shell_printf("%s %s\n", command, payload); + } +} + + +static void on_subscribe(void) +{ +} + + +static void ctrl_shell__broker_cleanup(void) +{ + completion_tree_free(commands_broker); + commands_broker = NULL; +} + + +void ctrl_shell__broker_init(struct ctrl_shell__module *mod) +{ + command_tree_create(); + + mod->completion_commands = commands_broker; + mod->request_topic = "$CONTROL/broker/v1"; + mod->response_topic = "$CONTROL/broker/v1/response"; + mod->line_callback = line_callback; + mod->response_callback = handle_response; + mod->on_subscribe = on_subscribe; + mod->cleanup = ctrl_shell__broker_cleanup; +} +#endif diff --git a/apps/mosquitto_ctrl/ctrl_shell_client.c b/apps/mosquitto_ctrl/ctrl_shell_client.c new file mode 100644 index 00000000..020962ec --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell_client.c @@ -0,0 +1,93 @@ +/* +Copyright (c) 2023 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include + +#include +#include +#include + +#include "ctrl_shell_internal.h" + +#ifdef WITH_EDITLINE + +#define UNUSED(A) (void)(A) + +int ctrl_shell__connect(void) +{ + if(data.port == PORT_UNDEFINED){ + data.port = 1883; + } + if(data.mosq){ + mosquitto_destroy(data.mosq); + } + data.mosq = mosquitto_new(data.clientid, true, NULL); + if(!strcmp(data.url_scheme, "mqtts") || !strcmp(data.url_scheme, "wss")){ + mosquitto_int_option(data.mosq, MOSQ_OPT_TLS_USE_OS_CERTS, 1); + } + if(data.transport == MOSQ_T_WEBSOCKETS){ + mosquitto_int_option(data.mosq, MOSQ_OPT_TRANSPORT, data.transport); + } + if(data.username && data.password){ + mosquitto_username_pw_set(data.mosq, data.username, data.password); + } + if(data.tls_cafile || data.tls_capath || data.tls_certfile || data.tls_keyfile){ + int rc = mosquitto_tls_set(data.mosq, data.tls_cafile, data.tls_capath, data.tls_certfile, data.tls_keyfile, NULL); + if(rc){ + if(rc == MOSQ_ERR_INVAL){ + ctrl_shell_printf("%sError setting TLS options: File not found.%s\n", ANSI_ERROR, ANSI_RESET); + }else{ + ctrl_shell_printf("%sError setting TLS options: %s.%s\n", ANSI_ERROR, mosquitto_strerror(rc), ANSI_RESET); + } + } + } + mosquitto_int_option(data.mosq, MOSQ_OPT_PROTOCOL_VERSION, 5); + + mosquitto_connect_callback_set(data.mosq, ctrl_shell__on_connect); + mosquitto_subscribe_callback_set(data.mosq, ctrl_shell__on_subscribe); + mosquitto_publish_v5_callback_set(data.mosq, ctrl_shell__on_publish); + mosquitto_message_callback_set(data.mosq, ctrl_shell__on_message); + ctrl_shell__connect_blocking(data.hostname, data.port); + if(data.connect_rc){ + ctrl_shell_printf("%sUnable to connect: %s%s\n", ANSI_ERROR, mosquitto_reason_string(data.connect_rc), ANSI_RESET); + return 1; + } + ctrl_shell__post_connect_init(); + + return 0; +} + +void ctrl_shell__disconnect(void) +{ + if(!data.mosq) return; + + mosquitto_disconnect(data.mosq); + mosquitto_loop_stop(data.mosq, false); + mosquitto_destroy(data.mosq); + data.mosq = NULL; + + for(int i=0; i + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include + +#include +#include + +#include "ctrl_shell_internal.h" + +#ifdef WITH_EDITLINE + +#define UNUSED(A) (void)(A) + +void completion_tree_arg_list_args_free(struct completion_tree_arg_list *arg_list) +{ + struct completion_tree_arg *arg, *next; + + if(!arg_list) return; + + arg = arg_list->args; + while(arg){ + next = arg->next; + free(arg); + arg = next; + } + arg_list->args = NULL; +} + +void completion_tree_arg_list_free(struct completion_tree_arg_list *arg_list) +{ + if(!arg_list) return; + if(arg_list->is_shared) return; + + completion_tree_arg_list_args_free(arg_list); + free(arg_list); +} + +void completion_tree_cmd_free(struct completion_tree_cmd *cmd) +{ + if(!cmd) return; + + for(int i=0; iarg_list_count; i++){ + completion_tree_arg_list_free(cmd->arg_lists[i]); + } + free(cmd->arg_lists); + free(cmd); +} + +void completion_tree_free(struct completion_tree_root *tree) +{ + struct completion_tree_cmd *cmd, *next; + + if(!tree) return; + + cmd = tree->commands; + while(cmd){ + next = cmd->next; + completion_tree_cmd_free(cmd); + cmd = next; + } + free(tree); +} + +struct completion_tree_cmd *completion_tree_cmd_add(struct completion_tree_root *root, const char *name) +{ + struct completion_tree_cmd *new_node; + + new_node = calloc(1, sizeof(struct completion_tree_cmd) + strlen(name) + 1); + if(!new_node) return NULL; + + strcpy(new_node->name, name); + + new_node->next = root->commands; + root->commands = new_node; + + return new_node; +} + +struct completion_tree_arg_list *completion_tree_cmd_new_arg_list(void) +{ + return calloc(1, sizeof(struct completion_tree_arg_list)); +} + +void completion_tree_cmd_append_arg_list(struct completion_tree_cmd *cmd, struct completion_tree_arg_list *new_list) +{ + struct completion_tree_arg_list **arg_list; + + arg_list = realloc(cmd->arg_lists, (size_t)(cmd->arg_list_count+1)*sizeof(struct completion_tree_arg_list *)); + if(!arg_list) return; + + cmd->arg_lists = arg_list; + + cmd->arg_lists[cmd->arg_list_count] = new_list; + cmd->arg_list_count++; +} + +struct completion_tree_arg_list *completion_tree_cmd_add_arg_list(struct completion_tree_cmd *cmd) +{ + if(!cmd) return NULL; + + struct completion_tree_arg_list *new_list; + new_list = completion_tree_cmd_new_arg_list(); + if(!new_list) return NULL; + + completion_tree_cmd_append_arg_list(cmd, new_list); + + return new_list; +} + +void completion_tree_arg_list_add_arg(struct completion_tree_arg_list *arg_list, const char *name) +{ + if(!arg_list || !name) return; + + struct completion_tree_arg *new_node; + + new_node = calloc(1, sizeof(struct completion_tree_arg) + strlen(name) + 1); + if(!new_node) return; + + strcpy(new_node->name, name); + + new_node->next = arg_list->args; + arg_list->args = new_node; +} +#endif diff --git a/apps/mosquitto_ctrl/ctrl_shell_dynsec.c b/apps/mosquitto_ctrl/ctrl_shell_dynsec.c new file mode 100644 index 00000000..4949c315 --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell_dynsec.c @@ -0,0 +1,1016 @@ +/* +Copyright (c) 2023 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include + +#include +#include +#include +#include + +#include "ctrl_shell_internal.h" +#include "json_help.h" + +#ifdef WITH_EDITLINE + +#define UNUSED(A) (void)(A) + +static struct completion_tree_root *commands_dynsec = NULL; +static struct completion_tree_arg_list *tree_clients = NULL; +static struct completion_tree_arg_list *tree_groups = NULL; +static struct completion_tree_arg_list *tree_roles = NULL; +static bool do_print_list = false; + +static void command_tree_create(void) +{ + struct completion_tree_cmd *cmd; + struct completion_tree_arg_list *arg_list; + + completion_tree_arg_list_args_free(tree_clients); + completion_tree_arg_list_args_free(tree_groups); + completion_tree_arg_list_args_free(tree_roles); + + if(commands_dynsec) return; + + commands_dynsec = calloc(1, sizeof(struct completion_tree_root)); + + if(!tree_clients){ + tree_clients = completion_tree_cmd_new_arg_list(); + tree_clients->is_shared = true; + } + + if(!tree_groups){ + tree_groups = completion_tree_cmd_new_arg_list(); + tree_groups->is_shared = true; + } + + if(!tree_roles){ + tree_roles = completion_tree_cmd_new_arg_list(); + tree_roles->is_shared = true; + } + + cmd = completion_tree_cmd_add(commands_dynsec, "addClientRole"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + cmd = completion_tree_cmd_add(commands_dynsec, "addGroupClient"); + completion_tree_cmd_append_arg_list(cmd, tree_groups); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + cmd = completion_tree_cmd_add(commands_dynsec, "addGroupRole"); + completion_tree_cmd_append_arg_list(cmd, tree_groups); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + { + cmd = completion_tree_cmd_add(commands_dynsec, "addRoleACL"); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + arg_list = completion_tree_cmd_add_arg_list(cmd); + completion_tree_arg_list_add_arg(arg_list, "publishClientReceive"); + completion_tree_arg_list_add_arg(arg_list, "publishClientSend"); + completion_tree_arg_list_add_arg(arg_list, "subscribeLiteral"); + completion_tree_arg_list_add_arg(arg_list, "subscribePattern"); + completion_tree_arg_list_add_arg(arg_list, "unsubscribeLiteral"); + completion_tree_arg_list_add_arg(arg_list, "unsubscribePattern"); + + arg_list = completion_tree_cmd_add_arg_list(cmd); + completion_tree_arg_list_add_arg(arg_list, "allow"); + completion_tree_arg_list_add_arg(arg_list, "deny"); + } + + completion_tree_cmd_add(commands_dynsec, "createClient"); + completion_tree_cmd_add(commands_dynsec, "createGroup"); + completion_tree_cmd_add(commands_dynsec, "createRole"); + + cmd = completion_tree_cmd_add(commands_dynsec, "deleteClient"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + cmd = completion_tree_cmd_add(commands_dynsec, "deleteGroup"); + completion_tree_cmd_append_arg_list(cmd, tree_groups); + + cmd = completion_tree_cmd_add(commands_dynsec, "deleteRole"); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + cmd = completion_tree_cmd_add(commands_dynsec, "disableClient"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + cmd = completion_tree_cmd_add(commands_dynsec, "enableClient"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + completion_tree_cmd_add(commands_dynsec, "getAnonymousGroup"); + + cmd = completion_tree_cmd_add(commands_dynsec, "getClient"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + completion_tree_cmd_add(commands_dynsec, "getDefaultACLAccess"); + + cmd = completion_tree_cmd_add(commands_dynsec, "getGroup"); + completion_tree_cmd_append_arg_list(cmd, tree_groups); + + cmd = completion_tree_cmd_add(commands_dynsec, "getRole"); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + completion_tree_cmd_add(commands_dynsec, "listClients"); + completion_tree_cmd_add(commands_dynsec, "listGroups"); + completion_tree_cmd_add(commands_dynsec, "listRoles"); + + cmd = completion_tree_cmd_add(commands_dynsec, "removeClientRole"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + cmd = completion_tree_cmd_add(commands_dynsec, "removeGroupClient"); + completion_tree_cmd_append_arg_list(cmd, tree_groups); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + cmd = completion_tree_cmd_add(commands_dynsec, "removeGroupRole"); + completion_tree_cmd_append_arg_list(cmd, tree_groups); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + { + cmd = completion_tree_cmd_add(commands_dynsec, "removeRoleACL"); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + arg_list = completion_tree_cmd_add_arg_list(cmd); + completion_tree_arg_list_add_arg(arg_list, "publishClientReceive"); + completion_tree_arg_list_add_arg(arg_list, "publishClientSend"); + completion_tree_arg_list_add_arg(arg_list, "subscribeLiteral"); + completion_tree_arg_list_add_arg(arg_list, "subscribePattern"); + completion_tree_arg_list_add_arg(arg_list, "unsubscribeLiteral"); + completion_tree_arg_list_add_arg(arg_list, "unsubscribePattern"); + } + + cmd = completion_tree_cmd_add(commands_dynsec, "setAnonymousGroup"); + completion_tree_cmd_append_arg_list(cmd, tree_groups); + + cmd = completion_tree_cmd_add(commands_dynsec, "setClientId"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + cmd = completion_tree_cmd_add(commands_dynsec, "setClientPassword"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + { + cmd = completion_tree_cmd_add(commands_dynsec, "modifyClient"); + completion_tree_cmd_append_arg_list(cmd, tree_clients); + + arg_list = completion_tree_cmd_add_arg_list(cmd); + completion_tree_arg_list_add_arg(arg_list, "textName"); + completion_tree_arg_list_add_arg(arg_list, "textDescription"); + } + + { + cmd = completion_tree_cmd_add(commands_dynsec, "modifyGroup"); + completion_tree_cmd_append_arg_list(cmd, tree_groups); + + arg_list = completion_tree_cmd_add_arg_list(cmd); + completion_tree_arg_list_add_arg(arg_list, "textName"); + completion_tree_arg_list_add_arg(arg_list, "textDescription"); + } + + { + cmd = completion_tree_cmd_add(commands_dynsec, "modifyRole"); + completion_tree_cmd_append_arg_list(cmd, tree_roles); + + arg_list = completion_tree_cmd_add_arg_list(cmd); + completion_tree_arg_list_add_arg(arg_list, "allowWildcardSubs"); + completion_tree_arg_list_add_arg(arg_list, "textName"); + completion_tree_arg_list_add_arg(arg_list, "textDescription"); + } + + { + cmd = completion_tree_cmd_add(commands_dynsec, "setDefaultACLAccess"); + + arg_list = completion_tree_cmd_add_arg_list(cmd); + completion_tree_arg_list_add_arg(arg_list, "publishClientReceive"); + completion_tree_arg_list_add_arg(arg_list, "publishClientSend"); + completion_tree_arg_list_add_arg(arg_list, "subscribe"); + completion_tree_arg_list_add_arg(arg_list, "unsubscribe"); + + arg_list = completion_tree_cmd_add_arg_list(cmd); + completion_tree_arg_list_add_arg(arg_list, "allow"); + completion_tree_arg_list_add_arg(arg_list, "deny"); + } + + completion_tree_cmd_add(commands_dynsec, "disconnect"); + completion_tree_cmd_add(commands_dynsec, "help"); + completion_tree_cmd_add(commands_dynsec, "return"); + completion_tree_cmd_add(commands_dynsec, "exit"); +} + + +static void print_help(char **saveptr) +{ + char *command = strtok_r(NULL, " ", saveptr); + if(command){ + if(!strcasecmp(command, "addClientRole")){ + ctrl_shell_print_help_command("addClientRole "); + ctrl_shell_printf("\nAdds a role directly to a client.\n"); + }else if(!strcasecmp(command, "addGroupClient")){ + ctrl_shell_print_help_command("addGroupClient "); + ctrl_shell_printf("\nAdds a client to a group.\n"); + }else if(!strcasecmp(command, "addGroupRole")){ + ctrl_shell_print_help_command("addGroupRole "); + ctrl_shell_printf("\nAdds a role to a group.\n"); + }else if(!strcasecmp(command, "addRoleACL")){ + ctrl_shell_print_help_command("addRoleACL publishClientReceive allow|deny "); + ctrl_shell_print_help_command("addRoleACL publishClientSend allow|deny "); + ctrl_shell_print_help_command("addRoleACL subscribeLiteral allow|deny "); + ctrl_shell_print_help_command("addRoleACL subscribePattern allow|deny "); + ctrl_shell_print_help_command("addRoleACL unsubscribeLiteral allow|deny "); + ctrl_shell_print_help_command("addRoleACL unsubscribePattern allow|deny "); + ctrl_shell_printf("\nAdds an ACL to a role.\n"); + }else if(!strcasecmp(command, "createClient")){ + ctrl_shell_print_help_command("createClient [password [clientid]]"); + ctrl_shell_printf("\nCreate a client with password and optional client id.\n"); + }else if(!strcasecmp(command, "createGroup")){ + ctrl_shell_print_help_command("createGroup "); + ctrl_shell_printf("\nCreate a new group.\n"); + }else if(!strcasecmp(command, "createRole")){ + ctrl_shell_print_help_command("createRole "); + ctrl_shell_printf("\nCreate a new role.\n"); + }else if(!strcasecmp(command, "deleteClient")){ + ctrl_shell_print_help_command("deleteClient "); + ctrl_shell_printf("\nDelete a client\n"); + }else if(!strcasecmp(command, "deleteGroup")){ + ctrl_shell_print_help_command("deleteGroup "); + ctrl_shell_printf("\nDelete a group\n"); + }else if(!strcasecmp(command, "deleteRole")){ + ctrl_shell_print_help_command("deleteRole "); + ctrl_shell_printf("\nDelete a role\n"); + }else if(!strcasecmp(command, "disableClient")){ + ctrl_shell_print_help_command("disableClient "); + ctrl_shell_printf("\nDisable a client. This client will not be able to log in, and will be kicked if it has an existing session.\n"); + }else if(!strcasecmp(command, "enableClient")){ + ctrl_shell_print_help_command("enableClient "); + ctrl_shell_printf("\nEnable a client. Disabled clients are unable to log in.\n"); + }else if(!strcasecmp(command, "getAnonymousGroup")){ + ctrl_shell_print_help_command("getAnonymousGroup"); + ctrl_shell_printf("\nPrint the group configured as the anonymous group.\n"); + }else if(!strcasecmp(command, "getClient")){ + ctrl_shell_print_help_command("getClient "); + ctrl_shell_printf("\nPrint details of a client and its groups and direct roles.\n"); + }else if(!strcasecmp(command, "getDefaultACLAccess")){ + ctrl_shell_print_help_command("getDefaultACLAccess"); + ctrl_shell_printf("\nPrint the default allow/deny values for the different classes of ACL.\n"); + }else if(!strcasecmp(command, "getGroup")){ + ctrl_shell_print_help_command("getGroup "); + ctrl_shell_printf("\nPrint details of a group and its roles.\n"); + }else if(!strcasecmp(command, "getRole")){ + ctrl_shell_print_help_command("getRole "); + ctrl_shell_printf("\nPrint details of a role and its ACLs.\n"); + }else if(!strcasecmp(command, "listClients")){ + ctrl_shell_print_help_command("listClients [count [offset]]"); + ctrl_shell_printf("\nPrint a list of clients configured in the dynsec plugin, with an optional total count and list offset.\n"); + }else if(!strcasecmp(command, "listGroups")){ + ctrl_shell_print_help_command("listGroups [count [offset]]"); + ctrl_shell_printf("\nPrint a list of groups configured in the dynsec plugin, with an optional total count and list offset.\n"); + }else if(!strcasecmp(command, "listRoles")){ + ctrl_shell_print_help_command("listRoles [count [offset]]"); + ctrl_shell_printf("\nPrint a list of roles configured in the dynsec plugin, with an optional total count and list offset.\n"); + }else if(!strcasecmp(command, "removeClientRole")){ + ctrl_shell_print_help_command("removeClientRole "); + ctrl_shell_printf("\nRemoves a role from a client, where the role was directly attached to the client.\n"); + }else if(!strcasecmp(command, "removeGroupClient")){ + ctrl_shell_print_help_command("removeGroupClient "); + ctrl_shell_printf("\nRemoves a client from a group.\n"); + }else if(!strcasecmp(command, "removeGroupRole")){ + ctrl_shell_print_help_command("removeGroupRole "); + ctrl_shell_printf("\nRemoves a role from a group.\n"); + }else if(!strcasecmp(command, "removeRoleACL")){ + ctrl_shell_print_help_command("removeRoleACL publishClientReceive "); + ctrl_shell_print_help_command("removeRoleACL publishClientSend "); + ctrl_shell_print_help_command("removeRoleACL subscribeLiteral "); + ctrl_shell_print_help_command("removeRoleACL subscribePattern "); + ctrl_shell_print_help_command("removeRoleACL unsubscribeLiteral "); + ctrl_shell_print_help_command("removeRoleACL unsubscribePattern "); + ctrl_shell_printf("\nRemoves an ACL from a role.\n"); + }else if(!strcasecmp(command, "setAnonymousGroup")){ + ctrl_shell_print_help_command("setAnonymousGroup "); + ctrl_shell_printf("\nSets the anonymous group to a new group.\n"); + }else if(!strcasecmp(command, "setClientId")){ + ctrl_shell_print_help_command("setClientId "); + ctrl_shell_print_help_command("setClientId "); + ctrl_shell_printf("\nSets or clears the clientid associated with a client. If a client has a clientid, all three of username, password, and clientid must match for a client to be able to authenticate.\n"); + }else if(!strcasecmp(command, "setClientPassword")){ + ctrl_shell_print_help_command("setClientPassword [password]"); + ctrl_shell_printf("\nSets a new password for a client.\n"); + }else if(!strcasecmp(command, "setDefaultACLAccess")){ + ctrl_shell_print_help_command("setDefaultACLAccess publishClientReceive allow|deny"); + ctrl_shell_print_help_command("setDefaultACLAccess publishClientSend allow|deny"); + ctrl_shell_print_help_command("setDefaultACLAccess subscribe allow|deny"); + ctrl_shell_print_help_command("setDefaultACLAccess unsubscribe allow|deny"); + ctrl_shell_printf("\nSets the default ACL access to use for an ACL type. The default access will be applied if no other ACL rules match.\n"); + ctrl_shell_printf("Setting a rule to 'allow' means that if no ACLs match, it will be accepted.\n"); + ctrl_shell_printf("Setting a rule to 'deny' means that if no ACLs match, it will be denied.\n"); + }else if(!strcasecmp(command, "modifyClient")){ + ctrl_shell_print_help_command("modifyClient textName "); + ctrl_shell_print_help_command("modifyClient textDescription "); + ctrl_shell_printf("\nModify the text name or text description for a client.\n"); + ctrl_shell_printf("These are free-text fields for your own use.\n"); + }else if(!strcasecmp(command, "modifyGroup")){ + ctrl_shell_print_help_command("modifyGroup textName "); + ctrl_shell_print_help_command("modifyGroup textDescription "); + ctrl_shell_printf("\nModify the text name or text description for a group.\n"); + ctrl_shell_printf("These are free-text fields for your own use.\n"); + }else if(!strcasecmp(command, "modifyRole")){ + ctrl_shell_print_help_command("modifyRole allowWildcardSubs true|false"); + ctrl_shell_print_help_command("modifyRole textName "); + ctrl_shell_print_help_command("modifyRole textDescription "); + ctrl_shell_printf("\nModify the text name or text description for a role.\n"); + ctrl_shell_printf("These are free-text fields for your own use.\n"); + }else{ + ctrl_shell_print_help_final(command, "dynsec"); + } + }else{ + ctrl_shell_printf("This is the mosquitto_ctrl interactive shell, for controlling aspects of a mosquitto broker.\n"); + ctrl_shell_printf("You are in dynsec mode, for controlling the dynamic-security clients, groups, and roles used in authentication and authorisation.\n"); + ctrl_shell_printf("Use '%sreturn%s' to leave dynsec mode.\n", ANSI_INPUT, ANSI_RESET); + + ctrl_shell_printf("Find help on a command using '%shelp %s'\n", ANSI_INPUT, ANSI_RESET); + ctrl_shell_printf("Press tab multiple times to find currently available commands.\n\n"); + } +} + +static int send_set_default_acl_access(char **saveptr) +{ + const char *acltype, *allow_s; + + acltype = strtok_r(NULL, " ", saveptr); + if(!acltype || ( + strcasecmp(acltype, "publishClientReceive") + && strcasecmp(acltype, "publishClientSend") + && strcasecmp(acltype, "subscribe") + && strcasecmp(acltype, "unsubscribe") + )){ + ctrl_shell_printf("setDefaultACLAccess acltype allow|deny\n"); + return MOSQ_ERR_INVAL; + } + + allow_s = strtok_r(NULL, " ", saveptr); + if(!allow_s || ( + strcasecmp(allow_s, "allow") + && strcasecmp(allow_s, "deny") + )){ + ctrl_shell_printf("setDefaultACLAccess acltype allow|deny\n"); + return MOSQ_ERR_INVAL; + } + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", "setDefaultACLAccess"); + cJSON *j_acls = cJSON_AddArrayToObject(j_command, "acls"); + cJSON *j_acl = cJSON_CreateObject(); + cJSON_AddItemToArray(j_acls, j_acl); + cJSON_AddStringToObject(j_acl, "acltype", acltype); + cJSON_AddBoolToObject(j_acl, "allow", !strcmp(allow_s, "allow")); + + return ctrl_shell_publish_blocking(j_command); +} + +static int list_update(const char *command) +{ + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", command); + + do_print_list = false; + + return ctrl_shell_publish_blocking(j_command); +} + +static int list_generic(const char *command, char **saveptr) +{ + const char *count, *offset; + + count = strtok_r(NULL, " ", saveptr); + offset = strtok_r(NULL, " ", saveptr); + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", command); + if(count){ + cJSON_AddNumberToObject(j_command, "count", atoi(count)); + } + if(offset){ + cJSON_AddNumberToObject(j_command, "offset", atoi(offset)); + } + + return ctrl_shell_publish_blocking(j_command); +} + +static int send_create_client(char **saveptr) +{ + char *username = strtok_r(NULL, " ", saveptr); + if(!username){ + ctrl_shell_printf("createClient username [password [clientid]]\n"); + ctrl_shell_printf("createClient username password [clientid]\n"); + return MOSQ_ERR_INVAL; + } + char *password = strtok_r(NULL, " ", saveptr); + char pwbuf1[200]; + char pwbuf2[200]; + char *clientid = NULL; + if(password){ + clientid = strtok_r(NULL, " ", saveptr); + }else{ + if(!ctrl_shell_get_password(pwbuf1, sizeof(pwbuf1)) + || !ctrl_shell_get_password(pwbuf2, sizeof(pwbuf2))){ + + ctrl_shell_printf("No password.\n"); + return MOSQ_ERR_INVAL; + } + + if(strcmp(pwbuf1, pwbuf2)){ + ctrl_shell_printf("Passwords do not match.\n"); + return MOSQ_ERR_INVAL; + } + + password = pwbuf1; + } + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", "createClient"); + cJSON_AddStringToObject(j_command, "username", username); + cJSON_AddStringToObject(j_command, "password", password); + if(clientid){ + cJSON_AddStringToObject(j_command, "clientid", clientid); + } + ctrl_shell_publish_blocking(j_command); + + return MOSQ_ERR_SUCCESS; +} + +static int send_add_role_acl(char **saveptr) +{ + char *rolename = strtok_r(NULL, " ", saveptr); + char *acltype = strtok_r(NULL, " ", saveptr); + char *allow_s = strtok_r(NULL, " ", saveptr); + char *topic = strtok_r(NULL, " ", saveptr); + + if(!rolename || !acltype || !allow_s || !topic){ + ctrl_shell_printf("addRoleACL rolename acltype allow|deny topic\n"); + return MOSQ_ERR_INVAL; + } + + if(strcasecmp(acltype, "publishClientReceive") + && strcasecmp(acltype, "publishClientSend") + && strcasecmp(acltype, "subscribeLiteral") + && strcasecmp(acltype, "subscribePattern") + && strcasecmp(acltype, "unsubscribeLiteral") + && strcasecmp(acltype, "unsubscribePattern") + ){ + + ctrl_shell_printf("addRoleACL rolename acltype allow|deny topic\n"); + ctrl_shell_printf("Invalid acltype '%s'\n", acltype); + return MOSQ_ERR_INVAL; + } + + if(strcasecmp(allow_s, "allow") && strcasecmp(allow_s, "deny")){ + ctrl_shell_printf("addRoleACL rolename acltype allow|deny topic\n"); + ctrl_shell_printf("Invalid allow/deny '%s'\n", allow_s); + return MOSQ_ERR_INVAL; + } + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", "addRoleACL"); + cJSON_AddStringToObject(j_command, "rolename", rolename); + cJSON_AddStringToObject(j_command, "acltype", acltype); + cJSON_AddStringToObject(j_command, "topic", topic); + cJSON_AddBoolToObject(j_command, "allow", !strcasecmp(allow_s, "allow")); + + return ctrl_shell_publish_blocking(j_command); +} + +static int send_remove_role_acl(char **saveptr) +{ + char *rolename = strtok_r(NULL, " ", saveptr); + char *acltype = strtok_r(NULL, " ", saveptr); + char *topic = strtok_r(NULL, " ", saveptr); + + if(!rolename || !acltype || !topic){ + ctrl_shell_printf("removeRoleACL rolename acltype topic\n"); + return MOSQ_ERR_INVAL; + } + + if(strcasecmp(acltype, "publishClientReceive") + && strcasecmp(acltype, "publishClientSend") + && strcasecmp(acltype, "subscribeLiteral") + && strcasecmp(acltype, "subscribePattern") + && strcasecmp(acltype, "unsubscribeLiteral") + && strcasecmp(acltype, "unsubscribePattern") + ){ + + + ctrl_shell_printf("removeRoleACL rolename acltype topic\n"); + ctrl_shell_printf("Invalid acltype '%s'\n", acltype); + return MOSQ_ERR_INVAL; + } + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", "removeRoleACL"); + cJSON_AddStringToObject(j_command, "rolename", rolename); + cJSON_AddStringToObject(j_command, "acltype", acltype); + cJSON_AddStringToObject(j_command, "topic", topic); + + return ctrl_shell_publish_blocking(j_command); + +} + +static int send_modify(const char *command, const char *objectname, char **saveptr) +{ + char *name = strtok_r(NULL, " ", saveptr); + char *itemlabel = strtok_r(NULL, " ", saveptr); + char *itemvalue = *saveptr; + if(!name || !itemlabel || !itemvalue){ + ctrl_shell_printf("%s %s \n", command, objectname); + return MOSQ_ERR_INVAL; + } + + if(strcasecmp(itemlabel, "textName") && strcasecmp(itemlabel, "textDescription") && strcasecmp(itemlabel, "allowWildcardSubs")){ + ctrl_shell_printf("%s %s \n", command, objectname); + ctrl_shell_printf("Unknown property '%s'\n", itemlabel); + return MOSQ_ERR_INVAL; + } + + if(!strcasecmp(itemlabel, "allowWildcardSubs")){ + if(strcasecmp(itemvalue, "true") && strcasecmp(itemvalue, "false")){ + ctrl_shell_printf("%s %s \n", command, objectname); + ctrl_shell_printf("Invalid value '%s'\n", itemvalue); + return MOSQ_ERR_INVAL; + } + } + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", command); + cJSON_AddStringToObject(j_command, objectname, name); + if(!strcasecmp(itemlabel, "allowWildcardSubs")){ + cJSON_AddBoolToObject(j_command, itemlabel, !strcasecmp(itemvalue, "true")); + }else{ + cJSON_AddStringToObject(j_command, itemlabel, itemvalue); + } + + return ctrl_shell_publish_blocking(j_command); +} + +static int send_set_client_password(char **saveptr) +{ + char *username, *password; + char pwbuf1[200], pwbuf2[200]; + + username = strtok_r(NULL, " ", saveptr); + if(!username){ + ctrl_shell_printf("setClientPassword [password]\n"); + return MOSQ_ERR_INVAL; + } + password = strtok_r(NULL, " ", saveptr); + if(!password){ + if(!ctrl_shell_get_password(pwbuf1, sizeof(pwbuf1)) + || !ctrl_shell_get_password(pwbuf2, sizeof(pwbuf2))){ + + ctrl_shell_printf("No password.\n"); + return MOSQ_ERR_INVAL; + } + + if(strcmp(pwbuf1, pwbuf2)){ + ctrl_shell_printf("Passwords do not match.\n"); + return MOSQ_ERR_INVAL; + } + + password = pwbuf1; + } + + cJSON *j_command = cJSON_CreateObject(); + cJSON_AddStringToObject(j_command, "command", "setClientPassword"); + cJSON_AddStringToObject(j_command, "username", username); + cJSON_AddStringToObject(j_command, "password", password); + + return ctrl_shell_publish_blocking(j_command); +} + + +static void line_callback(char *line) +{ + if(!line){ + ctrl_shell_callback_final(NULL); + return; + } + ctrl_shell_rtrim(line); + if(strlen(line) > 0){ + add_history(line); + }else{ + free(line); + return; + } + + char *saveptr = NULL; + char *command = strtok_r(line, " ", &saveptr); + + if(!strcasecmp(command, "addClientRole")){ + ctrl_shell_command_generic_arg2("addClientRole", "username", "rolename", &saveptr); + }else if(!strcasecmp(command, "addGroupClient")){ + ctrl_shell_command_generic_arg2("addGroupClient", "groupname", "username", &saveptr); + }else if(!strcasecmp(command, "addGroupRole")){ + ctrl_shell_command_generic_arg2("addGroupRole", "groupname", "rolename", &saveptr); + }else if(!strcasecmp(command, "addRoleACL")){ + send_add_role_acl(&saveptr); + }else if(!strcasecmp(command, "createClient")){ + if(send_create_client(&saveptr) == MOSQ_ERR_SUCCESS){ + list_update("listClients"); + } + }else if(!strcasecmp(command, "createGroup")){ + if(ctrl_shell_command_generic_arg1("createGroup", "groupname", &saveptr) == MOSQ_ERR_SUCCESS){ + list_update("listGroups"); + } + }else if(!strcasecmp(command, "createRole")){ + if(ctrl_shell_command_generic_arg1("createRole", "rolename", &saveptr) == MOSQ_ERR_SUCCESS){ + list_update("listRoles"); + } + }else if(!strcasecmp(command, "deleteClient")){ + if(ctrl_shell_command_generic_arg1("deleteClient", "username", &saveptr) == MOSQ_ERR_SUCCESS){ + list_update("listClients"); + } + }else if(!strcasecmp(command, "deleteGroup")){ + if(ctrl_shell_command_generic_arg1("deleteGroup", "groupname", &saveptr) == MOSQ_ERR_SUCCESS){ + list_update("listGroups"); + } + }else if(!strcasecmp(command, "deleteRole")){ + if(ctrl_shell_command_generic_arg1("deleteRole", "rolename", &saveptr) == MOSQ_ERR_SUCCESS){ + list_update("listRoles"); + } + }else if(!strcasecmp(command, "disableClient")){ + ctrl_shell_command_generic_arg1("disableClient", "username", &saveptr); + }else if(!strcasecmp(command, "enableClient")){ + ctrl_shell_command_generic_arg1("enableClient", "username", &saveptr); + }else if(!strcasecmp(command, "getAnonymousGroup")){ + ctrl_shell_command_generic_arg0("getAnonymousGroup"); + }else if(!strcasecmp(command, "getClient")){ + ctrl_shell_command_generic_arg1("getClient", "username", &saveptr); + }else if(!strcasecmp(command, "getDefaultACLAccess")){ + ctrl_shell_command_generic_arg0("getDefaultACLAccess"); + }else if(!strcasecmp(command, "getGroup")){ + ctrl_shell_command_generic_arg1("getGroup", "groupname", &saveptr); + }else if(!strcasecmp(command, "getRole")){ + ctrl_shell_command_generic_arg1("getRole", "rolename", &saveptr); + }else if(!strcasecmp(command, "listClients")){ + do_print_list = true; + list_generic("listClients", &saveptr); + }else if(!strcasecmp(command, "listGroups")){ + do_print_list = true; + list_generic("listGroups", &saveptr); + }else if(!strcasecmp(command, "listRoles")){ + do_print_list = true; + list_generic("listRoles", &saveptr); + }else if(!strcasecmp(command, "removeClientRole")){ + ctrl_shell_command_generic_arg2("removeClientRole", "username", "rolename", &saveptr); + }else if(!strcasecmp(command, "removeGroupClient")){ + ctrl_shell_command_generic_arg2("removeGroupClient", "groupname", "username", &saveptr); + }else if(!strcasecmp(command, "removeGroupRole")){ + ctrl_shell_command_generic_arg2("removeGroupRole", "groupname", "rolename", &saveptr); + }else if(!strcasecmp(command, "removeRoleACL")){ + send_remove_role_acl(&saveptr); + }else if(!strcasecmp(command, "setAnonymousGroup")){ + ctrl_shell_command_generic_arg1("setAnonymousGroup", "groupname", &saveptr); + }else if(!strcasecmp(command, "setClientId")){ + ctrl_shell_command_generic_arg2("setClientId", "username", "clientid", &saveptr); + }else if(!strcasecmp(command, "setClientPassword")){ + send_set_client_password(&saveptr); + }else if(!strcasecmp(command, "modifyClient")){ + send_modify("modifyClient", "username", &saveptr); + }else if(!strcasecmp(command, "modifyGroup")){ + send_modify("modifyGroup", "groupname", &saveptr); + }else if(!strcasecmp(command, "modifyRole")){ + send_modify("modifyRole", "rolename", &saveptr); + }else if(!strcasecmp(command, "setDefaultACLAccess")){ + send_set_default_acl_access(&saveptr); + }else if(!strcasecmp(command, "help")){ + print_help(&saveptr); + }else{ + if(!ctrl_shell_callback_final(line)){ + ctrl_shell_printf("Unknown command '%s'\n", command); + } + } + + free(line); +} + +static void print_json_value(cJSON *value, const char *null_value) +{ + if(value){ + if(cJSON_IsString(value)){ + if(value->valuestring){ + ctrl_shell_print_value(0, "%s", value->valuestring); + } + }else{ + char buffer[1024]; + cJSON_PrintPreallocated(value, buffer, sizeof(buffer), 0); + ctrl_shell_print_value(0, "%s", buffer); + } + }else if(null_value){ + ctrl_shell_print_value(0, "%s", null_value); + } +} + +static void print_json_array(cJSON *j_list, const char *label, const char *element_name, const char *optional_element_name, const char *optional_element_null_value) +{ + cJSON *j_elem; + + if(j_list && cJSON_IsArray(j_list) && cJSON_GetArraySize(j_list) > 0){ + ctrl_shell_print_label(0, label); + cJSON_ArrayForEach(j_elem, j_list){ + if(cJSON_IsObject(j_elem)){ + const char *stmp; + if(json_get_string(j_elem, element_name, &stmp, false) != MOSQ_ERR_SUCCESS){ + continue; + } + ctrl_shell_print_value(1, "%s", stmp); + if(optional_element_name){ + ctrl_shell_print_value(0, " (%s: ", optional_element_name); + print_json_value(cJSON_GetObjectItem(j_elem,optional_element_name),optional_element_null_value); + ctrl_shell_print_value(0, ")"); + } + }else if(cJSON_IsString(j_elem) && j_elem->valuestring){ + ctrl_shell_print_value(1, "%s", j_elem->valuestring); + } + ctrl_shell_print_value(0, "\n"); + } + } +} + + +static void print_client(cJSON *j_data) +{ + cJSON *j_client, *jtmp; + + j_client = cJSON_GetObjectItem(j_data, "client"); + if(j_client == NULL){ + ctrl_shell_printf("Invalid response from broker.\n"); + return; + } + + const char *username; + if(json_get_string(j_client, "username", &username, false) != MOSQ_ERR_SUCCESS){ + ctrl_shell_printf("Invalid response from broker.\n"); + return; + } + ctrl_shell_print_label(0, "Username:"); + ctrl_shell_print_value(1, "%s\n", username); + + const char *clientid; + if(json_get_string(j_client, "clientid", &clientid, false) == MOSQ_ERR_SUCCESS){ + ctrl_shell_print_label(0, "Clientid:"); + ctrl_shell_print_value(1, "%s\n", clientid); + } + + jtmp = cJSON_GetObjectItem(j_client, "disabled"); + if(jtmp && cJSON_IsBool(jtmp) && cJSON_IsTrue(jtmp)){ + ctrl_shell_print_label(0, "Disabled:"); + ctrl_shell_print_value(1, "true\n"); + } + + const char *textname; + if(json_get_string(j_client, "textname", &textname, false) == MOSQ_ERR_SUCCESS){ + ctrl_shell_print_label(0, "Text name:"); + ctrl_shell_print_value(1, "%s\n", textname); + } + + const char *textdescription; + if(json_get_string(j_client, "textdescription", &textdescription, false) == MOSQ_ERR_SUCCESS){ + ctrl_shell_print_label(0, "Text description:"); + ctrl_shell_print_value(1, "%s\n", textdescription); + } + + print_json_array(cJSON_GetObjectItem(j_client, "roles"), "Roles:", "rolename", "priority", "-1"); + print_json_array(cJSON_GetObjectItem(j_client, "groups"), "Groups:", "groupname", "priority", "-1"); +} + +static void print_group(cJSON *j_data) +{ + cJSON *j_group; + + j_group = cJSON_GetObjectItem(j_data, "group"); + if(j_group == NULL){ + ctrl_shell_printf("Invalid response from broker.\n"); + return; + } + + const char *groupname; + if(json_get_string(j_group, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + ctrl_shell_printf("Invalid response from broker.\n"); + return; + } + ctrl_shell_print_label(0, "Group name:"); + ctrl_shell_print_value(1, "%s\n", groupname); + + const char *textname; + if(json_get_string(j_group, "textname", &textname, false) == MOSQ_ERR_SUCCESS){ + ctrl_shell_print_label(0, "Text name:"); + ctrl_shell_print_value(1, "%s\n", textname); + } + + const char *textdescription; + if(json_get_string(j_group, "textdescription", &textdescription, false) == MOSQ_ERR_SUCCESS){ + ctrl_shell_print_label(0, "Text description:"); + ctrl_shell_print_value(1, "%s\n", textdescription); + } + + print_json_array(cJSON_GetObjectItem(j_group, "roles"), "Roles:", "rolename", "priority", "-1"); + print_json_array(cJSON_GetObjectItem(j_group, "clients"), "Clients:", "username", NULL, NULL); +} + + +static void print_role(cJSON *j_data) +{ + cJSON *j_role; + + j_role = cJSON_GetObjectItem(j_data, "role"); + if(j_role == NULL){ + ctrl_shell_printf("Invalid response from broker.\n"); + return; + } + + const char *rolename; + if(json_get_string(j_role, "rolename", &rolename, false) != MOSQ_ERR_SUCCESS){ + ctrl_shell_printf("Invalid response from broker.\n"); + return; + } + ctrl_shell_print_label(0, "Role name:"); + ctrl_shell_print_value(1, "%s\n", rolename); + + const char *textname; + if(json_get_string(j_role, "textname", &textname, false) == MOSQ_ERR_SUCCESS){ + ctrl_shell_print_label(0, "Text name:"); + ctrl_shell_print_value(1, "%s\n", textname); + } + + const char *textdescription; + if(json_get_string(j_role, "textdescription", &textdescription, false) == MOSQ_ERR_SUCCESS){ + ctrl_shell_print_label(0, "Text description:"); + ctrl_shell_print_value(1, "%s\n", textdescription); + } + + bool allowwildcardsubs; + if(json_get_bool(j_role, "allowwildcardsubs", &allowwildcardsubs, false, false) == MOSQ_ERR_SUCCESS){ + ctrl_shell_print_label(0, "Allow wildcard subscriptions:"); + ctrl_shell_print_value(1, "%s\n", allowwildcardsubs?"true":"false"); + } + + cJSON *j_acls = cJSON_GetObjectItem(j_role, "acls"); + if(j_acls && cJSON_GetArraySize(j_acls) > 0){ + ctrl_shell_print_label(0, "ACLs:"); + + cJSON *j_acl; + cJSON_ArrayForEach(j_acl, j_acls){ + const char *acltype; + const char *topic; + int priority; + bool allow; + + if(json_get_string(j_acl, "acltype", &acltype, false) == MOSQ_ERR_SUCCESS + && json_get_string(j_acl, "topic", &topic, false) == MOSQ_ERR_SUCCESS + && json_get_int(j_acl, "priority", &priority, true, -1) == MOSQ_ERR_SUCCESS + && json_get_bool(j_acl, "allow", &allow, false, false) == MOSQ_ERR_SUCCESS + ){ + + ctrl_shell_print_value(1, "%-*s %s %s (priority %d)\n", (int)strlen("publishClientReceive"), acltype, allow?"allow":"deny ", topic, priority); + } + } + } +} + +static void print_default_acls(cJSON *j_data) +{ + cJSON *j_acls = cJSON_GetObjectItem(j_data, "acls"); + + if(j_acls && cJSON_GetArraySize(j_acls) > 0){ + cJSON *j_acl; + cJSON_ArrayForEach(j_acl, j_acls){ + const char *acltype; + bool allow; + + if(json_get_string(j_acl, "acltype", &acltype, false) == MOSQ_ERR_SUCCESS + && json_get_bool(j_acl, "allow", &allow, false, false) == MOSQ_ERR_SUCCESS + ){ + + ctrl_shell_print_value(0, "%-*s %s\n", (int)strlen("publishClientReceive"), acltype, allow?"allow":"deny"); + } + } + } +} + +static void response_callback(const char *command, cJSON *j_data, const char *payload) +{ + if(!strcmp(command, "listClients")){ + completion_tree_arg_list_args_free(tree_clients); + + cJSON *clients, *client; + + clients = cJSON_GetObjectItem(j_data, "clients"); + cJSON_ArrayForEach(client, clients){ + if(do_print_list){ + ctrl_shell_print_value(0, "%s\n", client->valuestring); + } + completion_tree_arg_list_add_arg(tree_clients, client->valuestring); + } + do_print_list = false; + }else if(!strcmp(command, "listGroups")){ + completion_tree_arg_list_args_free(tree_groups); + + cJSON *groups, *group; + + groups = cJSON_GetObjectItem(j_data, "groups"); + cJSON_ArrayForEach(group, groups){ + if(do_print_list){ + ctrl_shell_print_value(0, "%s\n", group->valuestring); + } + completion_tree_arg_list_add_arg(tree_groups, group->valuestring); + } + do_print_list = false; + }else if(!strcmp(command, "listRoles")){ + completion_tree_arg_list_args_free(tree_roles); + + cJSON *roles, *role; + + roles = cJSON_GetObjectItem(j_data, "roles"); + cJSON_ArrayForEach(role, roles){ + if(do_print_list){ + ctrl_shell_print_value(0, "%s\n", role->valuestring); + } + completion_tree_arg_list_add_arg(tree_roles, role->valuestring); + } + do_print_list = false; + }else if(!strcasecmp(command, "getAnonymousGroup")){ + cJSON *group, *groupname; + + group = cJSON_GetObjectItem(j_data, "group"); + groupname = cJSON_GetObjectItem(group, "groupname"); + ctrl_shell_print_value(0, "%s\n", groupname->valuestring); + }else if(!strcasecmp(command, "getClient")){ + print_client(j_data); + }else if(!strcasecmp(command, "getGroup")){ + print_group(j_data); + }else if(!strcasecmp(command, "getRole")){ + print_role(j_data); + }else if(!strcasecmp(command, "getDefaultACLAccess")){ + print_default_acls(j_data); + }else{ + //ctrl_shell_printf("%s %s\n", command, payload); + } +} + + +static void on_subscribe(void) +{ + int rc; + + rc = list_update("listClients"); + if(rc){ + ctrl_shell_printf("Check the dynsec module is configured on the broker.\n"); + return; + } + list_update("listGroups"); + list_update("listRoles"); +} + + +static void ctrl_shell__dynsec_cleanup(void) +{ + completion_tree_free(commands_dynsec); + commands_dynsec = NULL; + + completion_tree_arg_list_args_free(tree_clients); + completion_tree_arg_list_args_free(tree_groups); + completion_tree_arg_list_args_free(tree_roles); + + free(tree_clients); + free(tree_groups); + free(tree_roles); + + tree_clients = NULL; + tree_groups = NULL; + tree_roles = NULL; +} + + +void ctrl_shell__dynsec_init(struct ctrl_shell__module *mod) +{ + command_tree_create(); + + mod->completion_commands = commands_dynsec; + mod->request_topic = "$CONTROL/dynamic-security/v1"; + mod->response_topic = "$CONTROL/dynamic-security/v1/response"; + mod->line_callback = line_callback; + mod->response_callback = response_callback; + mod->on_subscribe = on_subscribe; + mod->cleanup = ctrl_shell__dynsec_cleanup; +} +#endif diff --git a/apps/mosquitto_ctrl/ctrl_shell_internal.h b/apps/mosquitto_ctrl/ctrl_shell_internal.h new file mode 100644 index 00000000..e28d9b36 --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell_internal.h @@ -0,0 +1,105 @@ +/* +Copyright (c) 2023 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#ifndef CTRL_SHELL_INTERNAL_H +#define CTRL_SHELL_INTERNAL_H + +#ifdef WITH_EDITLINE + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include + +#include "ctrl_shell.h" +#include "mosquitto_ctrl.h" + +struct ctrl_shell{ + char **subscription_list; + int subscription_list_count; + int run; + struct mosquitto *mosq; + rl_vcpfunc_t *line_callback; + pthread_cond_t response_cond; + pthread_mutex_t response_mutex; + const char *request_topic; + struct completion_tree_root *commands; + void (*response_callback)(const char *command, cJSON *data, const char *payload); + void (*mod_cleanup)(void); + const char *url_scheme; + char *username; + char *password; + char *clientid; + char *tls_cafile; + char *tls_capath; + char *tls_certfile; + char *tls_keyfile; + char *hostname; + int port; + int connect_rc; + int subscribe_rc; + int publish_rc; + int transport; +}; + +struct ctrl_shell__module{ + struct completion_tree_root *completion_commands; + const char *request_topic; + const char *response_topic; + void (*response_callback)(const char *command, cJSON *data, const char *payload); + void (*line_callback)(char *line); + void (*on_subscribe)(void); + void (*cleanup)(void); +}; + +extern struct ctrl_shell data; +extern char prompt[200]; + +void term__set_echo(bool echo); + +int do_connect(void); + +void ctrl_shell__pre_connect_init(void); +void ctrl_shell__pre_connect_cleanup(void); +void ctrl_shell__post_connect_init(void); +void ctrl_shell__post_connect_cleanup(void); +void ctrl_shell__load_module(void (*mod_init)(struct ctrl_shell__module *mod)); + +void ctrl_shell__connect_blocking(const char *hostname, int port); +void ctrl_shell__on_connect(struct mosquitto *mosq, void *userdata, int rc); +void ctrl_shell__on_subscribe(struct mosquitto *mosq, void *userdata, int mid, int qos_count, const int *granted_qos); +void ctrl_shell__on_publish(struct mosquitto *mosq, void *userdata, int mid, int reason_code, const mosquitto_property *props); +void ctrl_shell__on_message(struct mosquitto *mosq, void *userdata, const struct mosquitto_message *msg); + +void ctrl_shell__broker_init(struct ctrl_shell__module *mod); +void ctrl_shell__dynsec_init(struct ctrl_shell__module *mod); + +void ctrl_shell__main(struct mosq_config *config); + +int ctrl_shell__connect(void); +void ctrl_shell__disconnect(void); + +void ctrl_shell__output(const char *buf); + +#ifdef __cplusplus +} +#endif + +#endif +#endif diff --git a/apps/mosquitto_ctrl/ctrl_shell_io.c b/apps/mosquitto_ctrl/ctrl_shell_io.c new file mode 100644 index 00000000..0d2e68c6 --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell_io.c @@ -0,0 +1,31 @@ +/* +Copyright (c) 2025 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include "config.h" + +#include +#include "ctrl_shell.h" + +void ctrl_shell__output(const char *buf) +{ + printf("%s", buf); +} + +char *ctrl_shell_fgets(char *s, int size, FILE *stream) +{ + return fgets(s, size, stream); +} diff --git a/apps/mosquitto_ctrl/ctrl_shell_post_connect.c b/apps/mosquitto_ctrl/ctrl_shell_post_connect.c new file mode 100644 index 00000000..0c284298 --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell_post_connect.c @@ -0,0 +1,143 @@ +/* +Copyright (c) 2023 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include + +#include +#include + +#include "ctrl_shell_internal.h" + +#ifdef WITH_EDITLINE + +#define UNUSED(A) (void)(A) + +static struct completion_tree_root *commands_post_connect = NULL; + +static void command_tree_create(void) +{ + if(commands_post_connect) return; + + commands_post_connect = calloc(1, sizeof(struct completion_tree_root)); + + completion_tree_cmd_add(commands_post_connect, "broker"); + completion_tree_cmd_add(commands_post_connect, "disconnect"); + completion_tree_cmd_add(commands_post_connect, "dynsec"); + completion_tree_cmd_add(commands_post_connect, "help"); + completion_tree_cmd_add(commands_post_connect, "exit"); +} + +static void print_help(char **saveptr) +{ + char *command = strtok_r(NULL, " ", saveptr); + if(command){ + if(!strcasecmp(command, "dynsec")){ + ctrl_shell_print_help_command("dynsec"); + ctrl_shell_printf("\nStart the dynamic-security control mode.\n"); + }else if(!strcasecmp(command, "broker")){ + ctrl_shell_print_help_command("broker"); + ctrl_shell_printf("\nStart the broker control mode.\n"); + }else{ + ctrl_shell_print_help_final(command, NULL); + } + }else{ + ctrl_shell_printf("This is the mosquitto_ctrl interactive shell, for controlling aspects of a mosquitto broker.\n"); + + ctrl_shell_printf("Find help on a command using '%shelp %s'\n", ANSI_INPUT, ANSI_RESET); + ctrl_shell_printf("Press tab multiple times to find currently available commands.\n\n"); + + ctrl_shell_printf("Example workflow:\n\n"); + ctrl_shell_printf("> auth\n"); + ctrl_shell_printf("username: admin\n"); + ctrl_shell_printf("password:\n"); + ctrl_shell_printf("> connect mqtt://localhost\n"); + ctrl_shell_printf("mqtt://localhost:1883> dynsec\n"); + ctrl_shell_printf("mqtt://localhost:1883|dynsec> createGroup newgroup\n"); + ctrl_shell_printf("OK\n\n"); + } +} + +struct module_data{ + const char *name; + void (*mod_init)(struct ctrl_shell__module *mod); +}; + +const struct module_data mod_data[] = { + { "broker", ctrl_shell__broker_init }, + { "dynsec", ctrl_shell__dynsec_init }, +}; + +static void line_callback(char *line) +{ + if(!line){ + ctrl_shell_callback_final(NULL); + return; + } + + ctrl_shell_rtrim(line); + if(strlen(line) > 0){ + add_history(line); + }else{ + free(line); + return; + } + + char *saveptr = NULL; + char *command = strtok_r(line, " ", &saveptr); + bool found = false; + + for(size_t i=0; i%s ", + ANSI_URL, data.url_scheme, data.hostname, data.port, ANSI_RESET, + ANSI_MODULE, mod_data[i].name, ANSI_INPUT, ANSI_RESET); + ctrl_shell__load_module(mod_data[i].mod_init); + found = true; + break; + } + } + if(!strcasecmp(command, "help")){ + found = true; + print_help(&saveptr); + } + if(found == false){ + if(!ctrl_shell_callback_final(line)){ + ctrl_shell_printf("Unknown command '%s'\n", command); + } + } + + free(line); +} + + +void ctrl_shell__post_connect_cleanup(void) +{ + completion_tree_free(commands_post_connect); + commands_post_connect = NULL; +} + + +void ctrl_shell__post_connect_init(void) +{ + command_tree_create(); + + ctrl_shell_completion_commands_set(commands_post_connect); + ctrl_shell_line_callback_set(line_callback); + + snprintf(prompt, sizeof(prompt), "%s%s://%s:%d%s>%s ", ANSI_URL, data.url_scheme, data.hostname, data.port, ANSI_INPUT, ANSI_RESET); +} +#endif diff --git a/apps/mosquitto_ctrl/ctrl_shell_pre_connect.c b/apps/mosquitto_ctrl/ctrl_shell_pre_connect.c new file mode 100644 index 00000000..97b95389 --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell_pre_connect.c @@ -0,0 +1,210 @@ +/* +Copyright (c) 2023 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include + +#include +#include +#include + +#include "ctrl_shell_internal.h" + +#ifdef WITH_EDITLINE + +#define UNUSED(A) (void)(A) + +static struct completion_tree_root *commands_pre_connect = NULL; + +static void command_tree_create(void) +{ + if(commands_pre_connect) return; + + commands_pre_connect = calloc(1, sizeof(struct completion_tree_root)); + + completion_tree_cmd_add(commands_pre_connect, "auth"); + completion_tree_cmd_add(commands_pre_connect, "connect"); + completion_tree_cmd_add(commands_pre_connect, "help"); + completion_tree_cmd_add(commands_pre_connect, "exit"); +} + + +void print_help(char **saveptr) +{ + char *command = strtok_r(NULL, " ", saveptr); + if(command){ + if(!strcasecmp(command, "auth")){ + ctrl_shell_print_help_command("auth [username]"); + ctrl_shell_printf("\nSet a username and password prior to connecting to a broker.\n"); + }else if(!strcasecmp(command, "connect")){ + ctrl_shell_print_help_command("connect"); + ctrl_shell_print_help_command("connect mqtt://hostname[:port]"); + ctrl_shell_print_help_command("connect mqtts://hostname[:port]"); + ctrl_shell_print_help_command("connect ws://hostname[:port]"); + ctrl_shell_print_help_command("connect wss://hostname[:port]"); + ctrl_shell_printf("\nConnect to a broker using the provided transport and port.\n"); + ctrl_shell_printf("If no URL is provided, connects to mqtt://localhost:1883\n"); + }else if(!strcasecmp(command, "exit")){ + ctrl_shell_print_help_command("exit"); + ctrl_shell_printf("\nQuit the program\n"); + }else if(!strcasecmp(command, "help")){ + ctrl_shell_print_help_command("help "); + ctrl_shell_printf("\nFind help on a command using '%shelp %s'\n", ANSI_INPUT, ANSI_RESET); + ctrl_shell_printf("Press tab multiple times to find currently available commands.\n"); + }else{ + ctrl_shell_printf("Unknown command '%s'\n", command); + } + }else{ + ctrl_shell_printf("This is the mosquitto_ctrl interactive shell, for controlling aspects of a mosquitto broker.\n"); + + ctrl_shell_printf("Find help on a command using '%shelp %s'\n", ANSI_INPUT, ANSI_RESET); + ctrl_shell_printf("Press tab multiple times to find currently available commands.\n\n"); + + ctrl_shell_printf("Example workflow:\n\n"); + ctrl_shell_printf("> auth\n"); + ctrl_shell_printf("username: admin\n"); + ctrl_shell_printf("password:\n"); + ctrl_shell_printf("> connect mqtt://localhost\n"); + ctrl_shell_printf("mqtt://localhost:1883> dynsec\n"); + ctrl_shell_printf("mqtt://localhost:1883|dynsec> createGroup newgroup\n"); + ctrl_shell_printf("OK\n\n"); + } +} + + +static void line_callback(char *line) +{ + char *saveptr = NULL; + char *command; + + if(!line){ + ctrl_shell_callback_final(NULL); + return; + } + + ctrl_shell_rtrim(line); + if(strlen(line) > 0){ + add_history(line); + }else{ + free(line); + return; + } + + command = strtok_r(line, " ", &saveptr); + + if(!strcasecmp(command, "auth")){ + free(data.username); + char *username_i = strtok_r(NULL, " ", &saveptr); + if(username_i){ + data.username = strdup(username_i); + }else{ + char promptbuf[50]; + snprintf(promptbuf, sizeof(promptbuf), "%susername:%s ", ANSI_INPUT, ANSI_RESET); + data.username = readline(promptbuf); + } + + char pwbuf[200]; + if(!ctrl_shell_get_password(pwbuf, sizeof(pwbuf))){ + ctrl_shell_printf("No password.\n"); + free(line); + return; + } + free(data.password); + data.password = strdup(pwbuf); + }else if(!strcasecmp(command, "connect")){ + char *url = strtok_r(NULL, " ", &saveptr); + if(url){ + if(!strncasecmp(url, "mqtt://", 7)) { + url += 7; + data.port = 1883; + data.url_scheme = "mqtt"; + } else if(!strncasecmp(url, "mqtts://", 8)) { +#ifdef WITH_TLS + url += 8; + data.port = 8883; + data.url_scheme = "mqtts"; +#else + ctrl_shell_printf("TLS support not available.\n"); + free(line); + return; +#endif + } else if(!strncasecmp(url, "ws://", 5)) { + url += 5; + data.port = 1883; + data.transport = MOSQ_T_WEBSOCKETS; + data.url_scheme = "ws"; + } else if(!strncasecmp(url, "wss://", 6)) { +#ifdef WITH_TLS + url += 6; + data.port = 8883; + data.transport = MOSQ_T_WEBSOCKETS; + data.url_scheme = "wss"; +#else + ctrl_shell_printf("TLS support not available.\n"); + free(line); + return; +#endif + } + char *hostname_i = strtok_r(url, ":", &saveptr); + char *port_i = strtok_r(NULL, ":", &saveptr); + + if(!hostname_i){ + ctrl_shell_printf("connect mqtt[s]://:port\n"); + free(line); + return; + } + free(data.hostname); + data.hostname = strdup(hostname_i); + if(port_i){ + data.port = atoi(port_i); + } + }else{ + if(data.hostname == NULL){ + data.hostname = strdup("localhost"); + } + if(data.port == PORT_UNDEFINED){ + data.port = 1883; + } + } + + ctrl_shell__connect(); + }else if(!strcasecmp(command, "help")){ + print_help(&saveptr); + }else if(!strcasecmp(command, "exit")){ + data.run = 0; + }else{ + ctrl_shell_printf("Unknown command '%s'\n", command); + } + + free(line); +} + + +void ctrl_shell__pre_connect_cleanup(void) +{ + completion_tree_free(commands_pre_connect); + commands_pre_connect = NULL; +} + + +void ctrl_shell__pre_connect_init(void) +{ + command_tree_create(); + ctrl_shell_completion_commands_set(commands_pre_connect); + ctrl_shell_line_callback_set(line_callback); + snprintf(prompt, sizeof(prompt), "%s>%s ", ANSI_INPUT, ANSI_RESET); +} +#endif diff --git a/apps/mosquitto_ctrl/ctrl_shell_printf.c b/apps/mosquitto_ctrl/ctrl_shell_printf.c new file mode 100644 index 00000000..538e5f61 --- /dev/null +++ b/apps/mosquitto_ctrl/ctrl_shell_printf.c @@ -0,0 +1,39 @@ +/* +Copyright (c) 2025 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include "config.h" + +#include +#include + +#include "ctrl_shell_internal.h" + +void ctrl_shell_vprintf(const char *fmt, va_list va) +{ + char buf[2000]; + vsnprintf(buf, sizeof(buf), fmt, va); + ctrl_shell__output(buf); +} + +void ctrl_shell_printf(const char *fmt, ...) +{ + va_list va; + + va_start(va, fmt); + ctrl_shell_vprintf(fmt, va); + va_end(va); +} diff --git a/apps/mosquitto_ctrl/dynsec.c b/apps/mosquitto_ctrl/dynsec.c index 505dc113..5c6850bc 100644 --- a/apps/mosquitto_ctrl/dynsec.c +++ b/apps/mosquitto_ctrl/dynsec.c @@ -704,7 +704,7 @@ static int dynsec_init(int argc, char *argv[]) fprintf(stderr, "dynsec init: Out of memory.\n"); return MOSQ_ERR_NOMEM; } - json_str = cJSON_Print(tree); + json_str = cJSON_PrintUnformatted(tree); cJSON_Delete(tree); #ifdef WIN32 diff --git a/apps/mosquitto_ctrl/dynsec_client.c b/apps/mosquitto_ctrl/dynsec_client.c index 8a1a9c41..0964fad8 100644 --- a/apps/mosquitto_ctrl/dynsec_client.c +++ b/apps/mosquitto_ctrl/dynsec_client.c @@ -260,7 +260,7 @@ int dynsec_client__file_set_password(int argc, char *argv[], const char *file) cJSON_DeleteItemFromObject(j_client, "encoded_password"); cJSON_AddItemToObject(j_client, "encoded_password", j_encoded_password); - json_str = cJSON_Print(j_tree); + json_str = cJSON_PrintUnformatted(j_tree); cJSON_Delete(j_tree); if(json_str == NULL){ fprintf(stderr, "Error: Out of memory.\n"); diff --git a/apps/mosquitto_ctrl/mosquitto_ctrl.c b/apps/mosquitto_ctrl/mosquitto_ctrl.c index f6c12245..50f2cf5e 100644 --- a/apps/mosquitto_ctrl/mosquitto_ctrl.c +++ b/apps/mosquitto_ctrl/mosquitto_ctrl.c @@ -31,6 +31,7 @@ Contributors: #include "lib_load.h" #include "mosquitto.h" #include "mosquitto_ctrl.h" +#include "ctrl_shell_internal.h" static void print_version(void) { @@ -61,8 +62,12 @@ int main(int argc, char *argv[]) char lib_name[200]; if(argc == 1){ +#ifdef WITH_EDITLINE + ctrl_shell__main(NULL); +#else print_usage(); - return 1; +#endif + return 0; } memset(&ctrl, 0, sizeof(ctrl)); @@ -75,13 +80,22 @@ int main(int argc, char *argv[]) rc = ctrl_config_parse(&ctrl.cfg, &argc, &argv); if(rc){ client_config_cleanup(&ctrl.cfg); + print_usage(); return rc; } - if(argc < 2){ - print_usage(); - client_config_cleanup(&ctrl.cfg); - return 1; +#ifdef WITH_EDITLINE + if(argc == 0){ + ctrl_shell__main(&ctrl.cfg); + return 0; + }else +#endif + { + if(argc < 2){ + print_usage(); + client_config_cleanup(&ctrl.cfg); + return 1; + } } /* In built modules */ diff --git a/apps/mosquitto_ctrl/mosquitto_ctrl.h b/apps/mosquitto_ctrl/mosquitto_ctrl.h index 61729d05..f038f773 100644 --- a/apps/mosquitto_ctrl/mosquitto_ctrl.h +++ b/apps/mosquitto_ctrl/mosquitto_ctrl.h @@ -45,7 +45,6 @@ struct mosq_config { char *username; char *password; char *options_file; -#ifdef WITH_TLS char *cafile; char *capath; char *certfile; @@ -57,20 +56,16 @@ struct mosq_config { char *tls_engine; char *tls_engine_kpass_sha1; char *keyform; -# ifdef FINAL_WITH_TLS_PSK char *psk; char *psk_identity; -# endif -#endif bool verbose; /* sub */ unsigned int timeout; /* sub */ -#ifdef WITH_SOCKS char *socks5_host; int socks5_port; char *socks5_username; char *socks5_password; -#endif char *data_file; + bool no_colour; }; struct mosq_ctrl { diff --git a/apps/mosquitto_ctrl/test/Makefile b/apps/mosquitto_ctrl/test/Makefile new file mode 100644 index 00000000..27ee5a44 --- /dev/null +++ b/apps/mosquitto_ctrl/test/Makefile @@ -0,0 +1,124 @@ +R=../../.. +include ${R}/config.mk + +.PHONY: all check test-compile test test-broker test-lib clean coverage + +LOCAL_CPPFLAGS+= \ + -DWITH_THREADING \ + -DWITH_TLS \ + -I../ \ + -I${R}/include \ + -I${R} \ + -I${R}/lib \ + -I${R}/lib/mock \ + -I${R}/test/mock + +LOCAL_CXXFLAGS+=-std=c++20 -Wall -ggdb -D TEST_SOURCE_DIR='"$(realpath .)"' +LOCAL_LDFLAGS+= +LOCAL_LIBADD+=-lcjson -lgmock -lgtest_main -lgtest ${R}/libcommon/libmosquitto_common.a + +OBJS = \ + ../ctrl_shell_broker.o \ + ../ctrl_shell.o \ + ../ctrl_shell_client.o \ + ../ctrl_shell_completion_tree.o \ + ../ctrl_shell_dynsec.o \ + ../ctrl_shell_ha.o \ + ../ctrl_shell_inspect.o \ + ../ctrl_shell_license.o \ + ../ctrl_shell_post_connect.o \ + ../ctrl_shell_pre_connect.o \ + ../ctrl_shell_printf.o \ + ../ctrl_shell_topictree.o + +LIBMOSQ_MOCKS = \ + ${R}/lib/mock/libmosquitto_mock.o \ + ${R}/lib/mock/actions_publish_mock.o \ + ${R}/lib/mock/actions_subscribe_mock.o \ + ${R}/lib/mock/callbacks_mock.o \ + ${R}/lib/mock/connect_mock.o \ + ${R}/lib/mock/loop_mock.o \ + ${R}/lib/mock/options_mock.o \ + ${R}/lib/mock/thread_mosq_mock.o + +LIB_OBJS = \ + ${OBJS} \ + json_help.o \ + ctrl_shell_mock.o \ + ${R}/test/mock/editline_mock.o \ + ${R}/test/mock/pthread_mock.o \ + ${LIBMOSQ_MOCKS} + +TEST_OBJS = \ + ctrl_shell_broker_test.o \ + ctrl_shell_dynsec_test.o \ + ctrl_shell_help_test.o \ + ctrl_shell_pre_connect_test.o + +ALL_TESTS = \ + ctrl_shell_broker_test \ + ctrl_shell_dynsec_test \ + ctrl_shell_help_test \ + ctrl_shell_pre_connect_test + +all : test-compile + +check : test + +# DEPS + +${LIBMOSQ_MOCKS}: + $(MAKE) -C ${R}/lib + +${OBJS} : + $(MAKE) -C ../ + +json_help.o : ${R}/common/json_help.c ${R}/common/json_help.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(LOCAL_CFLAGS) -c $< -o $@ + +# MOCKS + +ctrl_shell_mock.o : ctrl_shell_mock.cpp ctrl_shell_mock.hpp + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_CXXFLAGS) -c $< -o $@ + +${R}/test/mock/editline_mock.o : ${R}/test/mock/editline_mock.cpp ${R}/test/mock/editline_mock.hpp + $(MAKE) -C ${R}/test/mock test-compile + +${R}/test/mock/pthread_mock.o : ${R}/test/mock/pthread_mock.cpp ${R}/test/mock/pthread_mock.hpp + $(MAKE) -C ${R}/test/mock test-compile + +# TESTS + +${TEST_OBJS} : %.o: %.cpp + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_CXXFLAGS) -c $< -o $@ + +ctrl_shell_broker_test : ctrl_shell_broker_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + +ctrl_shell_dynsec_test : ctrl_shell_dynsec_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + +ctrl_shell_help_test : ctrl_shell_help_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + +ctrl_shell_pre_connect_test : ctrl_shell_pre_connect_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + + +test-compile : $(ALL_TESTS) + +test : test-compile + ./ctrl_shell_help_test + ./ctrl_shell_dynsec_test + +clean : + -rm -rf $(ALL_TESTS) + -rm -rf *.o *.gcda *.gcno coverage.info out/ + +coverage : + lcov --capture --directory . --output-file coverage.info + genhtml coverage.info --output-directory out + +install: + +uninstall: diff --git a/cmake/Findedit.cmake b/cmake/Findedit.cmake new file mode 100644 index 00000000..97c9ef0e --- /dev/null +++ b/cmake/Findedit.cmake @@ -0,0 +1,52 @@ +INCLUDE( FindPackageHandleStandardArgs ) + +# Checks an environment variable; note that the first check +# does not require the usual CMake $-sign. +IF( DEFINED ENV{EDITLINE_DIR} ) + SET( EDITLINE_DIR "$ENV{EDITLINE_DIR}" ) +ENDIF() + +FIND_PATH( + EDITLINE_INCLUDE_DIR + editline/readline.h + HINTS + EDITLINE_DIR +) + +FIND_LIBRARY( EDITLINE_LIBRARY + NAMES edit + HINTS ${EDITLINE_DIR} +) + +FIND_PACKAGE_HANDLE_STANDARD_ARGS( edit DEFAULT_MSG + EDITLINE_INCLUDE_DIR EDITLINE_LIBRARY +) + +SET(EDITLINE_FOUND ${EDIT_FOUND}) + +IF( EDITLINE_FOUND ) + SET( EDITLINE_INCLUDE_DIRS ${EDITLINE_INCLUDE_DIR} ) + SET( EDITLINE_LIBRARIES ${EDITLINE_LIBRARY} ) + + MARK_AS_ADVANCED( + EDITLINE_LIBRARY + EDITLINE_INCLUDE_DIR + EDITLINE_DIR + ) + + add_library(editline SHARED IMPORTED) + set_target_properties(editline + PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${EDITLINE_INCLUDE_DIRS}" + ) + + set_target_properties(editline + PROPERTIES + IMPORTED_LOCATION "${EDITLINE_LIBRARY}" + IMPORTED_IMPLIB "${EDITLINE_LIBRARY}" + ) +ELSE() + SET( EDITLINE_DIR "" CACHE STRING + "An optional hint to a directory for finding the `editline` library" + ) +ENDIF() diff --git a/config.mk b/config.mk index 32d6faf2..c502f92d 100644 --- a/config.mk +++ b/config.mk @@ -152,6 +152,9 @@ WITH_FUZZING=no # Build using clang and with address sanitiser enabled WITH_ASAN=no +# Build with editline support to allow the mosquitto_ctrl shell +WITH_EDITLINE=yes + # ============================================================================= # End of user configuration # ============================================================================= @@ -286,6 +289,7 @@ endif ifeq ($(WITH_COVERAGE),yes) LOCAL_CFLAGS+=-coverage + LOCAL_CXXFLAGS+=-coverage LOCAL_LDFLAGS+=-coverage endif @@ -301,3 +305,7 @@ ifeq ($(WITH_ARGON2),yes) LIB_ARGON2=-largon2 LIBMOSQ_COMMON+=${LIB_ARGON2} endif + +ifeq ($(WITH_EDITLINE),yes) + LOCAL_CPPFLAGS+=-DWITH_EDITLINE +endif diff --git a/fuzzing/broker/Makefile b/fuzzing/broker/Makefile index 48f1b693..ce5165e5 100644 --- a/fuzzing/broker/Makefile +++ b/fuzzing/broker/Makefile @@ -16,6 +16,7 @@ LOCAL_CPPFLAGS+=-I${R}/include/ -I${R}/src -I${R}/lib -I${R} -I${R}/common -I${R -DWITH_MEMORY_TRACKING -DWITH_PERSISTENCE -DWITH_SOCKS -DWITH_SYSTEMD \ -DWITH_SYS_TREE -DWITH_TLS -DWITH_TLS_PSK -DWITH_UNIX_SOCKETS -DWITH_WEBSOCKETS=WS_IS_BUILTIN LOCAL_CXXFLAGS+=-g -Wall -Werror -pthread +LOCAL_CFLAGS+=-g -Wall -Werror -pthread LOCAL_LDFLAGS+= LOCAL_LIBADD+=$(LIB_FUZZING_ENGINE) ${R}/src/mosquitto_broker.a -lssl -lcrypto -lcjson -lm ${R}/libcommon/libmosquitto_common.a -Wl,-Bdynamic -Wl,-Bstatic -largon2 -Wl,-Bdynamic @@ -33,7 +34,7 @@ ${PACKET_FUZZERS} : %: %.cpp fuzz_packet_read_base.o ${R}/src/mosquitto_broker.a install $@ ${OUT}/$@ fuzz_packet_read_base.o : fuzz_packet_read_base.c - $(CC) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) -c -o $@ $< + $(CC) $(LOCAL_CFLAGS) $(LOCAL_CPPFLAGS) -c -o $@ $< broker_fuzz_acl_file : broker_fuzz_acl_file.cpp ${R}/src/mosquitto_broker.a $(CXX) $(LOCAL_CXXFLAGS) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $< $(LOCAL_LIBADD) diff --git a/fuzzing/scripts/oss-fuzz-build.sh b/fuzzing/scripts/oss-fuzz-build.sh index e9376859..7731f32c 100755 --- a/fuzzing/scripts/oss-fuzz-build.sh +++ b/fuzzing/scripts/oss-fuzz-build.sh @@ -27,4 +27,4 @@ make install # Build broker and library static libraries cd ${SRC}/mosquitto -make WITH_STATIC_LIBRARIES=yes WITH_DOCS=no WITH_FUZZING=yes +make WITH_STATIC_LIBRARIES=yes WITH_DOCS=no WITH_FUZZING=yes WITH_EDITLINE=no diff --git a/fuzzing/scripts/oss-fuzz-dependencies.sh b/fuzzing/scripts/oss-fuzz-dependencies.sh index 500e8503..0bbbc62e 100755 --- a/fuzzing/scripts/oss-fuzz-dependencies.sh +++ b/fuzzing/scripts/oss-fuzz-dependencies.sh @@ -20,5 +20,5 @@ # Note that sqlite3 is required as a build dep of a plugin which is not # currently part of fuzz testing. Once it is part of fuzz testing, sqlite will # need to be built statically. -apt-get update && apt-get install -y libargon2-dev libtool-bin make libsqlite3-dev +apt-get update && apt-get install -y libargon2-dev libedit-dev libtool-bin make libsqlite3-dev git clone https://github.com/ralight/cJSON ${SRC}/cJSON diff --git a/lib/connect.c b/lib/connect.c index 8e19cb21..7281e4c8 100644 --- a/lib/connect.c +++ b/lib/connect.c @@ -20,6 +20,13 @@ Contributors: #include +#ifdef WIN32 +# include +# include +#else +# include +#endif + #include "callbacks.h" #include "http_client.h" #include "mosquitto.h" @@ -155,6 +162,33 @@ int mosquitto_reconnect(struct mosquitto *mosq) return mosquitto__reconnect(mosq, true); } +int get_address(int sock, char *buf, size_t len, uint16_t *remote_port) +{ + struct sockaddr_storage addr; + socklen_t addrlen; + + memset(&addr, 0, sizeof(struct sockaddr_storage)); + addrlen = sizeof(addr); + if(!getpeername(sock, (struct sockaddr *)&addr, &addrlen)){ + if(addr.ss_family == AF_INET){ + if(remote_port){ + *remote_port = ntohs(((struct sockaddr_in *)&addr)->sin_port); + } + if(inet_ntop(AF_INET, &((struct sockaddr_in *)&addr)->sin_addr.s_addr, buf, (socklen_t)len)){ + return 0; + } + }else if(addr.ss_family == AF_INET6){ + if(remote_port){ + *remote_port = ntohs(((struct sockaddr_in6 *)&addr)->sin6_port); + } + if(inet_ntop(AF_INET6, &((struct sockaddr_in6 *)&addr)->sin6_addr.s6_addr, buf, (socklen_t)len)){ + return 0; + } + } + } + return 1; +} + static int mosquitto__reconnect(struct mosquitto *mosq, bool blocking) { @@ -207,6 +241,9 @@ static int mosquitto__reconnect(struct mosquitto *mosq, bool blocking) { rc = net__socket_connect(mosq, mosq->host, mosq->port, mosq->bind_address, blocking); } + char address[1024]; + uint16_t port; + get_address(mosq->sock, address, 1024, &port); if(rc>0){ mosquitto__set_state(mosq, mosq_cs_connect_pending); return rc; diff --git a/man/CMakeLists.txt b/man/CMakeLists.txt index 2dff4c53..70f275b8 100644 --- a/man/CMakeLists.txt +++ b/man/CMakeLists.txt @@ -15,6 +15,7 @@ if(XSLTPROC AND NOT WIN32) compile_manpage("mosquitto_ctrl.1") compile_manpage("mosquitto_ctrl_dynsec.1") + compile_manpage("mosquitto_ctrl_shell.1") compile_manpage("mosquitto_passwd.1") compile_manpage("mosquitto_pub.1") compile_manpage("mosquitto_sub.1") @@ -29,6 +30,7 @@ if(XSLTPROC AND NOT WIN32) install(FILES mosquitto_ctrl.1 mosquitto_ctrl_dynsec.1 + mosquitto_ctrl_shell.1 mosquitto_passwd.1 mosquitto_pub.1 mosquitto_sub.1 diff --git a/man/Makefile b/man/Makefile index fccb2f7a..47ce397f 100644 --- a/man/Makefile +++ b/man/Makefile @@ -10,6 +10,7 @@ MANPAGES = \ mosquitto.conf.5 \ mosquitto_ctrl.1 \ mosquitto_ctrl_dynsec.1 \ + mosquitto_ctrl_shell.1 \ mosquitto_passwd.1 \ mosquitto_pub.1 \ mosquitto_rr.1 \ @@ -35,6 +36,7 @@ install : $(INSTALL) -d "${DESTDIR}$(mandir)/man1" $(INSTALL) -m 644 mosquitto_ctrl.1 "${DESTDIR}${mandir}/man1/mosquitto_ctrl.1" $(INSTALL) -m 644 mosquitto_ctrl_dynsec.1 "${DESTDIR}${mandir}/man1/mosquitto_ctrl_dynsec.1" + $(INSTALL) -m 644 mosquitto_ctrl_shell.1 "${DESTDIR}${mandir}/man1/mosquitto_ctrl_shell.1" $(INSTALL) -m 644 mosquitto_passwd.1 "${DESTDIR}${mandir}/man1/mosquitto_passwd.1" $(INSTALL) -m 644 mosquitto_pub.1 "${DESTDIR}${mandir}/man1/mosquitto_pub.1" $(INSTALL) -m 644 mosquitto_sub.1 "${DESTDIR}${mandir}/man1/mosquitto_sub.1" @@ -51,6 +53,7 @@ uninstall : -rm -f "${DESTDIR}${mandir}/man5/mosquitto.conf.5" -rm -f "${DESTDIR}${mandir}/man1/mosquitto_ctrl.1" -rm -f "${DESTDIR}${mandir}/man1/mosquitto_ctrl_dynsec.1" + -rm -f "${DESTDIR}${mandir}/man1/mosquitto_ctrl_shell.1" -rm -f "${DESTDIR}${mandir}/man1/mosquitto_passwd.1" -rm -f "${DESTDIR}${mandir}/man1/mosquitto_pub.1" -rm -f "${DESTDIR}${mandir}/man1/mosquitto_sub.1" @@ -75,6 +78,7 @@ potgen : xml2po -o po/mosquitto.conf/mosquitto.conf.5.pot mosquitto.conf.5.xml xml2po -o po/mosquitto_ctrl/mosquitto_ctrl.1.pot mosquitto_ctrl.1.xml xml2po -o po/mosquitto_ctrl/mosquitto_ctrl_dynsec.1.pot mosquitto_ctrl_dynsec.1.xml + xml2po -o po/mosquitto_ctrl/mosquitto_ctrl_shell.1.pot mosquitto_ctrl_shell.1.xml xml2po -o po/mosquitto_passwd/mosquitto_passwd.1.pot mosquitto_passwd.1.xml xml2po -o po/mosquitto_pub/mosquitto_pub.1.pot mosquitto_pub.1.xml xml2po -o po/mosquitto_sub/mosquitto_sub.1.pot mosquitto_sub.1.xml diff --git a/man/mosquitto_ctrl.1.xml b/man/mosquitto_ctrl.1.xml index 3672c049..6b763b56 100644 --- a/man/mosquitto_ctrl.1.xml +++ b/man/mosquitto_ctrl.1.xml @@ -72,6 +72,11 @@ + + mosquitto_ctrl + connection-options + (interactive shell mode) + mosquitto_ctrl @@ -81,6 +86,12 @@ Description mosquitto_ctrl is a tool for helping configure a Mosquitto broker instance. + It can be run primarily as a straightforward command line tool, as described here, or as an interactive shell as described in + + mosquitto_ctrl_shell + 1 + . The interactive shell makes most operations very straightforward and is recommended for ease of use. + @@ -631,6 +642,12 @@ 7 + + + mosquitto_ctrl_shell + 1 + + mosquitto_rr diff --git a/man/mosquitto_ctrl_shell.1.meta b/man/mosquitto_ctrl_shell.1.meta new file mode 100644 index 00000000..a7e88db8 --- /dev/null +++ b/man/mosquitto_ctrl_shell.1.meta @@ -0,0 +1,5 @@ +.. title: mosquitto_ctrl_shell man page +.. slug: mosquitto_ctrl_shell-1 +.. category: man +.. type: man +.. pretty_url: False diff --git a/man/mosquitto_ctrl_shell.1.xml b/man/mosquitto_ctrl_shell.1.xml new file mode 100644 index 00000000..94a83cea --- /dev/null +++ b/man/mosquitto_ctrl_shell.1.xml @@ -0,0 +1,437 @@ + + + + + + mosquitto_ctrl + 1 + Mosquitto Project + Commands + + + + mosquitto_ctrl_shell + a interactive shell for configuring a Mosquitto broker instance + + + + + mosquitto_ctrl + hostname + port-number + username + password + client-id + file + dir + + file + file + + + + + + Description + + mosquitto_ctrl is a tool for helping configure + a Mosquitto broker instance. + + + This man page describes how to use the interactive shell. For + information on the pure command line mode, see + + mosquitto_ctrl + 1 + . + The interactive shell makes most operations very straightforward + and is recommended for ease of use. + + + To run in interactive mode, run mosquitto_ctrl + with no module or command, i.e. with at most the set of options + described in the synopsis above. The shell will start and you + will be presented with a prompt. + + + + + Encrypted Connections + + mosquitto_ctrl supports TLS encrypted + connections. It is strongly recommended that you use an encrypted + connection for all remote use of mosquitto_ctrl. + + + To enable TLS connections, connect using the + or scheme inside the shell, or use either + or when starting + mosquitto_ctrl on the command line - this also allows custom CA + certificates to be used. + + Client certificates may be used for additional security. To + enable this, use the and + options. + + + + + Example shell workflow + + + The typical workflow for using the interactive shell is to configure + authentication and connect to a broker, then switch to the mode you are + interested in and run its commands. For example, to create a new group + in the dynamic-security plugin: + + + +$ mosquitto_ctrl +mosquitto_ctrl shell v2.1.0 +> auth +username: admin +password: +> connect mqtt://localhost +mqtt://localhost:1883> dynsec +mqtt://localhost:1883|dynsec> createGroup newgroup +OK + + + + + Shell Behaviour + + The interactive shell operates in a set of different modes which + have different commands available. The shell has tab completion for + the commands and their arguments, where possible. It should usually + be possible to press the tab key twice to be able to be shown a list + of the currently available commmands or arguments. + + + The shell has a history of commands that can be accessed by pressing + the up and down arrow keys. The history is not saved to disk. + + + Help can be obtained for any command by typing help + followed by the command name. For example: help createGroup. + + + The different modes are: + + + + Pre-connection + + + In this mode it is possible to set the authentication + details, connect to a broker, read help and exit. + + + + + Post-connection + + + In this mode, mosquitto_ctrl is connected to a broker + and you can switch between different control modules, + view help or exit. + + + + + dynsec + + Allows you to create, delete, and modify users, groups, roles, and ACLs. + + + + broker + + Allows you to view listener and plugin information. + + + + + + + Authentication + + mosquitto_ctrl supports authentication via + username and password, or via x509 client certificates. If you are + using username and password authentication, then you must set the + username and password before connecting. This can be done in one of + two ways. The first is by using the and + options on the command line. The second is by + using the auth command in the shell. + + + Authentication in the shell can done in one of two ways. The first + is by using the auth command, which will then + prompt for a username and password. The second is by using the + auth username command, + which will then prompt for a password. + + + + + Connection + + To connect to a broker, use the connect command. + + + + + Connect to the broker at localhost on port 1883. + > connect + + + Connect to a specific broker using the default port 1883. + > connect mqtt://test.mosquitto.org + + + Connect to a specific broker using the specific port 1884. + > connect mqtt://test.mosquitto.org:1884 + + + Connect to a specific broker using TLS. + > connect mqtts://test.mosquitto.org + + + Connect to a specific broker using websockets. + > connect ws://test.mosquitto.org + + + Connect to a specific broker using websockets over TLS. + > connect wss://test.mosquitto.org + + + + + If the option is used on the command line, the + shell will immediately attempt to connect to the host specified. + + + + + Dynamic-security mode + + Once connected, you can use the command dynsec to switch + to the dynamic-security mode. This will only work if the broker has the + dynamic-security plugin loaded, and you have permission to use it. + + + The dynamic-security mode has commands to create, delete, + and modify users, groups, roles, and ACLs. The commands can be discovered by + pressing the tab key twice. Usernames, group names, and role names can be + auto-completed for the appropriate commands by pressing the tab key. Help + is available for each command by using help followed by + the command name. + + + To leave the dynamic-security mode, use the return command, + or use the exit command to exit the shell. + + + + + Broker mode + + Use the command broker to switch + to the broker mode. This will only work if the broker has the + option set to true. + + + The broker mode has the commands listListeners, to show + currently configured listener configuration, and listPlugins, + to show currently loaded plugins. + + + To leave the broker mode, use the return command, + or use the exit command to exit the shell. + + + + + Connection Options + The options below may be given on the command line + + + + + Define the path to a file containing PEM encoded CA + certificates that are trusted. Used to enable SSL + communication. + See also + + + + + + Define the path to a directory containing PEM encoded CA + certificates that are trusted. Used to enable SSL + communication. + For to work correctly, the + certificate files must have ".crt" as the file ending + and you must run "openssl rehash <path to capath>" each + time you add/remove a certificate. + See also + + + + + + Define the path to a file containing a PEM encoded + certificate for this client, if required by the + server. + See also . + + + + + + Display usage information. + + + + + + + Specify the host to connect to. Defaults to localhost. + + + + + + + + The id to use for this client. If not given, a client id will be + generated depending on the MQTT version being used. For v3.1.1/v3.1, + the client generates a client id in the format + , where the + are replaced with random alphanumeric characters. For v5.0, the + client sends a zero length client id, and the server will generate a + client id for the client. + + + + + + + + Define the path to a file containing a PEM encoded + private key for this client, if required by the + server. + + See also . + + + + + + + + Connect to the port specified. If not given, the + default of 1883 for plain MQTT or 8883 for MQTT over + TLS will be used. + + + + + + + + + Provide a password to be used for authenticating with + the broker. Using this argument without also specifying + a username is invalid when using MQTT v3.1 or v3.1.1. + See also the option. + + + + + + + + + Provide a username to be used for authenticating with + the broker. See also the argument. + + + + + + + + Exit Status + + mosquitto_sub returns zero on success, or non-zero on error. + + + + + Bugs + + mosquitto bug information can be found at + + + + + + See Also + + + + mqtt + 7 + + + + + mosquitto_ctrl + 1 + + + + + mosquitto_rr + 1 + + + + + mosquitto_pub + 1 + + + + + mosquitto_sub + 1 + + + + + mosquitto + 8 + + + + + libmosquitto + 3 + + + + + mosquitto-tls + 7 + + + + + + + Author + Roger Light roger@atchoo.org + + diff --git a/plugins/dynamic-security/test.conf b/plugins/dynamic-security/test.conf index 6e717fdf..4448b317 100644 --- a/plugins/dynamic-security/test.conf +++ b/plugins/dynamic-security/test.conf @@ -1,3 +1,4 @@ listener 1883 plugin ./mosquitto_dynamic_security.so plugin_opt_config_file test.json +enable_control_api true diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 04e5beb0..445b0118 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -1,9 +1,10 @@ +add_subdirectory(mock) + add_subdirectory(apps) add_subdirectory(broker) add_subdirectory(client) add_subdirectory(lib) add_subdirectory(unit) -add_subdirectory(mock) add_custom_target(coverage COMMAND lcov --quiet --capture --directory . --output-file ${PROJECT_BINARY_DIR}/coverage.info --no-external diff --git a/test/apps/Makefile b/test/apps/Makefile index 8f47c17d..b424c422 100644 --- a/test/apps/Makefile +++ b/test/apps/Makefile @@ -9,6 +9,7 @@ all : check : test test-compile: + $(MAKE) -C ctrl test-compile test : $(MAKE) -C db_dump test diff --git a/test/apps/ctrl/CMakeLists.txt b/test/apps/ctrl/CMakeLists.txt index b1919463..415976ed 100644 --- a/test/apps/ctrl/CMakeLists.txt +++ b/test/apps/ctrl/CMakeLists.txt @@ -17,3 +17,76 @@ foreach(PY_TEST_FILE ${PY_TEST_FILES}) ENVIRONMENT "BUILD_ROOT=${CMAKE_BINARY_DIR}" ) endforeach() + + +function(add_ctrl_shell_test TEST_NAME) + if(NOT EDITLINE_FOUND) + return() + endif() + add_executable(${TEST_NAME} + ${TEST_NAME}.cpp + ${mosquitto_SOURCE_DIR}/common/json_help.c + $ + $ + $ + ) + target_compile_definitions(${TEST_NAME} PRIVATE WITH_EDITLINE) + target_include_directories(${TEST_NAME} PRIVATE + ${mosquitto_SOURCE_DIR} + ${mosquitto_SOURCE_DIR}/apps/mosquitto_ctrl + ${mosquitto_SOURCE_DIR}/common + ${mosquitto_SOURCE_DIR}/include + ${mosquitto_SOURCE_DIR}/lib + ${mosquitto_SOURCE_DIR}/test/mock + ${mosquitto_SOURCE_DIR}/test/mock/apps/mosquitto_ctrl + ${mosquitto_SOURCE_DIR}/test/mock/lib + ) + target_link_libraries(${TEST_NAME} PRIVATE + GTest::gmock_main + editline_mock + pthread_mock + cjson + libmosquitto_common + ) + gtest_discover_tests(${TEST_NAME}) +endfunction() + +function(add_ctrl_shell_test_real_editline TEST_NAME) + if(NOT EDITLINE_FOUND) + return() + endif() + add_executable(${TEST_NAME} + ${TEST_NAME}.cpp + ${mosquitto_SOURCE_DIR}/common/json_help.c + $ + $ + $ + ) + target_compile_definitions(${TEST_NAME} PRIVATE WITH_EDITLINE) + target_include_directories(${TEST_NAME} PRIVATE + ${mosquitto_SOURCE_DIR} + ${mosquitto_SOURCE_DIR}/apps/mosquitto_ctrl + ${mosquitto_SOURCE_DIR}/common + ${mosquitto_SOURCE_DIR}/include + ${mosquitto_SOURCE_DIR}/lib + ${mosquitto_SOURCE_DIR}/test/mock + ${mosquitto_SOURCE_DIR}/test/mock/apps/mosquitto_ctrl + ${mosquitto_SOURCE_DIR}/test/mock/lib + ) + target_link_libraries(${TEST_NAME} PRIVATE + "${EDITLINE_LIBRARIES}" + GTest::gmock_main + pthread_mock + cjson + libmosquitto_common + ) + gtest_discover_tests(${TEST_NAME}) +endfunction() + +add_ctrl_shell_test(ctrl_shell_test) +add_ctrl_shell_test(ctrl_shell_broker_test) +add_ctrl_shell_test_real_editline(ctrl_shell_completion_test) +add_ctrl_shell_test(ctrl_shell_dynsec_test) +add_ctrl_shell_test(ctrl_shell_help_test) +add_ctrl_shell_test(ctrl_shell_options_test) +add_ctrl_shell_test(ctrl_shell_pre_connect_test) diff --git a/test/apps/ctrl/Makefile b/test/apps/ctrl/Makefile index 7b15707c..11327974 100644 --- a/test/apps/ctrl/Makefile +++ b/test/apps/ctrl/Makefile @@ -1,19 +1,161 @@ R=../../.. include ${R}/config.mk -.PHONY: all check test ptest clean -.NOTPARALLEL: +.PHONY: all check test-compile test test-mock test-lib clean -all : +LOCAL_CPPFLAGS+= \ + -DWITH_THREADING \ + -DWITH_TLS \ + -I../ \ + -I${R}/include \ + -I${R} \ + -I${R}/apps/mosquitto_ctrl \ + -I${R}/lib \ + -I${R}/test/mock \ + -I${R}/test/mock/apps/mosquitto_ctrl \ + -I${R}/test/mock/lib + +LOCAL_CXXFLAGS+=-std=c++20 -Wall -ggdb -D TEST_SOURCE_DIR='"$(realpath .)"' +LOCAL_LDFLAGS+= +LOCAL_LIBADD+=-lcjson -lgmock -lgtest_main -lgtest ${R}/libcommon/libmosquitto_common.a + +CTRL_OBJS = \ + ${R}/apps/mosquitto_ctrl/ctrl_shell_broker.o \ + ${R}/apps/mosquitto_ctrl/ctrl_shell.o \ + ${R}/apps/mosquitto_ctrl/ctrl_shell_client.o \ + ${R}/apps/mosquitto_ctrl/ctrl_shell_completion_tree.o \ + ${R}/apps/mosquitto_ctrl/ctrl_shell_dynsec.o \ + ${R}/apps/mosquitto_ctrl/ctrl_shell_post_connect.o \ + ${R}/apps/mosquitto_ctrl/ctrl_shell_pre_connect.o \ + ${R}/apps/mosquitto_ctrl/ctrl_shell_printf.o + +LIBMOSQ_MOCKS = \ + ${R}/test/mock/lib/libmosquitto_mock.o \ + ${R}/test/mock/lib/actions_publish_mock.o \ + ${R}/test/mock/lib/actions_subscribe_mock.o \ + ${R}/test/mock/lib/callbacks_mock.o \ + ${R}/test/mock/lib/connect_mock.o \ + ${R}/test/mock/lib/loop_mock.o \ + ${R}/test/mock/lib/options_mock.o \ + ${R}/test/mock/lib/thread_mosq_mock.o + +LIB_OBJS = \ + ${CTRL_OBJS} \ + ${R}/common/json_help.o \ + ${R}/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.o \ + ${R}/test/mock/editline_mock.o \ + ${R}/test/mock/pthread_mock.o \ + ${LIBMOSQ_MOCKS} + +COMPLETION_OBJS = \ + ${CTRL_OBJS} \ + ${R}/common/json_help.o \ + ${R}/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.o \ + ${R}/test/mock/pthread_mock.o \ + ${LIBMOSQ_MOCKS} + +TEST_OBJS = \ + ctrl_shell_test.o \ + ctrl_shell_broker_test.o \ + ctrl_shell_completion_test.o \ + ctrl_shell_dynsec_test.o \ + ctrl_shell_help_test.o \ + ctrl_shell_options_test.o \ + ctrl_shell_pre_connect_test.o + +ifeq ($(WITH_GMOCK),yes) +MOCK_TESTS = \ + ctrl_shell_test \ + ctrl_shell_broker_test \ + ctrl_shell_completion_test \ + ctrl_shell_dynsec_test \ + ctrl_shell_help_test \ + ctrl_shell_options_test \ + ctrl_shell_pre_connect_test +else +MOCK_TESTS = +endif + +all : test-compile check : test -test : +# DEPS + +${LIBMOSQ_MOCKS}: + $(MAKE) -C ${R}/lib + +${CTRL_OBJS} : + $(MAKE) -C ${R}/apps/mosquitto_ctrl + +${R}/common/json_help.o : ${R}/common/json_help.c ${R}/common/json_help.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(LOCAL_CFLAGS) -c $< -o $@ + +# MOCKS + +${R}/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.o : ${R}/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.cpp ${R}/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.hpp + $(MAKE) -C ${R}/test/mock/apps/mosquitto_ctrl test-compile + +${R}/test/mock/editline_mock.o : ${R}/test/mock/editline_mock.cpp ${R}/test/mock/editline_mock.hpp + $(MAKE) -C ${R}/test/mock test-compile + +${R}/test/mock/pthread_mock.o : ${R}/test/mock/pthread_mock.cpp ${R}/test/mock/pthread_mock.hpp + $(MAKE) -C ${R}/test/mock test-compile + +# TESTS + +${TEST_OBJS} : %.o: %.cpp + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_CXXFLAGS) -c $< -o $@ + +ctrl_shell_test : ctrl_shell_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + +ctrl_shell_broker_test : ctrl_shell_broker_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + +ctrl_shell_completion_test : ctrl_shell_completion_test.o ${COMPLETION_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) -ledit + +ctrl_shell_dynsec_test : ctrl_shell_dynsec_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + +ctrl_shell_help_test : ctrl_shell_help_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + +ctrl_shell_options_test : ctrl_shell_options_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + +ctrl_shell_pre_connect_test : ctrl_shell_pre_connect_test.o ${LIB_OBJS} + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_LDFLAGS) -o $@ $^ $(LOCAL_LIBADD) + + +test-compile : $(MOCK_TESTS) + +test-mock : $(MOCK_TESTS) + ./ctrl_shell_broker_test + ./ctrl_shell_completion_test + ./ctrl_shell_dynsec_test + ./ctrl_shell_help_test + ./ctrl_shell_options_test + ./ctrl_shell_pre_connect_test + ./ctrl_shell_test + +test : test-mock ./ctrl-args.py ./ctrl-broker.py ./ctrl-dynsec.py -ptest: +ptest: test-mock ./test.py -clean: +clean : + -rm -rf $(MOCK_TESTS) + -rm -rf *.o *.gcda *.gcno coverage.info out/ + +coverage : + lcov --capture --directory . --output-file coverage.info + genhtml coverage.info --output-directory out + +install: + +uninstall: diff --git a/test/apps/ctrl/ctrl-args.py b/test/apps/ctrl/ctrl-args.py index 4fb06b80..c1ed6af4 100755 --- a/test/apps/ctrl/ctrl-args.py +++ b/test/apps/ctrl/ctrl-args.py @@ -20,30 +20,19 @@ def do_test(args, rc_expected, response=None): env = mosq_test.env_add_ld_library_path() -do_test([], 1) -do_test(["broker"], 1) do_test(["-A"], 1, response="Error: -A argument given but no address specified.\n\n") -do_test(["-A", "127.0.0.1"], 1) # Gives generic help do_test(["--cafile"], 1, response="Error: --cafile argument given but no file specified.\n\n") -do_test(["--cafile", ssl_dir / "all-ca.crt"], 1) # Gives generic help do_test(["--cafile", "missing", "broker", "listListeners"], 1, "Error: Problem setting TLS options: File not found.\n") do_test(["--capath"], 1, response="Error: --capath argument given but no directory specified.\n\n") -do_test(["--capath", ssl_dir], 1) # Gives generic help do_test(["--cert"], 1, response="Error: --cert argument given but no file specified.\n\n") do_test(["--cert", ssl_dir / "client.crt"], 1, response="Error: Both certfile and keyfile must be provided if one of them is set.\n") +do_test(["--help"], 1) # Gives generic help do_test(["--key"], 1, response="Error: --key argument given but no file specified.\n\n") do_test(["--key", ssl_dir / "client.key"], 1, response="Error: Both certfile and keyfile must be provided if one of them is set.\n") do_test(["--ciphers"], 1, response="Error: --ciphers argument given but no ciphers specified.\n\n") -do_test(["--ciphers", "DEFAULT"], 1) # Gives generic help -do_test(["--debug"], 1) # Gives generic help do_test(["-f"], 1, response="Error: -f argument given but no data file specified.\n\n") -do_test(["-f", ssl_dir / "test"], 1) # Gives generic help -do_test(["--help"], 1) # Gives generic help do_test(["--host"], 1, response="Error: -h argument given but no host specified.\n\n") -do_test(["--host", "127.0.0.1"], 1) # Gives generic help do_test(["-i"], 1, response="Error: -i argument given but no id specified.\n\n") -do_test(["-i", "clientid"], 1) # Gives generic help -do_test(["--insecure"], 1) # Gives generic help do_test(["--keyform"], 1, response="Error: --keyform argument given but no keyform specified.\n\n") do_test(["--keyform", "key"], 1, response="Error: If keyform is set, keyfile must be also specified.\n") do_test(["--keyform", "key", "--cafile", "file", "--cert", "file", "--key", "file", "broker", "listListeners"], 1, @@ -52,74 +41,87 @@ do_test(['-L'], 1, response="Error: -L argument given but no URL specified.\n\n" do_test(['-L', 'invalid://'], 1, response="Error: Unsupported URL scheme.\n\n") do_test(['-L', 'mqtt://localhost'], 1, response="Error: Invalid URL for -L argument specified - topic missing.\n") do_test(['-L', 'mqtts://localhost'], 1, response="Error: Invalid URL for -L argument specified - topic missing.\n") -do_test(['-L', 'mqtts://localhost/'], 1) do_test(['-L', 'mqtts://:@localhost/topic'], 1, response="Error: Empty username in URL.\n") do_test(['-L', 'mqtts://localhost:/topic'], 1, response="Error: Empty port in URL.\n") -do_test(['-L', 'mqtts://username:password@localhost:1887/topic'], 1) do_test(["-o"], 1, response="Error: -o argument given but no options file specified.\n\n") -do_test(["-o", "file"], 1) # Gives generic help do_test(["-p"], 1, response="Error: -p argument given but no port specified.\n\n") -do_test(["-p", "1887"], 1) # Gives generic help do_test(["-p", "-1"], 1, response="Error: Invalid port given: -1\n") do_test(["-p", "65536"], 1, response="Error: Invalid port given: 65536\n") do_test(["-P"], 1, response="Error: -P argument given but no password specified.\n\n") -do_test(["-P", "password"], 1) # Gives generic help do_test(["--proxy"], 1, response="Error: --proxy argument given but no proxy url specified.\n\n") do_test(["--proxy", "mqtt://localhost"], 1, response="Error: Unsupported proxy protocol: mqtt://localhost\n") do_test(["--proxy", "socks5h://"], 1, response="Error: Invalid proxy.\n") do_test(["--proxy", "socks5h://localhost:0"], 1, response="Error: Invalid proxy port 0\n") do_test(["--proxy", "socks5h://localhost:65536"], 1, response="Error: Invalid proxy port 65536\n") -do_test(["--proxy", "socks5h://localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://username@localhost@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://username@localhost:localhost:1080"], 1) # Gives generic help -do_test(["--proxy", "socks5h://localhost:1080"], 1) # Gives generic help -do_test(["--proxy", "socks5h://username@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://username:password@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://username:password@localhost:1080"], 1) # Gives generic help -do_test(["--proxy", "socks5h://:"], 1) # Gives generic help -do_test(["--proxy", "socks5h://@"], 1) # Gives generic help do_test(["--proxy", "socks5h://username%@localhost"], 1, response="Error: Invalid URL encoding in username.\n") -do_test(["--proxy", "socks5h://username%25@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://%25username@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://user%3aname@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://user%40name@localhost"], 1) # Gives generic help do_test(["--proxy", "socks5h://username%41@localhost"], 1, response="Error: Invalid URL encoding in username.\n") do_test(["--proxy", "socks5h://username:password%@localhost"], 1, response="Error: Invalid URL encoding in password.\n") -do_test(["--proxy", "socks5h://username:password%25@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://username:%25password@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://username:password%3a@localhost"], 1) # Gives generic help -do_test(["--proxy", "socks5h://username:password%40@localhost"], 1) # Gives generic help do_test(["--proxy", "socks5h://username:password%41@localhost"], 1, response="Error: Invalid URL encoding in password.\n") do_test(["--psk"], 1, response="Error: --psk argument given but no key specified.\n\n") do_test(["--psk", "missing.psk"], 1, response="Error: --psk-identity required if --psk used.\n") do_test(["--psk-identity"], 1, response="Error: --psk-identity argument given but no identity specified.\n\n") do_test(["--cafile", ssl_dir / "all-ca.crt", "--psk", "missing.psk", "--psk-identity", "identity"], 1, response="Error: Only one of --psk or --cafile/--capath may be used at once.\n") do_test(["-q"], 1, response="Error: -q argument given but no QoS specified.\n\n") -do_test(["-q", "1"], 1) # Gives generic help do_test(["-q", "-1"], 1, response="Error: Invalid QoS given: -1\n") do_test(["-q", "3"], 1, response="Error: Invalid QoS given: 3\n") -do_test(["--quiet"], 1) # Gives generic help do_test(["--tls-alpn"], 1, response="Error: --tls-alpn argument given but no protocol specified.\n\n") -do_test(["--tls-alpn", "protocol"], 1) # Gives generic help do_test(["--tls-engine"], 1, response="Error: --tls-engine argument given but no engine_id specified.\n\n") -do_test(["--tls-engine", "engine"], 1) # Gives generic help do_test(["--tls-engine-kpass-sha1"], 1, response="Error: --tls-engine-kpass-sha1 argument given but no kpass sha1 specified.\n\n") -do_test(["--tls-engine-kpass-sha1", "sha1"], 1) # Gives generic help do_test(["--tls-version"], 1, response="Error: --tls-version argument given but no version specified.\n\n") -do_test(["--tls-version", "tlsv1.3"], 1) # Gives generic help do_test(["--username"], 1, response="Error: -u argument given but no username specified.\n\n") -do_test(["--username", "username"], 1) # Gives generic help do_test(["--unix"], 1, response="Error: --unix argument given but no socket path specified.\n\n") -do_test(["--unix", "sock"], 1) # Gives generic help do_test(["-V"], 1, response="Error: --protocol-version argument given but no version specified.\n\n") do_test(["-V", "2"], 1, response="Error: Invalid protocol version argument given.\n\n") -do_test(["-V", "31"], 1) # Gives generic help -do_test(["-V", "311"], 1) # Gives generic help -do_test(["-V", "5"], 1) # Gives generic help do_test(["-V", "6"], 1, response="Error: Invalid protocol version argument given.\n\n") -do_test(["--verbose"], 1) # Gives generic help -do_test(["--version"], 1) # Gives generic help do_test(["--unknown"], 1, response="Error: Unknown option '--unknown'.\n") +do_test(["--version"], 1) # Gives generic help + +# Behaviour with incomplete args is now to run the shell, so these tests don't work +#do_test([], 1) +#do_test(["broker"], 1) +#do_test(["-A", "127.0.0.1"], 1) # Gives generic help +#do_test(["--cafile", ssl_dir / "all-ca.crt"], 1) # Gives generic help +#do_test(["--capath", ssl_dir], 1) # Gives generic help +#do_test(["--ciphers", "DEFAULT"], 1) # Gives generic help +#do_test(["--debug"], 1) # Gives generic help +#do_test(["-f", ssl_dir / "test"], 1) # Gives generic help +#do_test(["--host", "127.0.0.1"], 1) # Gives generic help +#do_test(["-i", "clientid"], 1) # Gives generic help +#do_test(["--insecure"], 1) # Gives generic help +#do_test(['-L', 'mqtts://localhost/'], 1) +#do_test(['-L', 'mqtts://username:password@localhost:1887/topic'], 1) +#do_test(["-o", "file"], 1) # Gives generic help +#do_test(["-p", "1887"], 1) # Gives generic help +#do_test(["-P", "password"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username@localhost@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username@localhost:localhost:1080"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://localhost:1080"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username:password@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username:password@localhost:1080"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://:"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://@"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username%25@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://%25username@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://user%3aname@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://user%40name@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username:password%25@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username:%25password@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username:password%3a@localhost"], 1) # Gives generic help +#do_test(["--proxy", "socks5h://username:password%40@localhost"], 1) # Gives generic help +#do_test(["-q", "1"], 1) # Gives generic help +#do_test(["--quiet"], 1) # Gives generic help +#do_test(["--tls-alpn", "protocol"], 1) # Gives generic help +#do_test(["--tls-engine", "engine"], 1) # Gives generic help +#do_test(["--tls-engine-kpass-sha1", "sha1"], 1) # Gives generic help +#do_test(["--tls-version", "tlsv1.3"], 1) # Gives generic help +#do_test(["--unix", "sock"], 1) # Gives generic help +#do_test(["--username", "username"], 1) # Gives generic help +#do_test(["-V", "31"], 1) # Gives generic help +#do_test(["-V", "311"], 1) # Gives generic help +#do_test(["-V", "5"], 1) # Gives generic help +#do_test(["--verbose"], 1) # Gives generic help # Broker do_test(["broker", "unknown"], 13, response="Command 'unknown' not recognised.\n") diff --git a/test/apps/ctrl/ctrl_shell_broker_test.cpp b/test/apps/ctrl/ctrl_shell_broker_test.cpp new file mode 100644 index 00000000..f3d3e55e --- /dev/null +++ b/test/apps/ctrl/ctrl_shell_broker_test.cpp @@ -0,0 +1,363 @@ +/* +Copyright (c) 2022 Cedalo GmbH +*/ + +// clang-format off +#include "mosquitto_internal.h" // keep this at the top for `#define uthash_free` +#include "ctrl_shell.h" +#include "ctrl_shell_internal.h" +#include "json_help.h" +#include "utlist.h" +// clang-format on +#include +#include + +#include + +#include "ctrl_shell_mock.hpp" +#include "editline_mock.hpp" +#include "libmosquitto_mock.hpp" +#include "pthread_mock.hpp" + +namespace t = testing; + +typedef void (*LIBMOSQ_FUNC_connect_callback)(struct mosquitto *, void *, int); +typedef void (*LIBMOSQ_FUNC_message_callback)(struct mosquitto *, void *, const struct mosquitto_message *); +typedef void (*LIBMOSQ_FUNC_subscribe_callback)(struct mosquitto *, void *, int, int, const int *); +typedef void (*LIBMOSQ_FUNC_publish_v5_callback)(struct mosquitto *, void *, int, int, const mosquitto_property *); + +struct pending_payload{ + struct pending_payload *next, *prev; + char payload[1024]; +}; + +class CtrlShellBrokerTest : public ::t::Test +{ + public: + ::t::StrictMock ctrl_shell_mock_{}; + ::t::StrictMock editline_mock_{}; + ::t::StrictMock libmosquitto_mock_{}; + ::t::StrictMock pthread_mock_{}; + LIBMOSQ_FUNC_connect_callback on_connect{}; + LIBMOSQ_FUNC_message_callback on_message{}; + LIBMOSQ_FUNC_subscribe_callback on_subscribe{}; + LIBMOSQ_FUNC_publish_v5_callback on_publish{}; + struct pending_payload *pending_payloads = nullptr; + + void expect_setup(struct mosq_config *config) + { + editline_mock_.reset(); + EXPECT_CALL(editline_mock_, rl_bind_key(t::Eq('\t'), t::_)); + EXPECT_CALL(editline_mock_, add_history(t::_)).WillRepeatedly(t::Return(0)); + EXPECT_CALL(editline_mock_, clear_history()).Times(t::AnyNumber()); + config->no_colour = true; + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell__output(t::StartsWith("mosquitto_ctrl shell v"))); + } + + void expect_connect(struct mosquitto *mosq, const char *host, int port) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_new(t::Eq(nullptr), t::Eq(true), t::Eq(nullptr))) + .WillOnce(t::Return(mosq)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_int_option(t::Eq(mosq), MOSQ_OPT_PROTOCOL_VERSION, 5)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe_callback_set(t::Eq(mosq), t::A())) + .WillRepeatedly(t::SaveArg<1>(&this->on_subscribe)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish_v5_callback_set(t::Eq(mosq), t::A())) + .WillRepeatedly(t::SaveArg<1>(&this->on_publish)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect(t::Eq(mosq), t::StrEq(host), port, 60)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_start(t::Eq(mosq))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect_callback_set(t::Eq(mosq), t::A())) + .WillRepeatedly(t::SaveArg<1>(&this->on_connect)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_message_callback_set(t::Eq(mosq), t::A())) + .WillOnce(t::SaveArg<1>(&this->on_message)); + } + + void expect_disconnect(struct mosquitto *mosq) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_disconnect(t::Eq(mosq))); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_stop(t::Eq(mosq), false)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_destroy(t::Eq(mosq))); + } + + void expect_outputs(const char **outputs, size_t count) + { + for(size_t i=0; ipayload, sizeof(pp->payload), "%s", respons); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(mosq), nullptr, t::StrEq("$CONTROL/broker/v1"), t::_, + t::StrEq(request), 1, false)) + .WillOnce(t::Invoke([this, pp](){ + DL_APPEND(this->pending_payloads, pp); + return 0; + })); + } + + void expect_request_response_success(struct mosquitto *mosq, const char *request, const char *command) + { + char response[100]; + snprintf(response, sizeof(response), "{\"responses\":[{\"command\":\"%s\"}]}", command); + expect_request_response(mosq, request, response); + } + + void expect_request_response_empty(struct mosquitto *mosq, const char *command) + { + char request[100]; + char response[100]; + + snprintf(request, sizeof(request), "{\"commands\":[{\"command\":\"%s\"}]}", command); + snprintf(response, sizeof(response), "{\"responses\":[{\"command\":\"%s\",\"data\":{}}]}", command); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(mosq), nullptr, t::StrEq("$CONTROL/broker/v1"), t::_, + t::StrEq(request), 1, false)) + .WillOnce(t::Invoke([this, &command](){ + append_empty_response(command); + return 0; + })); + } + + void append_response(const char *response) + { + struct pending_payload *pp = (struct pending_payload *)calloc(1, sizeof(struct pending_payload)); + snprintf(pp->payload, sizeof(pp->payload), "%s", response); + DL_APPEND(this->pending_payloads, pp); + } + + void append_empty_response(const char *command) + { + struct pending_payload *pp = (struct pending_payload *)calloc(1, sizeof(struct pending_payload)); + snprintf(pp->payload, sizeof(pp->payload), + "{\"responses\":[{\"command\":\"%s\",\"data\":{}}]}", command); + DL_APPEND(this->pending_payloads, pp); + } + + void expect_broker(const char *host, int port) + { + char buf[200]; + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("broker"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe(t::_, nullptr, t::StrEq("$CONTROL/broker/v1/response"), 1)) + .WillOnce(t::Return(0)); + } + + void expect_connect_and_messages(struct mosquitto *mosq) + { + /* This is a hacky way of working around the async mqtt send/receive which we don't directly control. + * Each send starts a wait which times out after two seconds. We use that call to produce the effect we want. + */ + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(mosq, nullptr, 0); return 0; })) + .WillRepeatedly(t::Invoke([this, mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + struct pending_payload *pp = pending_payloads; + if(pp){ + DL_DELETE(pending_payloads, pp); + msg.payload = pp->payload; + msg.payloadlen = (int)strlen((char *)msg.payload); + this->on_message(mosq, nullptr, &msg); + free(pp); + } + return 0; + })); + } +}; + + +TEST_F(CtrlShellBrokerTest, SubscribeDenied) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + + char buf[200]; + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("broker"))); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|broker> "))) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe(t::_, nullptr, t::StrEq("$CONTROL/broker/v1/response"), 1)); + + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(&mosq, nullptr, 0); return 0; })) + .WillOnce(t::Invoke([this, &mosq](){ + int granted_qos[1] = {128}; + this->on_subscribe(&mosq, nullptr, 1, 1, granted_qos); + return 0; + })) + .WillRepeatedly(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + struct pending_payload *pp = pending_payloads; + if(pp){ + DL_DELETE(pending_payloads, pp); + msg.payload = pp->payload; + msg.payloadlen = (int)strlen((char *)msg.payload); + this->on_message(&mosq, nullptr, &msg); + free(pp); + } + return 0; + })); + + const char *outputs[] = { + "Subscribe failed, check you have permission to access this module.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellBrokerTest, PublishDenied) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + + char buf[200]; + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("broker"))); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|broker> "))) + .WillOnce(t::Return(strdup("listListeners"))) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe(t::_, nullptr, t::StrEq("$CONTROL/broker/v1/response"), 1)); + + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(&mosq, nullptr, 0); return 0; })) + .WillOnce(t::Invoke([this, &mosq](){ + int granted_qos[1] = {1}; + this->on_subscribe(&mosq, nullptr, 1, 1, granted_qos); + return 0; + })) + .WillOnce(t::Invoke([this, &mosq](){ + this->on_publish(&mosq, nullptr, 1, 128, nullptr); + return 0; + })); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/broker/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listListeners\"}]}"), 1, false)); + + const char *outputs[] = { + "Publish failed, check you have permission to access this module.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellBrokerTest, ListListeners) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_broker(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|broker> "))) + .WillOnce(t::Return(strdup("listListeners"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"listListeners\"}]}"; + const char response[] = "{\"responses\":[{\"command\":\"listListeners\",\"data\":{" + "\"listeners\":[" + "{\"port\":1883,\"protocol\":\"mqtt\",\"tls\":false}" + "]}}]}"; + expect_request_response(&mosq, request, response); + + const char *outputs[] = { + "Listener:", + " Port:", + " 1883\n", + " Protocol:", + " mqtt\n", + " TLS:", + " false\n\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellBrokerTest, ListPlugins) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_broker(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|broker> "))) + .WillOnce(t::Return(strdup("listPlugins"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"listPlugins\"}]}"; + const char response[] = "{\"responses\":[{\"command\":\"listPlugins\",\"data\":{" + "\"plugins\":[" + "{\"name\":\"plugin1\",\"control-endpoints\":[\"$CONTROL/plugin1\"]}" + "]}}]}"; + expect_request_response(&mosq, request, response); + + const char *outputs[] = { + "Plugin:", + " plugin1\n", + "Control endpoints:", + " $CONTROL/plugin1\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} diff --git a/test/apps/ctrl/ctrl_shell_completion_test.cpp b/test/apps/ctrl/ctrl_shell_completion_test.cpp new file mode 100644 index 00000000..2277f177 --- /dev/null +++ b/test/apps/ctrl/ctrl_shell_completion_test.cpp @@ -0,0 +1,103 @@ +/* +Copyright (c) 2022 Cedalo GmbH +*/ + +// clang-format off +#include "mosquitto_internal.h" // keep this at the top for `#define uthash_free` +#include "ctrl_shell.h" +#include "ctrl_shell_internal.h" +#include "json_help.h" +#include "utlist.h" +// clang-format on +#include +#include + +#include + +#include "ctrl_shell_mock.hpp" +#include "libmosquitto_mock.hpp" +#include "pthread_mock.hpp" + +extern "C" { + char **completion_matcher(const char *text, int start, int end); + char *completion_generator(const char *text, int state); + void ctrl_shell__cleanup(void); +} + +namespace t = testing; + +typedef void (*LIBMOSQ_FUNC_connect_callback)(struct mosquitto *, void *, int); +typedef void (*LIBMOSQ_FUNC_message_callback)(struct mosquitto *, void *, const struct mosquitto_message *); + +class CtrlShellCompletionTest : public ::t::Test +{ + public: + ::t::StrictMock ctrl_shell_mock_{}; + ::t::StrictMock libmosquitto_mock_{}; + ::t::StrictMock pthread_mock_{}; + + void expect_setup() + { + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillRepeatedly(t::Return(0)); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe(t::_, t::_, t::_, 1)) + .WillRepeatedly(t::Return(0)); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::_, nullptr, t::_, t::_, t::_, 1, false)) + .WillRepeatedly(t::Return(0)); + + rl_readline_name = "mosquitto_ctrl"; + rl_completion_entry_function = completion_generator; + rl_attempted_completion_function = completion_matcher; + + ctrl_shell__load_module(ctrl_shell__dynsec_init); + } + + void expect_outputs(const char **outputs, size_t count) + { + for(size_t i=0; i +#include + +#include + +#include "ctrl_shell_mock.hpp" +#include "editline_mock.hpp" +#include "libmosquitto_mock.hpp" +#include "pthread_mock.hpp" + +namespace t = testing; + +typedef void (*LIBMOSQ_FUNC_connect_callback)(struct mosquitto *, void *, int); +typedef void (*LIBMOSQ_FUNC_message_callback)(struct mosquitto *, void *, const struct mosquitto_message *); + +struct pending_payload{ + struct pending_payload *next, *prev; + char payload[1024]; +}; + +class CtrlShellDynsecTest : public ::t::Test +{ + public: + ::t::StrictMock ctrl_shell_mock_{}; + ::t::StrictMock editline_mock_{}; + ::t::StrictMock libmosquitto_mock_{}; + ::t::StrictMock pthread_mock_{}; + LIBMOSQ_FUNC_connect_callback on_connect{}; + LIBMOSQ_FUNC_message_callback on_message{}; + struct pending_payload *pending_payloads = nullptr; + + void expect_setup(struct mosq_config *config) + { + editline_mock_.reset(); + EXPECT_CALL(editline_mock_, rl_bind_key(t::Eq('\t'), t::_)); + EXPECT_CALL(editline_mock_, add_history(t::_)).WillRepeatedly(t::Return(0)); + EXPECT_CALL(editline_mock_, clear_history()).Times(t::AnyNumber()); + config->no_colour = true; + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell__output(t::StartsWith("mosquitto_ctrl shell v"))); + } + + void expect_connect(struct mosquitto *mosq, const char *host, int port) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_new(t::Eq(nullptr), t::Eq(true), t::Eq(nullptr))) + .WillOnce(t::Return(mosq)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_int_option(t::Eq(mosq), MOSQ_OPT_PROTOCOL_VERSION, 5)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish_v5_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect(t::Eq(mosq), t::StrEq(host), port, 60)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_start(t::Eq(mosq))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect_callback_set(t::Eq(mosq), t::A())) + .WillRepeatedly(t::SaveArg<1>(&this->on_connect)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_message_callback_set(t::Eq(mosq), t::A())) + .WillOnce(t::SaveArg<1>(&this->on_message)); + } + + void expect_disconnect(struct mosquitto *mosq) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_disconnect(t::Eq(mosq))); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_stop(t::Eq(mosq), false)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_destroy(t::Eq(mosq))); + } + + void expect_outputs(const char **outputs, size_t count) + { + for(size_t i=0; ipayload, sizeof(pp->payload), "%s", respons); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq(request), 1, false)) + .WillOnce(t::Invoke([this, pp](){ + DL_APPEND(this->pending_payloads, pp); + return 0; + })); + } + + void expect_request_response_success(struct mosquitto *mosq, const char *request, const char *command) + { + char response[100]; + snprintf(response, sizeof(response), "{\"responses\":[{\"command\":\"%s\"}]}", command); + expect_request_response(mosq, request, response); + } + + void expect_request_response_empty(struct mosquitto *mosq, const char *command) + { + char request[100]; + char response[100]; + + snprintf(request, sizeof(request), "{\"commands\":[{\"command\":\"%s\"}]}", command); + snprintf(response, sizeof(response), "{\"responses\":[{\"command\":\"%s\",\"data\":{}}]}", command); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq(request), 1, false)) + .WillOnce(t::Invoke([this, command](){ + append_empty_response(command); + return 0; + })); + } + + void append_response(const char *response) + { + struct pending_payload *pp = (struct pending_payload *)calloc(1, sizeof(struct pending_payload)); + snprintf(pp->payload, sizeof(pp->payload), "%s", response); + DL_APPEND(this->pending_payloads, pp); + } + + void append_empty_response(const char *command) + { + struct pending_payload *pp = (struct pending_payload *)calloc(1, sizeof(struct pending_payload)); + snprintf(pp->payload, sizeof(pp->payload), + "{\"responses\":[{\"command\":\"%s\",\"data\":{}}]}", command); + DL_APPEND(this->pending_payloads, pp); + } + + void expect_single_lists(struct mosquitto *mosq) + { + expect_request_response_empty(mosq, "listClients"); + expect_request_response_empty(mosq, "listGroups"); + expect_request_response_empty(mosq, "listRoles"); + } + + void expect_dynsec(const char *host, int port) + { + char buf[200]; + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("dynsec"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe(t::_, nullptr, t::StrEq("$CONTROL/dynamic-security/v1/response"), 1)) + .WillOnce(t::Return(0)); + } + + void expect_connect_and_messages(struct mosquitto *mosq) + { + /* This is a hacky way of working around the async mqtt send/receive which we don't directly control. + * Each send starts a wait which times out after two seconds. We use that call to produce the effect we want. + */ + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(mosq, nullptr, 0); return 0; })) + .WillRepeatedly(t::Invoke([this, mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + struct pending_payload *pp = this->pending_payloads; + if(pp){ + DL_DELETE(pending_payloads, pp); + msg.payload = pp->payload; + msg.payloadlen = (int)strlen((char *)msg.payload); + this->on_message(mosq, nullptr, &msg); + free(pp); + } + return 0; + })); + } + + void expect_generic_arg1(const char *command, const char *itemlabel, const char *itemvalue) + { + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char line[200]; + char payload[500]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + snprintf(line, sizeof(line), "%s %s", command, itemvalue); + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup(line))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + snprintf(payload, sizeof(payload), + "{\"commands\":[{\"command\":\"%s\",\"%s\":\"%s\"}]}", + command, itemlabel, itemvalue); + expect_request_response_success(&mosq, payload, command); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + } + + void expect_generic_arg1_with_list_update(const char *command, const char *itemlabel, const char *itemvalue, const char *listcmd) + { + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char line[200]; + char request[500]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + snprintf(line, sizeof(line), "%s %s", command, itemvalue); + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup(line))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + snprintf(request, sizeof(request), + "{\"commands\":[{\"command\":\"%s\",\"%s\":\"%s\"}]}", + command, itemlabel, itemvalue); + expect_request_response_success(&mosq, request, command); + + expect_request_response_empty(&mosq, listcmd); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + } + + void expect_generic_arg1_with_error(const char *command, const char *itemlabel) + { + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char line[200]; + char error[200]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + snprintf(line, sizeof(line), "%s", command); + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup(line))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + snprintf(error, sizeof(error), "%s %s\n", command, itemlabel); + const char *outputs[] = { + error, + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + } + + void expect_generic_arg2(const char *command, const char *itemlabel1, const char *itemvalue1, const char *itemlabel2, const char *itemvalue2) + { + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char line[200]; + char payload[500]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + snprintf(line, sizeof(line), "%s %s %s", command, itemvalue1, itemvalue2); + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup(line))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + snprintf(payload, sizeof(payload), + "{\"commands\":[{\"command\":\"%s\"," + "\"%s\":\"%s\"," + "\"%s\":\"%s\"" + "}]}", + command, itemlabel1, itemvalue1, itemlabel2, itemvalue2); + expect_request_response_success(&mosq, payload, command); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + } + + void expect_send_set_acl_default_access(const char *acltype) + { + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char buf[500]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + snprintf(buf, sizeof(buf), "setDefaultACLAccess %s allow", acltype); + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup(buf))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + snprintf(buf, sizeof(buf), + "{\"commands\":[{\"command\":\"setDefaultACLAccess\"," + "\"acls\":[" + "{" + "\"acltype\":\"%s\"," + "\"allow\":true}]}]}", + acltype); + expect_request_response_success(&mosq, buf, "setDefaultACLAccess"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + } +}; + + +TEST_F(CtrlShellDynsecTest, NoDynsec) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(&mosq, nullptr, 0); return 0; })) + .WillOnce(t::Return(0)) // Subscribe + .WillOnce(t::Return(ETIMEDOUT)); // First message to dynsec fails + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\"}]}"), 1, false)) + .WillOnce(t::Return(0)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(nullptr)); + + const char *outputs[] = { + "Timed out with no response.\n", + "Check the dynsec module is configured on the broker.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, CreateClient) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("createClient"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "createClient username [password [clientid]]\n", + "createClient username password [clientid]\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, CreateClientWithPassword) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("createClient username password"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_empty_response("listClients"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_empty_response("listClients"); + return 0; + })); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + expect_request_response_success(&mosq, + "{\"commands\":[{\"command\":\"createClient\",\"username\":\"username\",\"password\":\"password\"}]}", + "createClient"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, CreateClientWithPasswordAndClientid) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("createClient username1 password1 clientid1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_empty_response("listClients"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_empty_response("listClients"); + return 0; + })); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + expect_request_response_success(&mosq, + "{\"commands\":[{\"command\":\"createClient\",\"username\":\"username1\",\"password\":\"password1\",\"clientid\":\"clientid1\"}]}", + "createClient"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, CreateClientPasswordCliMatching) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("createClient username1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_empty_response("listClients"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_empty_response("listClients"); + return 0; + })); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "password1"); + return s; + })) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "password1"); + return s; + })); + + expect_request_response_success(&mosq, + "{\"commands\":[{\"command\":\"createClient\",\"username\":\"username1\",\"password\":\"password1\"}]}", + "createClient"); + + const char *outputs[] = { + "password:", + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, CreateClientPasswordCliNotMatching) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("createClient username"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "mypassword"); + return s; + })) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "nomatch"); + return s; + })); + + const char *outputs[] = { + "password:", + "Passwords do not match.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, CreateClientPasswordCliOneOnly) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("createClient username"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "mypassword"); + return s; + })) + .WillOnce(t::Return(nullptr)); + + const char *outputs[] = { + "password:", + "No password.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, EnableClient) +{ + expect_generic_arg1("enableClient", "username", "username1"); +} + +TEST_F(CtrlShellDynsecTest, EnableClientMissing) +{ + expect_generic_arg1_with_error("enableClient", "username"); +} + +/* FIXME - Not found cases */ + +TEST_F(CtrlShellDynsecTest, DisableClient) +{ + expect_generic_arg1("disableClient", "username", "username1"); +} + +TEST_F(CtrlShellDynsecTest, DisableClientMissing) +{ + expect_generic_arg1_with_error("disableClient", "username"); +} + +TEST_F(CtrlShellDynsecTest, SetAnonGroup) +{ + expect_generic_arg1("setAnonymousGroup", "groupname", "groupname1"); +} + +TEST_F(CtrlShellDynsecTest, SetAnonGroupMissing) +{ + expect_generic_arg1_with_error("setAnonymousGroup", "groupname"); +} + + +TEST_F(CtrlShellDynsecTest, AddClientRole) +{ + expect_generic_arg2("addClientRole", "username", "username1", "rolename", "rolename1"); +} + + +TEST_F(CtrlShellDynsecTest, AddGroupClient) +{ + expect_generic_arg2("addGroupClient", "groupname", "groupname1", "username", "username1"); +} + + +TEST_F(CtrlShellDynsecTest, AddGroupRole) +{ + expect_generic_arg2("addGroupRole", "groupname", "groupname1", "rolename", "rolename1"); +} + + +TEST_F(CtrlShellDynsecTest, RemoveClientRole) +{ + expect_generic_arg2("removeClientRole", "username", "username1", "rolename", "rolename1"); +} + + +TEST_F(CtrlShellDynsecTest, RemoveGroupClient) +{ + expect_generic_arg2("removeGroupClient", "groupname", "groupname1", "username", "username1"); +} + + +TEST_F(CtrlShellDynsecTest, RemoveGroupRole) +{ + expect_generic_arg2("removeGroupRole", "groupname", "groupname1", "rolename", "rolename1"); +} + + +TEST_F(CtrlShellDynsecTest, SetClientId) +{ + expect_generic_arg2("setClientId", "username", "username1", "clientid", "clientid1"); +} + +TEST_F(CtrlShellDynsecTest, SetDefaultACLAccessPublishClientReceive) +{ + expect_send_set_acl_default_access("publishClientReceive"); +} + + +TEST_F(CtrlShellDynsecTest, SetDefaultACLAccessPublishClientSend) +{ + expect_send_set_acl_default_access("publishClientSend"); +} + + +TEST_F(CtrlShellDynsecTest, SetDefaultACLAccessSubscribe) +{ + expect_send_set_acl_default_access("subscribe"); +} + + +TEST_F(CtrlShellDynsecTest, SetDefaultACLAccessUnsubscribe) +{ + expect_send_set_acl_default_access("unsubscribe"); +} + + +TEST_F(CtrlShellDynsecTest, SetDefaultACLAccessBadType) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("setDefaultACLAccess badtype allow"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "setDefaultACLAccess acltype allow|deny\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, SetDefaultACLAccessBadAllow) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("setDefaultACLAccess subscribe bad"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "setDefaultACLAccess acltype allow|deny\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, SetDefaultACLAccessNoAllow) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("setDefaultACLAccess publishClientSend"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "setDefaultACLAccess acltype allow|deny\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + +TEST_F(CtrlShellDynsecTest, GetDefaultACLAccess) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("getDefaultACLAccess"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"getDefaultACLAccess\"}]}"; + const char response[] = + "{\"responses\":[{\"command\":\"getDefaultACLAccess\",\"data\":{" + "\"acls\":[" + "{\"acltype\":\"publishClientSend\",\"allow\":false}," + "{\"acltype\":\"publishClientReceive\",\"allow\":true}," + "{\"acltype\":\"subscribe\",\"allow\":false}," + "{\"acltype\":\"unsubscribe\",\"allow\":true}" + "]}}]}"; + + expect_request_response(&mosq, request, response); + + const char *outputs[] = { + "publishClientSend deny\n", + "publishClientReceive allow\n", + "subscribe deny\n", + "unsubscribe allow\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, GetAnonymousGroup) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("getAnonymousGroup"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"getAnonymousGroup\"}]}"; + const char response[] = + "{\"responses\":[{\"command\":\"getAnonymousGroup\",\"data\":{" + "\"group\":{\"groupname\":\"group1\"}" + "}}]}"; + expect_request_response(&mosq, request, response); + + const char *outputs[] = { + "group1\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, GetClient) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("getClient username1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"getClient\",\"username\":\"username1\"}]}"; + const char response[] = + "{\"responses\":[{\"command\":\"getClient\",\"data\":{" + "\"client\":{" + "\"username\":\"username1\"," + "\"clientid\":\"clientid1\"," + "\"disabled\":true," + "\"textname\":\"textname1\"," + "\"textdescription\":\"textdescription1\"," + "\"roles\":[" + "{\"rolename\":\"role1\",\"priority\":1}" + "]," + "\"groups\":[" + "{\"groupname\":\"group1\",\"priority\":2}" + "]}}}]}"; + expect_request_response(&mosq, request, response); + + const char *outputs[] = { + "Username:", + " username1\n", + "Clientid:", + " clientid1\n", + "Text name:", + " textname1\n", + "Text description:", + " textdescription1\n", + "Disabled:", + " true\n", + "Roles:", + " role1", + "Groups:", + " group1", + " (priority: ", + "1", + "2", + ")", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, GetGroup) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("getGroup groupname1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"getGroup\",\"groupname\":\"groupname1\"}]}"; + const char response[] = + "{\"responses\":[{\"command\":\"getGroup\",\"data\":{" + "\"group\":{" + "\"groupname\":\"groupname1\"," + "\"textname\":\"textname1\"," + "\"textdescription\":\"textdescription1\"," + "\"roles\":[" + "{\"rolename\":\"role1\",\"priority\":1}" + "]," + "\"clients\":[" + "{\"username\":\"username1\",\"priority\":2}" + "]}}}]}"; + expect_request_response(&mosq, request, response); + + const char *outputs[] = { + "Group name:", + " groupname1\n", + "Text name:", + " textname1\n", + "Text description:", + " textdescription1\n", + "Roles:", + " role1", + "Clients:", + " username1", + " (priority: ", + "1", + ")", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellDynsecTest, GetRole) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("getRole rolename1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"getRole\",\"rolename\":\"rolename1\"}]}"; + const char response[] = + "{\"responses\":[{\"command\":\"getRole\",\"data\":{" + "\"role\":{" + "\"rolename\":\"rolename1\"," + "\"textname\":\"textname1\"," + "\"textdescription\":\"textdescription1\"," + "\"allowwildcardsubs\":true," + "\"acls\":[" + "{\"acltype\":\"subscribeLiteral\",\"topic\":\"topic1\",\"allow\":true,\"priority\":1}" + "]}}}]}"; + expect_request_response(&mosq, request, response); + + const char *outputs[] = { + "Role name:", + " rolename1\n", + "Text name:", + " textname1\n", + "Text description:", + " textdescription1\n", + "Allow wildcard subscriptions:", + " true\n", + "ACLs:", + " subscribeLiteral allow topic1 (priority 1)\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, SetClientPasswordCliMatching) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("setClientPassword username1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "password1"); + return s; + })) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "password1"); + return s; + })); + + expect_request_response_success(&mosq, + "{\"commands\":[{\"command\":\"setClientPassword\",\"username\":\"username1\",\"password\":\"password1\"}]}", + "setClientPassword"); + + const char *outputs[] = { + "password:", + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, SetClientPasswordCliNotMatching) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("setClientPassword username1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "password1"); + return s; + })) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "password2"); + return s; + })); + + const char *outputs[] = { + "password:", + "Passwords do not match.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + +TEST_F(CtrlShellDynsecTest, SetClientPasswordCliNoPassword) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("setClientPassword username1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Invoke([](char *, int , FILE *){ + return nullptr; + })); + + const char *outputs[] = { + "password:", + "No password.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + +TEST_F(CtrlShellDynsecTest, SetClientPasswordCliNoUsername) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("setClientPassword"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "setClientPassword [password]\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, AddRoleACLPublishClientReceive) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 publishClientReceive allow topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"addRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"publishClientReceive\"," + "\"topic\":\"topic1\"," + "\"allow\":true" + "}]}"; + expect_request_response_success(&mosq, request, "addRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, AddRoleACLPublishClientSend) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 publishClientSend allow topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"addRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"publishClientSend\"," + "\"topic\":\"topic1\"," + "\"allow\":true" + "}]}"; + expect_request_response_success(&mosq, request, "addRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, AddRoleACLSubscribeLiteral) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 subscribeLiteral allow topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"addRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"subscribeLiteral\"," + "\"topic\":\"topic1\"," + "\"allow\":true" + "}]}"; + expect_request_response_success(&mosq, request, "addRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, AddRoleACLSubscribePattern) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 subscribePattern allow topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"addRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"subscribePattern\"," + "\"topic\":\"topic1\"," + "\"allow\":true" + "}]}"; + expect_request_response_success(&mosq, request, "addRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + + + +TEST_F(CtrlShellDynsecTest, AddRoleACLUnsubscribeLiteral) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 unsubscribeLiteral allow topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"addRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"unsubscribeLiteral\"," + "\"topic\":\"topic1\"," + "\"allow\":true" + "}]}"; + expect_request_response_success(&mosq, request, "addRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, AddRoleACLUnsubscribePattern) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 unsubscribePattern deny topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"addRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"unsubscribePattern\"," + "\"topic\":\"topic1\"," + "\"allow\":false" + "}]}"; + expect_request_response_success(&mosq, request, "addRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, AddRoleACLBadAllow) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 unsubscribePattern bad topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "Invalid allow/deny 'bad'\n", + "addRoleACL rolename acltype allow|deny topic\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, AddRoleACLBadACLType) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 bad allow topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "Invalid acltype 'bad'\n", + "addRoleACL rolename acltype allow|deny topic\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, AddRoleACLNoTopic) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("addRoleACL rolename1 subscribeLiteral allow"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "addRoleACL rolename acltype allow|deny topic\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, RemoveRoleACLPublishClientReceive) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("removeRoleACL rolename1 publishClientReceive topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"removeRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"publishClientReceive\"," + "\"topic\":\"topic1\"" + "}]}"; + expect_request_response_success(&mosq, request, "removeRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, RemoveRoleACLPublishClientSend) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("removeRoleACL rolename1 publishClientSend topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"removeRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"publishClientSend\"," + "\"topic\":\"topic1\"" + "}]}"; + expect_request_response_success(&mosq, request, "removeRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, RemoveRoleACLUnsubscribeLiteral) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("removeRoleACL rolename1 unsubscribeLiteral topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"removeRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"unsubscribeLiteral\"," + "\"topic\":\"topic1\"" + "}]}"; + expect_request_response_success(&mosq, request, "removeRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, RemoveRoleACLUnsubscribePattern) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("removeRoleACL rolename1 unsubscribePattern topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"removeRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"unsubscribePattern\"," + "\"topic\":\"topic1\"" + "}]}"; + expect_request_response_success(&mosq, request, "removeRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, RemoveRoleACLSubscribeLiteral) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("removeRoleACL rolename1 subscribeLiteral topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"removeRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"subscribeLiteral\"," + "\"topic\":\"topic1\"" + "}]}"; + expect_request_response_success(&mosq, request, "removeRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, RemoveRoleACLSubscribePattern) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("removeRoleACL rolename1 subscribePattern topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"removeRoleACL\"," + "\"rolename\":\"rolename1\"," + "\"acltype\":\"subscribePattern\"," + "\"topic\":\"topic1\"" + "}]}"; + expect_request_response_success(&mosq, request, "removeRoleACL"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, RemoveRoleACLBadACLType) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("removeRoleACL rolename1 bad topic1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "Invalid acltype 'bad'\n", + "removeRoleACL rolename acltype topic\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, RemoveRoleACLNoTopic) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("removeRoleACL rolename1 subscribeLiteral "))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "removeRoleACL rolename acltype topic\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + +TEST_F(CtrlShellDynsecTest, CreateGroup) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listGroups\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_empty_response("listGroups"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_empty_response("listGroups"); + return 0; + })); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("createGroup group1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"createGroup\",\"groupname\":\"group1\"}]}"; + expect_request_response_success(&mosq, request, "createGroup"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + +TEST_F(CtrlShellDynsecTest, CreateRole) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listRoles\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_empty_response("listRoles"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_empty_response("listRoles"); + return 0; + })); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("createRole role1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"createRole\",\"rolename\":\"role1\"}]}"; + expect_request_response_success(&mosq, request, "createRole"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + +TEST_F(CtrlShellDynsecTest, DeleteClient) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_empty_response("listClients"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_empty_response("listClients"); + return 0; + })); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("deleteClient user1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"deleteClient\",\"username\":\"user1\"}]}"; + expect_request_response_success(&mosq, request, "deleteClient"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, DeleteGroup) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listGroups\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_empty_response("listGroups"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_empty_response("listGroups"); + return 0; + })); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("deleteGroup group1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"deleteGroup\",\"groupname\":\"group1\"}]}"; + expect_request_response_success(&mosq, request, "deleteGroup"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + +TEST_F(CtrlShellDynsecTest, DeleteRole) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listRoles\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_empty_response("listRoles"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_empty_response("listRoles"); + return 0; + })); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("deleteRole role1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + + const char request[] = "{\"commands\":[{\"command\":\"deleteRole\",\"rolename\":\"role1\"}]}"; + expect_request_response_success(&mosq, request, "deleteRole"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyClientTextName) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyClient user1 textName new name"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"modifyClient\"," + "\"username\":\"user1\"," + "\"textName\":\"new name\"" + "}]}"; + expect_request_response_success(&mosq, request, "modifyClient"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyClientTextDescription) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyClient user1 textDescription new description"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"modifyClient\"," + "\"username\":\"user1\"," + "\"textDescription\":\"new description\"" + "}]}"; + expect_request_response_success(&mosq, request, "modifyClient"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyGroupTextName) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyGroup group1 textName new name"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"modifyGroup\"," + "\"groupname\":\"group1\"," + "\"textName\":\"new name\"" + "}]}"; + expect_request_response_success(&mosq, request, "modifyGroup"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyGroupTextDescription) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyGroup group1 textDescription new description"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"modifyGroup\"," + "\"groupname\":\"group1\"," + "\"textDescription\":\"new description\"" + "}]}"; + expect_request_response_success(&mosq, request, "modifyGroup"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyRoleTextName) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyRole role1 textName new name"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"modifyRole\"," + "\"rolename\":\"role1\"," + "\"textName\":\"new name\"" + "}]}"; + expect_request_response_success(&mosq, request, "modifyRole"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyRoleTextDescription) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyRole role1 textDescription new description"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"modifyRole\"," + "\"rolename\":\"role1\"," + "\"textDescription\":\"new description\"" + "}]}"; + expect_request_response_success(&mosq, request, "modifyRole"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyRoleWildcardSubs) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyRole role1 allowWildcardSubs true"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char request[] = "{\"commands\":[{" + "\"command\":\"modifyRole\"," + "\"rolename\":\"role1\"," + "\"allowWildcardSubs\":true" + "}]}"; + expect_request_response_success(&mosq, request, "modifyRole"); + + const char *outputs[] = { + "OK\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyRoleAllowWildcardSubsBadValue) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyRole role1 allowWildcardSubs bad"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "modifyRole rolename \n", + "Invalid value 'bad'\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyRoleBadProp) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyRole role1 badprop new description"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "modifyRole rolename \n", + "Unknown property 'badprop'\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ModifyRoleNoProp) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("modifyRole role1"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_single_lists(&mosq); + + const char *outputs[] = { + "modifyRole rolename \n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListClients) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listClients"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listClients\",\"data\":{" + "\"clients\":[\"client1\",\"client2\"]" + "}}]}"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listClients\",\"data\":{" + "\"clients\":[\"client1\",\"client2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "client1\n", + "client2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListClientsWithCount) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listClients 2"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\",\"count\":2}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listClients\",\"data\":{" + "\"clients\":[\"client1\",\"client2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "client1\n", + "client2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListClientsWithCountAndOffset) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listClients 2 3"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\",\"count\":2,\"offset\":3}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listClients\",\"data\":{" + "\"clients\":[\"client1\",\"client2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "client1\n", + "client2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListGroups) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listGroups"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listGroups\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listGroups\",\"data\":{" + "\"groups\":[\"group1\",\"group2\"]" + "}}]}"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listGroups\",\"data\":{" + "\"groups\":[\"group1\",\"group2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "group1\n", + "group2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListGroupsWithCount) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listGroups 2"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listGroups\",\"count\":2}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listGroups\",\"data\":{" + "\"groups\":[\"group1\",\"group2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "group1\n", + "group2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListGroupsWithCountAndOffset) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listGroups 2 3"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listGroups\",\"count\":2,\"offset\":3}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listGroups\",\"data\":{" + "\"groups\":[\"group1\",\"group2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "group1\n", + "group2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListRoles) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listRoles"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listRoles\"}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listRoles\",\"data\":{" + "\"roles\":[\"role1\",\"role2\"]" + "}}]}"); + return 0; + })) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listRoles\",\"data\":{" + "\"roles\":[\"role1\",\"role2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "role1\n", + "role2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListRolesWithCount) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listRoles 2"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listRoles\",\"count\":2}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listRoles\",\"data\":{" + "\"roles\":[\"role1\",\"role2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "role1\n", + "role2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} + + +TEST_F(CtrlShellDynsecTest, ListRolesWithCountAndOffset) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_dynsec(host, port); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("listRoles 2 3"))) + .WillOnce(t::Return(strdup("exit"))); + + expect_connect_and_messages(&mosq); + expect_request_response_empty(&mosq, "listClients"); + expect_request_response_empty(&mosq, "listGroups"); + expect_request_response_empty(&mosq, "listRoles"); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listRoles\",\"count\":2,\"offset\":3}]}"), 1, false)) + .WillOnce(t::Invoke([this](){ + append_response("{\"responses\":[{\"command\":\"listRoles\",\"data\":{" + "\"roles\":[\"role1\",\"role2\"]" + "}}]}"); + return 0; + })); + + + const char *outputs[] = { + "role1\n", + "role2\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + EXPECT_EQ(pending_payloads, nullptr); +} diff --git a/test/apps/ctrl/ctrl_shell_help_test.cpp b/test/apps/ctrl/ctrl_shell_help_test.cpp new file mode 100644 index 00000000..dda7b898 --- /dev/null +++ b/test/apps/ctrl/ctrl_shell_help_test.cpp @@ -0,0 +1,533 @@ +/* +Copyright (c) 2022 Cedalo GmbH +*/ + +// clang-format off +#include "mosquitto_internal.h" // keep this at the top for `#define uthash_free` +#include "ctrl_shell.h" +#include "ctrl_shell_internal.h" +#include "json_help.h" +// clang-format on +#include +#include + +#include + +#include "ctrl_shell_mock.hpp" +#include "editline_mock.hpp" +#include "libmosquitto_mock.hpp" +#include "pthread_mock.hpp" + +namespace t = testing; + +typedef void (*LIBMOSQ_FUNC_connect_callback)(struct mosquitto *, void *, int); +typedef void (*LIBMOSQ_FUNC_message_callback)(struct mosquitto *, void *, const struct mosquitto_message *); + +class CtrlShellHelpTest : public ::t::Test +{ + public: + ::t::StrictMock ctrl_shell_mock_{}; + ::t::StrictMock editline_mock_{}; + ::t::StrictMock libmosquitto_mock_{}; + ::t::StrictMock pthread_mock_{}; + LIBMOSQ_FUNC_connect_callback on_connect{}; + LIBMOSQ_FUNC_message_callback on_message{}; + + void expect_setup(struct mosq_config *config) + { + editline_mock_.reset(); + EXPECT_CALL(editline_mock_, rl_bind_key(t::Eq('\t'), t::_)); + EXPECT_CALL(editline_mock_, add_history(t::_)).WillRepeatedly(t::Return(0)); + EXPECT_CALL(editline_mock_, clear_history()).Times(t::AnyNumber()); + config->no_colour = true; + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell__output(t::StartsWith("mosquitto_ctrl shell v"))); + } + + void expect_connect(struct mosquitto *mosq, const char *host, int port) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_new(t::Eq(nullptr), t::Eq(true), t::Eq(nullptr))) + .WillOnce(t::Return(mosq)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_int_option(t::Eq(mosq), MOSQ_OPT_PROTOCOL_VERSION, 5)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish_v5_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect(t::Eq(mosq), t::StrEq(host), port, 60)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_start(t::Eq(mosq))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect_callback_set(t::Eq(mosq), t::A())) + .WillRepeatedly(t::SaveArg<1>(&this->on_connect)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_message_callback_set(t::Eq(mosq), t::A())) + .WillOnce(t::SaveArg<1>(&this->on_message)); + } + + void expect_disconnect(struct mosquitto *mosq) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_disconnect(t::Eq(mosq))); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_stop(t::Eq(mosq), false)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_destroy(t::Eq(mosq))); + } + + void expect_outputs(const char **outputs, size_t count) + { + for(size_t i=0; i "))) + .WillOnce(t::Return(strdup("help"))) + .WillOnce(t::Return(strdup("help auth"))) + .WillOnce(t::Return(strdup("help connect"))) + .WillOnce(t::Return(strdup("help exit"))) + .WillOnce(t::Return(strdup("help help"))) + .WillOnce(t::Return(strdup("help unknown"))) + .WillOnce(t::Return(strdup("unknown"))) + .WillOnce(t::Return(nullptr)); + + const char *outputs[] = { + "This is the mosquitto_ctrl interactive shell, for controlling aspects of a mosquitto broker.\n", + "Find help on a command using 'help '\n", + "Press tab multiple times to find currently available commands.\n\n", + "\n", + "Example workflow:\n\n", + "> auth\n", + "username: admin\n", + "password:\n", + "> connect mqtt://localhost\n", + "mqtt://localhost:1883> dynsec\n", + "mqtt://localhost:1883|dynsec> createGroup newgroup\n", + "OK\n\n", + + "auth [username]\n", + "\nSet a username and password prior to connecting to a broker.\n", /* help auth */ + "connect\n", + "connect mqtt://hostname[:port]\n", + "connect mqtts://hostname[:port]\n", + "connect ws://hostname[:port]\n", + "connect wss://hostname[:port]\n", + "\nConnect to a broker using the provided transport and port.\n", + "If no URL is provided, connects to mqtt://localhost:1883\n", /* help connect */ + "exit\n", + "\nQuit the program\n", /* help exit */ + "help \n", + "\nFind help on a command using 'help '\n", + "Press tab multiple times to find currently available commands.\n", /* help help */ + "Unknown command 'unknown'\n", /* help unknown */ + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + +TEST_F(CtrlShellHelpTest, Exit) +{ + mosq_config config{}; + + expect_setup(&config); + + char *s = strdup("exit"); + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s)); + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell__output(t::StrEq("\n"))); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellHelpTest, Connect) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char buf[200]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(nullptr)); + + /* This is a hacky way of working around the async mqtt CONNECT/CONNACK which we don't directly control. */ + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(&mosq, nullptr, 0); return 0; })); + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell__output(t::StrEq("\n"))); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellHelpTest, PostConnectHelp) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char buf[200]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("help"))) + .WillOnce(t::Return(strdup("help dynsec"))) + .WillOnce(t::Return(strdup("help broker"))) + .WillOnce(t::Return(strdup("help disconnect"))) + .WillOnce(t::Return(strdup("help exit"))) + .WillOnce(t::Return(strdup("help help"))) + .WillOnce(t::Return(strdup("help unknown"))) + .WillOnce(t::Return(strdup("unknown"))) + .WillOnce(t::Return(strdup("disconnect"))); + + /* This is a hacky way of working around the async mqtt send/receive which we don't directly control. + * Each send starts a wait which times out after two seconds. We use that call to produce the effect we want. + */ + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(&mosq, nullptr, 0); return 0; })); + + const char *outputs[] = { + "This is the mosquitto_ctrl interactive shell, for controlling aspects of a mosquitto broker.\n", + "Find help on a command using 'help '\n", + "Press tab multiple times to find currently available commands.\n\n", + "Example workflow:\n\n", + "> auth\n", + "username: admin\n", + "password:\n", + "> connect mqtt://localhost\n", + "mqtt://localhost:1883> dynsec\n", + "mqtt://localhost:1883|dynsec> createGroup newgroup\n", + "OK\n\n", + "\n", + + "dynsec\n", + "\nStart the dynamic-security control mode.\n", /* help dynsec */ + "broker\n", + "\nStart the broker control mode.\n", /* help broker */ + "disconnect\n", + "\nDisconnect from the broker\n", /* help disconnect */ + "exit\n", + "\nQuit the program\n", /* help exit */ + "help \n", + "\nFind help on a command using 'help '\n", + "Press tab multiple times to find currently available commands.\n", /* help help */ + "Unknown command 'unknown'\n", /* help unknown */ + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + expect_disconnect(&mosq); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellHelpTest, BrokerHelp) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char buf[200]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("broker"))) + .WillOnce(t::Return(strdup("disconnect"))); + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|broker> "))) + .WillOnce(t::Return(strdup("help "))) // Extra space on end to invoke trim + .WillOnce(t::Return(strdup("help listPlugins"))) + .WillOnce(t::Return(strdup("help listListeners"))) + .WillOnce(t::Return(strdup("help disconnect"))) + .WillOnce(t::Return(strdup("help help"))) + .WillOnce(t::Return(strdup("help return"))) + .WillOnce(t::Return(strdup("help exit"))) + .WillOnce(t::Return(strdup("help unknown"))) + .WillOnce(t::Return(strdup("unknown"))) + .WillOnce(t::Return(strdup("return"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe(t::_, nullptr, t::StrEq("$CONTROL/broker/v1/response"), 1)) + .WillOnce(t::Return(0)); + + /* This is a hacky way of working around the async mqtt send/receive which we don't directly control. + * Each send starts a wait which times out after two seconds. We use that call to produce the effect we want. + */ + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(&mosq, nullptr, 0); return 0; })) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(&mosq, nullptr, &msg); return 0; })); + + const char *outputs[] = { + "This is the mosquitto_ctrl interactive shell, for controlling aspects of a mosquitto broker.\n", + "You are in broker mode, for controlling some core broker functionality.\n", + "Use 'return' to leave this mode.\n", + "Find help on a command using 'help '\n", + "Press tab multiple times to find currently available commands.\n\n", + "\n", + "Unknown command 'unknown'\n", + + "listPlugins\n", + "\nLists currently loaded plugins.\n", + "listListeners\n", + "\nLists current listeners.\n", + "disconnect\n", + "\nDisconnect from the broker\n", /* help disconnect */ + "return\n", + "\nLeave broker mode.\n", + "exit\n", + "\nQuit the program\n", /* help exit */ + "help \n", + "\nFind help on a command using 'help '\n", + "Press tab multiple times to find currently available commands.\n", /* help help */ + "Invalid response from broker.\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + expect_disconnect(&mosq); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellHelpTest, DynsecHelp) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + char buf[200]; + + expect_setup(&config); + expect_connect(&mosq, host, port); + + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("dynsec"))) + .WillOnce(t::Return(nullptr)); + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883|dynsec> "))) + .WillOnce(t::Return(strdup("help"))) + .WillOnce(t::Return(strdup("help addClientRole"))) + .WillOnce(t::Return(strdup("help addGroupClient"))) + .WillOnce(t::Return(strdup("help addGroupRole"))) + .WillOnce(t::Return(strdup("help addRoleACL"))) + .WillOnce(t::Return(strdup("help createClient"))) + .WillOnce(t::Return(strdup("help createGroup"))) + .WillOnce(t::Return(strdup("help createRole"))) + .WillOnce(t::Return(strdup("help deleteClient"))) + .WillOnce(t::Return(strdup("help deleteGroup"))) + .WillOnce(t::Return(strdup("help deleteRole"))) + .WillOnce(t::Return(strdup("help disableClient"))) + .WillOnce(t::Return(strdup("help enableClient"))) + .WillOnce(t::Return(strdup("help getAnonymousGroup"))) + .WillOnce(t::Return(strdup("help getClient"))) + .WillOnce(t::Return(strdup("help getDefaultACLAccess"))) + .WillOnce(t::Return(strdup("help getGroup"))) + .WillOnce(t::Return(strdup("help getRole"))) + .WillOnce(t::Return(strdup("help listClients"))) + .WillOnce(t::Return(strdup("help listGroups"))) + .WillOnce(t::Return(strdup("help listRoles"))) + .WillOnce(t::Return(strdup("help removeClientRole"))) + .WillOnce(t::Return(strdup("help removeGroupClient"))) + .WillOnce(t::Return(strdup("help removeGroupRole"))) + .WillOnce(t::Return(strdup("help removeRoleACL"))) + .WillOnce(t::Return(strdup("help setAnonymousGroup"))) + .WillOnce(t::Return(strdup("help setClientId"))) + .WillOnce(t::Return(strdup("help setClientPassword"))) + .WillOnce(t::Return(strdup("help setDefaultACLAccess"))) + .WillOnce(t::Return(strdup("help modifyClient"))) + .WillOnce(t::Return(strdup("help modifyGroup"))) + .WillOnce(t::Return(strdup("help modifyRole"))) + .WillOnce(t::Return(strdup("help disconnect"))) + .WillOnce(t::Return(strdup("help help"))) + .WillOnce(t::Return(strdup("help return"))) + .WillOnce(t::Return(strdup("help exit"))) + .WillOnce(t::Return(strdup("help unknown"))) + .WillOnce(t::Return(strdup("unknown"))) + .WillOnce(t::Return(strdup("return"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe(t::_, nullptr, t::StrEq("$CONTROL/dynamic-security/v1/response"), 1)) + .WillOnce(t::Return(0)); + + /* This is a hacky way of working around the async mqtt send/receive which we don't directly control. + * Each send starts a wait which times out after two seconds. We use that call to produce the effect we want. + */ + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(&mosq, nullptr, 0); return 0; })) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(&mosq, nullptr, &msg); return 0; })) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(&mosq, nullptr, &msg); return 0; })) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(&mosq, nullptr, &msg); return 0; })) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(&mosq, nullptr, &msg); return 0; })) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(&mosq, nullptr, &msg); return 0; })) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(&mosq, nullptr, &msg); return 0; })) + .WillOnce(t::Invoke([this, &mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(&mosq, nullptr, &msg); return 0; })); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listClients\"}]}"), 1, false)) + .WillOnce(t::Return(0)); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listGroups\"}]}"), 1, false)) + .WillOnce(t::Return(0)); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(&mosq), nullptr, t::StrEq("$CONTROL/dynamic-security/v1"), t::_, + t::StrEq("{\"commands\":[{\"command\":\"listRoles\"}]}"), 1, false)) + .WillOnce(t::Return(0)); + + const char *outputs[] = { + "This is the mosquitto_ctrl interactive shell, for controlling aspects of a mosquitto broker.\n", + "You are in dynsec mode, for controlling the dynamic-security clients, groups, and roles used in authentication and authorisation.\n", + "Use 'return' to leave dynsec mode.\n", + "Find help on a command using 'help '\n", + "Press tab multiple times to find currently available commands.\n\n", + "\n", + "Unknown command 'unknown'\n", + + "addClientRole \n", + "\nAdds a role directly to a client.\n", + "addGroupClient \n", + "\nAdds a client to a group.\n", + "addGroupRole \n", + "\nAdds a role to a group.\n", + "addRoleACL publishClientReceive allow|deny \n", + "addRoleACL publishClientSend allow|deny \n", + "addRoleACL subscribeLiteral allow|deny \n", + "addRoleACL subscribePattern allow|deny \n", + "addRoleACL unsubscribeLiteral allow|deny \n", + "addRoleACL unsubscribePattern allow|deny \n", + "\nAdds an ACL to a role.\n", + "createClient [password [clientid]]\n", + "\nCreate a client with password and optional client id.\n", + "createGroup \n", + "\nCreate a new group.\n", + "createRole \n", + "\nCreate a new role.\n", + "deleteClient \n", + "\nDelete a client\n", + "deleteGroup \n", + "\nDelete a group\n", + "deleteRole \n", + "\nDelete a role\n", + "disableClient \n", + "\nDisable a client. This client will not be able to log in, and will be kicked if it has an existing session.\n", + "enableClient \n", + "\nEnable a client. Disabled clients are unable to log in.\n", + "getAnonymousGroup\n", + "\nPrint the group configured as the anonymous group.\n", + "getClient \n", + "\nPrint details of a client and its groups and direct roles.\n", + "getDefaultACLAccess\n", + "\nPrint the default allow/deny values for the different classes of ACL.\n", + "getGroup \n", + "\nPrint details of a group and its roles.\n", + "getRole \n", + "\nPrint details of a role and its ACLs.\n", + "listClients [count [offset]]\n", + "\nPrint a list of clients configured in the dynsec plugin, with an optional total count and list offset.\n", + "listGroups [count [offset]]\n", + "\nPrint a list of groups configured in the dynsec plugin, with an optional total count and list offset.\n", + "listRoles [count [offset]]\n", + "\nPrint a list of roles configured in the dynsec plugin, with an optional total count and list offset.\n", + "removeClientRole \n", + "\nRemoves a role from a client, where the role was directly attached to the client.\n", + "removeGroupClient \n", + "\nRemoves a client from a group.\n", + "removeGroupRole \n", + "\nRemoves a role from a group.\n", + "removeRoleACL publishClientReceive \n", + "removeRoleACL publishClientSend \n", + "removeRoleACL subscribeLiteral \n", + "removeRoleACL subscribePattern \n", + "removeRoleACL unsubscribeLiteral \n", + "removeRoleACL unsubscribePattern \n", + "\nRemoves an ACL from a role.\n", + "setAnonymousGroup \n", + "\nSets the anonymous group to a new group.\n", + "setClientId \n", + "setClientId \n", + "\nSets or clears the clientid associated with a client. If a client has a clientid, all three of username, password, and clientid must match for a client to be able to authenticate.\n", + "setClientPassword [password]\n", + "\nSets a new password for a client.\n", + "setDefaultACLAccess publishClientReceive allow|deny\n", + "setDefaultACLAccess publishClientSend allow|deny\n", + "setDefaultACLAccess subscribe allow|deny\n", + "setDefaultACLAccess unsubscribe allow|deny\n", + "\nSets the default ACL access to use for an ACL type. The default access will be applied if no other ACL rules match.\n", + "Setting a rule to 'allow' means that if no ACLs match, it will be accepted.\n", + "Setting a rule to 'deny' means that if no ACLs match, it will be denied.\n", + "modifyClient textName \n", + "modifyClient textDescription \n", + "\nModify the text name or text description for a client.\n", + "These are free-text fields for your own use.\n", + "modifyGroup textName \n", + "modifyGroup textDescription \n", + "\nModify the text name or text description for a group.\n", + "modifyRole textName \n", + "modifyRole textDescription \n", + "modifyRole allowWildcardSubs true|false\n", + "\nModify the text name or text description for a role.\n", + + "disconnect\n", + "\nDisconnect from the broker\n", /* help disconnect */ + "return\n", + "\nLeave dynsec mode.\n", + "exit\n", + "\nQuit the program\n", /* help exit */ + "help \n", + "\nFind help on a command using 'help '\n", "Press tab multiple times to find currently available commands.\n", /* help help */ + "Invalid response from broker.\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} diff --git a/test/apps/ctrl/ctrl_shell_options_test.cpp b/test/apps/ctrl/ctrl_shell_options_test.cpp new file mode 100644 index 00000000..8d75297f --- /dev/null +++ b/test/apps/ctrl/ctrl_shell_options_test.cpp @@ -0,0 +1,368 @@ +/* +Copyright (c) 2022 Cedalo GmbH +*/ + +// clang-format off +#include "mosquitto_internal.h" // keep this at the top for `#define uthash_free` +#include "ctrl_shell.h" +#include "ctrl_shell_internal.h" +#include "json_help.h" +#include "utlist.h" +// clang-format on +#include +#include + +#include + +#include "ctrl_shell_mock.hpp" +#include "editline_mock.hpp" +#include "libmosquitto_mock.hpp" +#include "pthread_mock.hpp" + +namespace t = testing; + +typedef void (*LIBMOSQ_FUNC_connect_callback)(struct mosquitto *, void *, int); +typedef void (*LIBMOSQ_FUNC_message_callback)(struct mosquitto *, void *, const struct mosquitto_message *); + +struct pending_payload{ + struct pending_payload *next, *prev; + char payload[1024]; +}; + +class CtrlShellOptionsTest : public ::t::Test +{ + public: + ::t::StrictMock ctrl_shell_mock_{}; + ::t::StrictMock editline_mock_{}; + ::t::StrictMock libmosquitto_mock_{}; + ::t::StrictMock pthread_mock_{}; + LIBMOSQ_FUNC_connect_callback on_connect{}; + LIBMOSQ_FUNC_message_callback on_message{}; + struct pending_payload *pending_payloads = nullptr; + + void expect_setup(struct mosq_config *config) + { + editline_mock_.reset(); + EXPECT_CALL(editline_mock_, rl_bind_key(t::Eq('\t'), t::_)); + EXPECT_CALL(editline_mock_, add_history(t::_)).WillRepeatedly(t::Return(0)); + EXPECT_CALL(editline_mock_, clear_history()).Times(t::AnyNumber()); + config->no_colour = true; + config->port = PORT_UNDEFINED; + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell__output(t::StartsWith("mosquitto_ctrl shell v"))); + } + + void expect_connect(struct mosquitto *mosq, const char *host, int port) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_new(t::Eq(nullptr), t::Eq(true), t::Eq(nullptr))) + .WillOnce(t::Return(mosq)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_int_option(t::Eq(mosq), MOSQ_OPT_PROTOCOL_VERSION, 5)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish_v5_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect(t::Eq(mosq), t::StrEq(host), port, 60)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_start(t::Eq(mosq))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect_callback_set(t::Eq(mosq), t::A())) + .WillRepeatedly(t::SaveArg<1>(&this->on_connect)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_message_callback_set(t::Eq(mosq), t::A())) + .WillOnce(t::SaveArg<1>(&this->on_message)); + } + + void expect_disconnect(struct mosquitto *mosq) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_disconnect(t::Eq(mosq))); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_stop(t::Eq(mosq), false)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_destroy(t::Eq(mosq))); + } + + void expect_outputs(const char **outputs, size_t count) + { + for(size_t i=0; ipayload, sizeof(pp->payload), "%s", respons); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(mosq), nullptr, t::StrEq("$CONTROL/broker/v1"), t::_, + t::StrEq(request), 1, false)) + .WillOnce(t::Invoke([this, pp](){ + DL_APPEND(this->pending_payloads, pp); + return 0; + })); + } + + void expect_request_response_success(struct mosquitto *mosq, const char *request, const char *command) + { + char response[100]; + snprintf(response, sizeof(response), "{\"responses\":[{\"command\":\"%s\"}]}", command); + expect_request_response(mosq, request, response); + } + + void expect_request_response_empty(struct mosquitto *mosq, const char *command) + { + char request[100]; + char response[100]; + + snprintf(request, sizeof(request), "{\"commands\":[{\"command\":\"%s\"}]}", command); + snprintf(response, sizeof(response), "{\"responses\":[{\"command\":\"%s\",\"data\":{}}]}", command); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(mosq), nullptr, t::StrEq("$CONTROL/broker/v1"), t::_, + t::StrEq(request), 1, false)) + .WillOnce(t::Invoke([this, &command](){ + append_empty_response(command); + return 0; + })); + } + + void append_response(const char *response) + { + struct pending_payload *pp = (struct pending_payload *)calloc(1, sizeof(struct pending_payload)); + snprintf(pp->payload, sizeof(pp->payload), "%s", response); + DL_APPEND(this->pending_payloads, pp); + } + + void append_empty_response(const char *command) + { + struct pending_payload *pp = (struct pending_payload *)calloc(1, sizeof(struct pending_payload)); + snprintf(pp->payload, sizeof(pp->payload), + "{\"responses\":[{\"command\":\"%s\",\"data\":{}}]}", command); + DL_APPEND(this->pending_payloads, pp); + } + + void expect_broker(const char *host, int port) + { + char buf[200]; + snprintf(buf, sizeof(buf), "connect mqtt://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("broker"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe(t::_, nullptr, t::StrEq("$CONTROL/broker/v1/response"), 1)) + .WillOnce(t::Return(0)); + } + + void expect_connect_and_messages(struct mosquitto *mosq) + { + /* This is a hacky way of working around the async mqtt send/receive which we don't directly control. + * Each send starts a wait which times out after two seconds. We use that call to produce the effect we want. + */ + EXPECT_CALL(pthread_mock_, pthread_cond_timedwait(t::_, t::_, t::_)) + .WillOnce(t::Invoke([this, mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + this->on_connect(mosq, nullptr, 0); return 0; })) + .WillRepeatedly(t::Invoke([this, mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + struct pending_payload *pp = pending_payloads; + if(pp){ + DL_DELETE(pending_payloads, pp); + msg.payload = pp->payload; + msg.payloadlen = (int)strlen((char *)msg.payload); + this->on_message(mosq, nullptr, &msg); + free(pp); + } + return 0; + })); + } +}; + + +TEST_F(CtrlShellOptionsTest, ConnectTLS) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 8883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_connect_and_messages(&mosq); + + + char buf[200]; + snprintf(buf, sizeof(buf), "connect mqtts://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_int_option(t::_, MOSQ_OPT_TLS_USE_OS_CERTS, 1)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtts://localhost:8883> "))) + .WillOnce(t::Return(strdup("exit"))); + + const char *outputs[] = { + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellOptionsTest, ConnectWebsockets) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 8080; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_connect_and_messages(&mosq); + + + char buf[200]; + snprintf(buf, sizeof(buf), "connect ws://%s:%d", host, port); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_int_option(t::_, MOSQ_OPT_TRANSPORT, MOSQ_T_WEBSOCKETS)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("ws://localhost:8080> "))) + .WillOnce(t::Return(strdup("exit"))); + + const char *outputs[] = { + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellOptionsTest, ConnectImplicitPort) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_connect_and_messages(&mosq); + + + char buf[200]; + snprintf(buf, sizeof(buf), "connect %s", host); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("exit"))); + + const char *outputs[] = { + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellOptionsTest, ConnectCertNotFound) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_connect_and_messages(&mosq); + + config.cafile = strdup("missing cafile"); + config.capath = strdup("missing capath"); + config.certfile = strdup("missing certfile"); + config.keyfile = strdup("missing keyfile"); + + char buf[200]; + snprintf(buf, sizeof(buf), "connect %s", host); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_tls_set( + t::_, + t::StrEq("missing cafile"), + t::StrEq("missing capath"), + t::StrEq("missing certfile"), + t::StrEq("missing keyfile"), + nullptr)) + .WillOnce(t::Return(MOSQ_ERR_INVAL)); + + const char *outputs[] = { + "Error setting TLS options: File not found.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + free(config.cafile); + free(config.capath); + free(config.certfile); + free(config.keyfile); +} + + +TEST_F(CtrlShellOptionsTest, ConnectCertError) +{ + mosq_config config{}; + mosquitto mosq{}; + const char host[] = "localhost"; + int port = 1883; + + expect_setup(&config); + expect_connect(&mosq, host, port); + expect_connect_and_messages(&mosq); + + config.cafile = strdup("cafile"); + config.capath = strdup("capath"); + config.certfile = strdup("certfile"); + config.keyfile = strdup("keyfile"); + + char buf[200]; + snprintf(buf, sizeof(buf), "connect %s", host); + char *s_conn = strdup(buf); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(s_conn)); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("mqtt://localhost:1883> "))) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_tls_set( + t::_, + t::StrEq("cafile"), + t::StrEq("capath"), + t::StrEq("certfile"), + t::StrEq("keyfile"), + nullptr)) + .WillOnce(t::Return(MOSQ_ERR_TLS)); + + const char *outputs[] = { + "Error setting TLS options: A TLS error occurred.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); + free(config.cafile); + free(config.capath); + free(config.certfile); + free(config.keyfile); +} diff --git a/test/apps/ctrl/ctrl_shell_pre_connect_test.cpp b/test/apps/ctrl/ctrl_shell_pre_connect_test.cpp new file mode 100644 index 00000000..13e490b2 --- /dev/null +++ b/test/apps/ctrl/ctrl_shell_pre_connect_test.cpp @@ -0,0 +1,167 @@ +/* +Copyright (c) 2022 Cedalo GmbH +*/ + +// clang-format off +#include "mosquitto_internal.h" // keep this at the top for `#define uthash_free` +#include "ctrl_shell.h" +#include "ctrl_shell_internal.h" +#include "json_help.h" +// clang-format on +#include +#include + +#include + +#include "ctrl_shell_mock.hpp" +#include "editline_mock.hpp" +#include "libmosquitto_mock.hpp" +#include "pthread_mock.hpp" + +namespace t = testing; + +typedef void (*LIBMOSQ_FUNC_connect_callback)(struct mosquitto *, void *, int); +typedef void (*LIBMOSQ_FUNC_message_callback)(struct mosquitto *, void *, const struct mosquitto_message *); + +class CtrlShellPreConnectTest : public ::t::Test +{ + public: + ::t::StrictMock ctrl_shell_mock_{}; + ::t::StrictMock editline_mock_{}; + ::t::StrictMock libmosquitto_mock_{}; + ::t::StrictMock pthread_mock_{}; + LIBMOSQ_FUNC_connect_callback on_connect{}; + LIBMOSQ_FUNC_message_callback on_message{}; + + void expect_setup(struct mosq_config *config) + { + editline_mock_.reset(); + EXPECT_CALL(editline_mock_, rl_bind_key(t::Eq('\t'), t::_)); + EXPECT_CALL(editline_mock_, add_history(t::_)).WillRepeatedly(t::Return(0)); + EXPECT_CALL(editline_mock_, clear_history()).Times(t::AnyNumber()); + config->no_colour = true; + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell__output(t::StartsWith("mosquitto_ctrl shell v"))); + } + + void expect_connect(struct mosquitto *mosq, const char *host, int port) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_new(t::Eq(nullptr), t::Eq(true), t::Eq(nullptr))) + .WillOnce(t::Return(mosq)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_int_option(t::Eq(mosq), MOSQ_OPT_PROTOCOL_VERSION, 5)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish_v5_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect(t::Eq(mosq), t::StrEq(host), port, 60)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_start(t::Eq(mosq))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect_callback_set(t::Eq(mosq), t::A())) + .WillRepeatedly(t::SaveArg<1>(&this->on_connect)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_message_callback_set(t::Eq(mosq), t::A())) + .WillOnce(t::SaveArg<1>(&this->on_message)); + } + + void expect_disconnect(struct mosquitto *mosq) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_disconnect(t::Eq(mosq))); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_stop(t::Eq(mosq), false)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_destroy(t::Eq(mosq))); + } + + void expect_outputs(const char **outputs, size_t count) + { + for(size_t i=0; ion_connect(mosq, nullptr, 0); return 0; })) + .WillRepeatedly(t::Invoke([this, mosq](pthread_cond_t *, pthread_mutex_t *, const struct timespec *){ + mosquitto_message msg{}; + this->on_message(mosq, nullptr, &msg); return 0; })); + } +}; + + +TEST_F(CtrlShellPreConnectTest, AuthNoUsername) +{ + mosq_config config{}; + + expect_setup(&config); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(strdup("auth"))) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("username: "))) + .WillOnce(t::Return(strdup("username1"))); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "password1"); + return s; + })); + + const char *outputs[] = { + "password:", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellPreConnectTest, AuthWithUsername) +{ + mosq_config config{}; + + expect_setup(&config); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(strdup("auth username1"))) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Invoke([](char *s, int size, FILE *){ + snprintf(s, (size_t)size, "password1"); + return s; + })); + + const char *outputs[] = { + "password:", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} + + +TEST_F(CtrlShellPreConnectTest, AuthNoPassword) +{ + mosq_config config{}; + + expect_setup(&config); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("> "))) + .WillOnce(t::Return(strdup("auth username1"))) + .WillOnce(t::Return(strdup("exit"))); + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell_fgets(t::_, t::_, t::_)) + .WillOnce(t::Return(nullptr)); + + const char *outputs[] = { + "password:", + "No password.\n", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(&config); +} diff --git a/test/apps/ctrl/ctrl_shell_test.cpp b/test/apps/ctrl/ctrl_shell_test.cpp new file mode 100644 index 00000000..a0745775 --- /dev/null +++ b/test/apps/ctrl/ctrl_shell_test.cpp @@ -0,0 +1,171 @@ +/* +Copyright (c) 2022 Cedalo GmbH +*/ + +// clang-format off +#include "mosquitto_internal.h" // keep this at the top for `#define uthash_free` +#include "ctrl_shell.h" +#include "ctrl_shell_internal.h" +#include "json_help.h" +#include "utlist.h" +// clang-format on +#include +#include + +#include + +#include "ctrl_shell_mock.hpp" +#include "editline_mock.hpp" +#include "libmosquitto_mock.hpp" +#include "pthread_mock.hpp" + +namespace t = testing; + +typedef void (*LIBMOSQ_FUNC_connect_callback)(struct mosquitto *, void *, int); +typedef void (*LIBMOSQ_FUNC_message_callback)(struct mosquitto *, void *, const struct mosquitto_message *); + +struct pending_payload{ + struct pending_payload *next, *prev; + char payload[1024]; +}; + +class CtrlShellTest : public ::t::Test +{ + public: + ::t::StrictMock ctrl_shell_mock_{}; + ::t::StrictMock editline_mock_{}; + ::t::StrictMock libmosquitto_mock_{}; + ::t::StrictMock pthread_mock_{}; + LIBMOSQ_FUNC_connect_callback on_connect{}; + LIBMOSQ_FUNC_message_callback on_message{}; + struct pending_payload *pending_payloads = nullptr; + + void expect_setup(struct mosq_config *config) + { + editline_mock_.reset(); + EXPECT_CALL(editline_mock_, rl_bind_key(t::Eq('\t'), t::_)); + EXPECT_CALL(editline_mock_, add_history(t::_)).WillRepeatedly(t::Return(0)); + EXPECT_CALL(editline_mock_, clear_history()).Times(t::AnyNumber()); + config->no_colour = true; + + EXPECT_CALL(ctrl_shell_mock_, ctrl_shell__output(t::StartsWith("mosquitto_ctrl shell v"))); + } + + void expect_connect(struct mosquitto *mosq, const char *host, int port) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_new(t::Eq(nullptr), t::Eq(true), t::Eq(nullptr))) + .WillOnce(t::Return(mosq)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_int_option(t::Eq(mosq), MOSQ_OPT_PROTOCOL_VERSION, 5)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_subscribe_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish_v5_callback_set(t::Eq(mosq), t::_)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect(t::Eq(mosq), t::StrEq(host), port, 60)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_start(t::Eq(mosq))); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_connect_callback_set(t::Eq(mosq), t::A())) + .WillRepeatedly(t::SaveArg<1>(&this->on_connect)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_message_callback_set(t::Eq(mosq), t::A())) + .WillOnce(t::SaveArg<1>(&this->on_message)); + } + + void expect_disconnect(struct mosquitto *mosq) + { + EXPECT_CALL(libmosquitto_mock_, mosquitto_disconnect(t::Eq(mosq))); + EXPECT_CALL(libmosquitto_mock_, mosquitto_loop_stop(t::Eq(mosq), false)); + EXPECT_CALL(libmosquitto_mock_, mosquitto_destroy(t::Eq(mosq))); + } + + void expect_outputs(const char **outputs, size_t count) + { + for(size_t i=0; ipayload, sizeof(pp->payload), "%s", respons); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(mosq), nullptr, t::StrEq("$CONTROL/broker/v1"), t::_, + t::StrEq(request), 1, false)) + .WillOnce(t::Invoke([this, pp](){ + DL_APPEND(this->pending_payloads, pp); + return 0; + })); + } + + void expect_request_response_success(struct mosquitto *mosq, const char *request, const char *command) + { + char response[100]; + snprintf(response, sizeof(response), "{\"responses\":[{\"command\":\"%s\"}]}", command); + expect_request_response(mosq, request, response); + } + + void expect_request_response_empty(struct mosquitto *mosq, const char *command) + { + char request[100]; + char response[100]; + + snprintf(request, sizeof(request), "{\"commands\":[{\"command\":\"%s\"}]}", command); + snprintf(response, sizeof(response), "{\"responses\":[{\"command\":\"%s\",\"data\":{}}]}", command); + + EXPECT_CALL(libmosquitto_mock_, mosquitto_publish(t::Eq(mosq), nullptr, t::StrEq("$CONTROL/broker/v1"), t::_, + t::StrEq(request), 1, false)) + .WillOnce(t::Invoke([this, &command](){ + append_empty_response(command); + return 0; + })); + } + + void append_response(const char *response) + { + struct pending_payload *pp = (struct pending_payload *)calloc(1, sizeof(struct pending_payload)); + snprintf(pp->payload, sizeof(pp->payload), "%s", response); + DL_APPEND(this->pending_payloads, pp); + } + + void append_empty_response(const char *command) + { + struct pending_payload *pp = (struct pending_payload *)calloc(1, sizeof(struct pending_payload)); + snprintf(pp->payload, sizeof(pp->payload), + "{\"responses\":[{\"command\":\"%s\",\"data\":{}}]}", command); + DL_APPEND(this->pending_payloads, pp); + } +}; + + +#if 0 +/* Hangs on CI, presumably due to blocking read() */ +TEST_F(CtrlShellTest, NoConfig) +{ + /* No config means we have colour mode enabled */ + mosq_config config{}; + + expect_setup(&config); + + EXPECT_CALL(editline_mock_, readline(t::StrEq("\x1\x1B[38;5;80m\x2>\x1\x1B[0m\x2 "))) + .WillOnce(t::Return(strdup("exit"))); + + const char *outputs[] = { + "\x1B]10;?\a\x1B]11;?\a", + "\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + ctrl_shell__main(nullptr); +} +#endif + +extern "C" { void set_no_colour(void); } + +TEST_F(CtrlShellTest, PrintLabelValue) +{ + const char *outputs[] = { + "My Label:", + " 10\n", + }; + expect_outputs(outputs, sizeof(outputs)/sizeof(char *)); + + set_no_colour(); + ctrl_shell_print_label_value(0, "My Label:", 10, "%u\n", 10); +} diff --git a/test/mock/CMakeLists.txt b/test/mock/CMakeLists.txt index 5707caa4..64e08ebc 100644 --- a/test/mock/CMakeLists.txt +++ b/test/mock/CMakeLists.txt @@ -1,10 +1,13 @@ add_subdirectory(lib) +add_subdirectory(apps) -add_library(editline_mock OBJECT editline_mock.cpp) -target_include_directories(editline_mock - PUBLIC - ${mosquitto_SOURCE_DIR}/test/mock -) +if(EDITLINE_FOUND) + add_library(editline_mock OBJECT editline_mock.cpp) + target_include_directories(editline_mock + PUBLIC + ${mosquitto_SOURCE_DIR}/test/mock + ) +endif() add_library(pthread_mock OBJECT pthread_mock.cpp) target_include_directories(pthread_mock diff --git a/test/mock/Makefile b/test/mock/Makefile index 9a176ff0..136abf33 100644 --- a/test/mock/Makefile +++ b/test/mock/Makefile @@ -21,6 +21,7 @@ MOCKS = \ all : test-compile test-compile : ${MOCKS} + $(MAKE) -C apps/mosquitto_ctrl test-compile $(MAKE) -C lib test-compile check : test diff --git a/test/mock/apps/CMakeLists.txt b/test/mock/apps/CMakeLists.txt new file mode 100644 index 00000000..fea3e91a --- /dev/null +++ b/test/mock/apps/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(mosquitto_ctrl) diff --git a/test/mock/apps/mosquitto_ctrl/CMakeLists.txt b/test/mock/apps/mosquitto_ctrl/CMakeLists.txt new file mode 100644 index 00000000..0e7996d6 --- /dev/null +++ b/test/mock/apps/mosquitto_ctrl/CMakeLists.txt @@ -0,0 +1,16 @@ +if(EDITLINE_FOUND) + add_library(ctrl_shell_mock OBJECT + ctrl_shell_mock.cpp + ctrl_shell_mock.hpp + ) + target_compile_definitions(ctrl_shell_mock PRIVATE WITH_EDITLINE) + + target_include_directories(ctrl_shell_mock + PUBLIC + ${mosquitto_SOURCE_DIR} + ${mosquitto_SOURCE_DIR}/include + ${mosquitto_SOURCE_DIR}/apps/mosquitto_ctrl + ${mosquitto_SOURCE_DIR}/test/mock + ${mosquitto_SOURCE_DIR}/test/mock/apps/mosquitto_ctrl + ) +endif() diff --git a/test/mock/apps/mosquitto_ctrl/Makefile b/test/mock/apps/mosquitto_ctrl/Makefile new file mode 100644 index 00000000..a6fe1e64 --- /dev/null +++ b/test/mock/apps/mosquitto_ctrl/Makefile @@ -0,0 +1,34 @@ +R=../../../.. +include ${R}/config.mk + +.PHONY: all check test-compile test clean + +LOCAL_CPPFLAGS+= \ + -I../ \ + -I${R} \ + -I${R}/apps/mosquitto_ctrl \ + -I${R}/include \ + -I${R}/lib \ + -I${R}/test/mock + +LOCAL_CXXFLAGS+=-std=c++20 -Wall -ggdb -D TEST_SOURCE_DIR='"$(realpath .)"' + +MOCK_OBJS = \ + ctrl_shell_mock.o + +all : test-compile + +test-compile: ${MOCK_OBJS} +check : test + +# MOCKS + +${MOCK_OBJS} : %.o: %.cpp ctrl_shell_mock.hpp + $(CROSS_COMPILE)$(CXX) $(LOCAL_CPPFLAGS) $(LOCAL_CXXFLAGS) -c $< -o $@ + +clean : + -rm -rf *.o *.gcda *.gcno + +install: + +uninstall: diff --git a/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.cpp b/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.cpp new file mode 100644 index 00000000..1dab575b --- /dev/null +++ b/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.cpp @@ -0,0 +1,14 @@ +#include "ctrl_shell_mock.hpp" + +CtrlShellMock::CtrlShellMock() {} +CtrlShellMock::~CtrlShellMock() {} + +void ctrl_shell__output(const char *s) +{ + return CtrlShellMock::get_mock().ctrl_shell__output(s); +} + +char *ctrl_shell_fgets(char *s, int size, FILE *stream) +{ + return CtrlShellMock::get_mock().ctrl_shell_fgets(s, size, stream); +} diff --git a/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.hpp b/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.hpp new file mode 100644 index 00000000..c1e13a1a --- /dev/null +++ b/test/mock/apps/mosquitto_ctrl/ctrl_shell_mock.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +#include "ctrl_shell_internal.h" + +#include "c_function_mock.hpp" + +class CtrlShellMock : public CFunctionMock { + public: + CtrlShellMock(); + virtual ~CtrlShellMock(); + + MOCK_METHOD(void, ctrl_shell__output, (const char *s)); + MOCK_METHOD(char *, ctrl_shell_fgets, (char *s, int size, FILE *stream)); +};