From fdff255916458057e9c6b87f20ba21accf9b5078 Mon Sep 17 00:00:00 2001 From: "Roger A. Light" Date: Wed, 23 Sep 2020 22:59:31 +0100 Subject: [PATCH] New dynamic security plugin. --- .gitignore | 2 + CMakeLists.txt | 7 + apps/CMakeLists.txt | 1 + apps/Makefile | 1 + apps/mosquitto_ctrl/CMakeLists.txt | 29 + apps/mosquitto_ctrl/Makefile | 79 ++ apps/mosquitto_ctrl/client.c | 147 +++ apps/mosquitto_ctrl/dynsec.c | 505 ++++++++ apps/mosquitto_ctrl/dynsec_client.c | 163 +++ apps/mosquitto_ctrl/dynsec_group.c | 185 +++ apps/mosquitto_ctrl/dynsec_role.c | 191 +++ apps/mosquitto_ctrl/mosquitto_ctrl.c | 90 ++ apps/mosquitto_ctrl/mosquitto_ctrl.h | 109 ++ apps/mosquitto_ctrl/options.c | 898 +++++++++++++++ client/CMakeLists.txt | 4 - client/args.txt | 2 +- cmake/FindcJSON.cmake | 1 + config.mk | 14 +- docker/local/Dockerfile | 18 +- man/Makefile | 7 + man/mosquitto.8.xml | 6 + man/mosquitto_ctrl.1.meta | 5 + man/mosquitto_ctrl.1.xml | 638 ++++++++++ plugins/CMakeLists.txt | 1 + plugins/Makefile | 1 + plugins/README.md | 7 +- plugins/dynamic-security/CMakeLists.txt | 36 + plugins/dynamic-security/Makefile | 60 + plugins/dynamic-security/README.md | 579 ++++++++++ plugins/dynamic-security/acl.c | 248 ++++ plugins/dynamic-security/auth.c | 193 ++++ plugins/dynamic-security/clients.c | 868 ++++++++++++++ plugins/dynamic-security/dynamic_security.h | 223 ++++ plugins/dynamic-security/groups.c | 1023 +++++++++++++++++ {src => plugins/dynamic-security}/json_help.c | 9 +- {src => plugins/dynamic-security}/json_help.h | 0 plugins/dynamic-security/plugin.c | 485 ++++++++ plugins/dynamic-security/roles.c | 953 +++++++++++++++ plugins/dynamic-security/sub_matches_sub.c | 218 ++++ src/linker-macosx.syms | 1 + src/linker.syms | 1 + src/mosquitto_broker_internal.h | 4 + src/security_default.c | 5 +- test/broker/14-dynsec-client.py | 143 +++ test/broker/14-dynsec-group.py | 160 +++ test/broker/14-dynsec-modify-client.py | 219 ++++ test/broker/14-dynsec-modify-group.py | 212 ++++ test/broker/14-dynsec-modify-role.py | 160 +++ test/broker/14-dynsec-role.py | 220 ++++ test/broker/14-plugin-register-control.py | 55 - test/broker/Makefile | 7 +- test/broker/test.py | 7 +- test/mosq_test.py | 45 + 53 files changed, 9165 insertions(+), 80 deletions(-) create mode 100644 apps/mosquitto_ctrl/CMakeLists.txt create mode 100644 apps/mosquitto_ctrl/Makefile create mode 100644 apps/mosquitto_ctrl/client.c create mode 100644 apps/mosquitto_ctrl/dynsec.c create mode 100644 apps/mosquitto_ctrl/dynsec_client.c create mode 100644 apps/mosquitto_ctrl/dynsec_group.c create mode 100644 apps/mosquitto_ctrl/dynsec_role.c create mode 100644 apps/mosquitto_ctrl/mosquitto_ctrl.c create mode 100644 apps/mosquitto_ctrl/mosquitto_ctrl.h create mode 100644 apps/mosquitto_ctrl/options.c create mode 100644 man/mosquitto_ctrl.1.meta create mode 100644 man/mosquitto_ctrl.1.xml create mode 100644 plugins/dynamic-security/CMakeLists.txt create mode 100644 plugins/dynamic-security/Makefile create mode 100644 plugins/dynamic-security/README.md create mode 100644 plugins/dynamic-security/acl.c create mode 100644 plugins/dynamic-security/auth.c create mode 100644 plugins/dynamic-security/clients.c create mode 100644 plugins/dynamic-security/dynamic_security.h create mode 100644 plugins/dynamic-security/groups.c rename {src => plugins/dynamic-security}/json_help.c (91%) rename {src => plugins/dynamic-security}/json_help.h (100%) create mode 100644 plugins/dynamic-security/plugin.c create mode 100644 plugins/dynamic-security/roles.c create mode 100644 plugins/dynamic-security/sub_matches_sub.c create mode 100755 test/broker/14-dynsec-client.py create mode 100755 test/broker/14-dynsec-group.py create mode 100755 test/broker/14-dynsec-modify-client.py create mode 100755 test/broker/14-dynsec-modify-group.py create mode 100755 test/broker/14-dynsec-modify-role.py create mode 100755 test/broker/14-dynsec-role.py delete mode 100755 test/broker/14-plugin-register-control.py diff --git a/.gitignore b/.gitignore index 8e5877fb..575aafe7 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,7 @@ c/*.test cpp/*.test apps/db_dump/mosquitto_db_dump +apps/mosquitto_ctrl/mosquitto_ctrl apps/mosquitto_passwd/mosquitto_passwd build/ @@ -44,6 +45,7 @@ man/mosquitto.8 man/mosquitto-tls.7 man/mosquitto.conf.5 man/libmosquitto.3 +man/mosquitto_ctrl.1 man/mosquitto_passwd.1 man/mosquitto_pub.1 man/mosquitto_rr.1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 3462e3f8..d2acbc6d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -97,6 +97,13 @@ if (WITH_DLT) add_definitions("-DWITH_DLT") endif (WITH_DLT) +FIND_PACKAGE(cJSON) +if (CJSON_FOUND) + message(STATUS ${CJSON_FOUND}) + add_definitions("-DWITH_CJSON") +endif() + + # ======================================== # Include projects # ======================================== diff --git a/apps/CMakeLists.txt b/apps/CMakeLists.txt index d2e6ce83..bc39864e 100644 --- a/apps/CMakeLists.txt +++ b/apps/CMakeLists.txt @@ -1 +1,2 @@ +add_subdirectory(mosquitto_ctrl) add_subdirectory(mosquitto_passwd) diff --git a/apps/Makefile b/apps/Makefile index c31ef483..0fa8483f 100644 --- a/apps/Makefile +++ b/apps/Makefile @@ -1,5 +1,6 @@ DIRS= \ db_dump \ + mosquitto_ctrl \ mosquitto_passwd .PHONY : all binary check clean reallyclean test install uninstall diff --git a/apps/mosquitto_ctrl/CMakeLists.txt b/apps/mosquitto_ctrl/CMakeLists.txt new file mode 100644 index 00000000..a7c19237 --- /dev/null +++ b/apps/mosquitto_ctrl/CMakeLists.txt @@ -0,0 +1,29 @@ +if (WITH_TLS AND CJSON_FOUND) + + include_directories(${mosquitto_SOURCE_DIR} ${mosquitto_SOURCE_DIR}/include + ${mosquitto_SOURCE_DIR}/lib ${mosquitto_SOURCE_DIR}/src + ${OPENSSL_INCLUDE_DIR} ${STDBOOL_H_PATH} ${STDINT_H_PATH} + ${CJSON_INCLUDE_DIRS}) + + add_executable(mosquitto_ctrl + mosquitto_ctrl.c mosquitto_ctrl.h + client.c + dynsec.c + dynsec_client.c + dynsec_group.c + dynsec_role.c + ../../lib/memory_mosq.c ../../lib/memory_mosq.h + ../../src/memory_public.c + options.c + ../../src/password_mosq.c ../../src/password_mosq.h + ) + + if (WITH_STATIC_LIBRARIES) + target_link_libraries(mosquitto_ctrl libmosquitto_static) + else() + target_link_libraries(mosquitto_ctrl libmosquitto) + endif() + target_link_libraries(mosquitto_ctrl ${OPENSSL_LIBRARIES} ${CJSON_LIBRARIES}) + + install(TARGETS mosquitto_ctrl RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") +endif (WITH_TLS AND CJSON_FOUND) diff --git a/apps/mosquitto_ctrl/Makefile b/apps/mosquitto_ctrl/Makefile new file mode 100644 index 00000000..ad87760b --- /dev/null +++ b/apps/mosquitto_ctrl/Makefile @@ -0,0 +1,79 @@ +include ../../config.mk + +.PHONY: all install uninstall clean reallyclean + +ifeq ($(WITH_SHARED_LIBRARIES),yes) +LIBMOSQ:=../../lib/libmosquitto.so.${SOVERSION} +else +LIBMOSQ:=../../lib/libmosquitto.a +endif + +LOCAL_CPPFLAGS:=-I/usr/include/cjson -I/usr/local/include/cjson + +OBJS= mosquitto_ctrl.o \ + client.o \ + dynsec.o \ + dynsec_client.o \ + dynsec_group.o \ + dynsec_role.o \ + memory_mosq.o \ + memory_public.o \ + options.o \ + password_mosq.o + +ifeq ($(WITH_TLS),yes) +ifeq ($(WITH_CJSON),yes) +TARGET:=mosquitto_ctrl +endif +endif + +all : $(TARGET) + +mosquitto_ctrl : ${OBJS} + ${CROSS_COMPILE}${CC} ${APP_LDFLAGS} $^ -o $@ $(PASSWD_LDADD) $(LIBMOSQ) -lcjson + +mosquitto_ctrl.o : mosquitto_ctrl.c mosquitto_ctrl.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +client.o : client.c mosquitto_ctrl.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +dynsec.o : dynsec.c mosquitto_ctrl.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +dynsec_client.o : dynsec_client.c mosquitto_ctrl.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +dynsec_group.o : dynsec_group.c mosquitto_ctrl.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +dynsec_role.o : dynsec_role.c mosquitto_ctrl.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +memory_mosq.o : ../../lib/memory_mosq.c + ${CROSS_COMPILE}${CC} $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +memory_public.o : ../../src/memory_public.c + ${CROSS_COMPILE}${CC} $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +options.o : options.c mosquitto_ctrl.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +misc_mosq.o : ../../lib/misc_mosq.c ../../lib/misc_mosq.h + ${CROSS_COMPILE}${CC} $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +password_mosq.o : ../../src/password_mosq.c ../../src/password_mosq.h + ${CROSS_COMPILE}${CC} $(APP_CPPFLAGS) $(APP_CFLAGS) -c $< -o $@ + +install : all + $(INSTALL) -d "${DESTDIR}$(prefix)/bin" + $(INSTALL) ${STRIP_OPTS} mosquitto_ctrl "${DESTDIR}${prefix}/bin/mosquitto_ctrl" + +uninstall : + -rm -f "${DESTDIR}${prefix}/bin/mosquitto_ctrl" + +clean : + -rm -f *.o mosquitto_ctrl *.gcda *.gcno + +reallyclean : clean + -rm -rf *.orig *.db diff --git a/apps/mosquitto_ctrl/client.c b/apps/mosquitto_ctrl/client.c new file mode 100644 index 00000000..391697b8 --- /dev/null +++ b/apps/mosquitto_ctrl/client.c @@ -0,0 +1,147 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include "mosquitto_ctrl.h" + +static int run = 1; + +static void on_message(struct mosquitto *mosq, void *obj, const struct mosquitto_message *msg, const mosquitto_property *properties) +{ + struct mosq_ctrl *ctrl = obj; + + if(ctrl->payload_callback){ + ctrl->payload_callback(ctrl, msg->payloadlen, msg->payload); + } + + mosquitto_disconnect_v5(mosq, 0, NULL); + run = 0; +} + + +static void on_publish(struct mosquitto *mosq, void *obj, int mid, int reason_code, const mosquitto_property *properties) +{ + if(reason_code > 127){ + fprintf(stderr, "Publish error: %s\n", mosquitto_reason_string(reason_code)); + run = 0; + mosquitto_disconnect_v5(mosq, 0, NULL); + } +} + + +static void on_subscribe(struct mosquitto *mosq, void *obj, int mid, int qos_count, const int *granted_qos, const mosquitto_property *properties) +{ + struct mosq_ctrl *ctrl = obj; + char *json_str; + + if(qos_count == 1){ + if(granted_qos[0] < 128){ + /* Success */ + json_str = cJSON_PrintUnformatted(ctrl->j_tree); + cJSON_Delete(ctrl->j_tree); + ctrl->j_tree = NULL; + if(json_str == NULL){ + fprintf(stderr, "Error: Out of memory.\n"); + run = 0; + mosquitto_disconnect_v5(mosq, 0, NULL); + } + mosquitto_publish(mosq, NULL, ctrl->request_topic, strlen(json_str), json_str, ctrl->cfg.qos, 0); + free(ctrl->request_topic); + ctrl->request_topic = NULL; + free(json_str); + }else{ + if(ctrl->cfg.protocol_version == MQTT_PROTOCOL_V5){ + fprintf(stderr, "Subscribe error: %s\n", mosquitto_reason_string(granted_qos[0])); + }else{ + fprintf(stderr, "Subscribe error: Subscription refused.\n"); + } + run = 0; + mosquitto_disconnect_v5(mosq, 0, NULL); + } + }else{ + run = 0; + mosquitto_disconnect_v5(mosq, 0, NULL); + } +} + + +static void on_connect(struct mosquitto *mosq, void *obj, int reason_code, int flags, const mosquitto_property *properties) +{ + struct mosq_ctrl *ctrl = obj; + + if(reason_code == 0){ + if(ctrl->response_topic){ + mosquitto_subscribe(mosq, NULL, ctrl->response_topic, ctrl->cfg.qos); + free(ctrl->response_topic); + ctrl->response_topic = NULL; + } + }else{ + if(ctrl->cfg.protocol_version == MQTT_PROTOCOL_V5){ + if(reason_code == MQTT_RC_UNSUPPORTED_PROTOCOL_VERSION){ + fprintf(stderr, "Connection error: %s. Try connecting to an MQTT v5 broker, or use MQTT v3.x mode.\n", mosquitto_reason_string(reason_code)); + }else{ + fprintf(stderr, "Connection error: %s\n", mosquitto_reason_string(reason_code)); + } + }else{ + fprintf(stderr, "Connection error: %s\n", mosquitto_connack_string(reason_code)); + } + run = 0; + mosquitto_disconnect_v5(mosq, 0, NULL); + } +} + + +int client_request_response(struct mosq_ctrl *ctrl) +{ + struct mosquitto *mosq; + int rc; + time_t start; + + mosquitto_lib_init(); + + mosq = mosquitto_new(ctrl->cfg.id, true, ctrl); + rc = client_opts_set(mosq, &ctrl->cfg); + if(rc) goto cleanup; + + mosquitto_connect_v5_callback_set(mosq, on_connect); + mosquitto_subscribe_v5_callback_set(mosq, on_subscribe); + mosquitto_publish_v5_callback_set(mosq, on_publish); + mosquitto_message_v5_callback_set(mosq, on_message); + + rc = client_connect(mosq, &ctrl->cfg); + if(rc) goto cleanup; + + start = time(NULL); + while(run && start+10 > time(NULL)){ + mosquitto_loop(mosq, -1, 1); + } + +cleanup: + mosquitto_destroy(mosq); + mosquitto_lib_cleanup(); + return rc; +} diff --git a/apps/mosquitto_ctrl/dynsec.c b/apps/mosquitto_ctrl/dynsec.c new file mode 100644 index 00000000..64a258d7 --- /dev/null +++ b/apps/mosquitto_ctrl/dynsec.c @@ -0,0 +1,505 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include +#include +#include +#include + +#include "mosquitto_ctrl.h" +#include "mosquitto.h" +#include "password_mosq.h" + +void dynsec__print_usage(void) +{ + printf("\nDynamic Security module\n"); + printf("=======================\n"); + printf("\nInitialisation\n--------------\n"); + printf("Create a new configuration file with an admin user:\n"); + printf(" mosquitto_ctrl dynsec init [admin-role]\n"); + + printf("\nGeneral\n-------\n"); + printf("Set ACL default access: setDefaultACLAccess allow|deny\n"); + printf("Set group for anonymous clients: setAnonymousGroup \n"); + + printf("\nClients\n-------\n"); + printf("Create a new client: createClient \n"); + printf("Delete a client: deleteClient \n"); + printf("Set a client password: setClientPassword \n"); + printf("Add a role to a client: addClientRole [priority]\n"); + printf(" Higher priority (larger numerical value) roles are evaluated first.\n"); + printf("Remove role from a client: removeClientRole \n"); + printf("Get client information: getClient \n"); + printf("List all clients: listClients [count [offset]]\n"); + + printf("\nGroups\n------\n"); + printf("Create a new group: createGroup \n"); + printf("Delete a group: deleteGroup \n"); + printf("Add a role to a group: addGroupRole [priority]\n"); + printf(" Higher priority (larger numerical value) roles are evaluated first.\n"); + printf("Remove role from a group: removeGroupRole \n"); + printf("Add client to a group: addGroupClient [priority]\n"); + printf(" Priority sets the group priority for the given client only.\n"); + printf(" Higher priority (larger numerical value) groups are evaluated first.\n"); + printf("Remove client from a group: removeGroupClient \n"); + printf("Get group information: getGroup \n"); + printf("List all groups: listGroups [count [offset]]\n"); + + printf("\nRoles\n------\n"); + printf("Create a new role: createRole \n"); + printf("Delete a role: deleteRole \n"); + printf("Add an ACL to a role: addRoleACL [priority]\n"); + printf(" Higher priority (larger numerical value) ACLs are evaluated first.\n"); + printf("Remove ACL from a role: removeRoleACL \n"); + printf("Get role information: getRole \n"); + printf("List all roles: listRoles [count [offset]]\n"); + printf("\naclspec: allow|deny\n"); + printf("acltype: publishClientToBroker|publishBrokerToClient\n"); + printf(" |subscribeLiteral|subscribePattern\n"); + printf(" |unsubscribeLiteral|unsubscribePattern\n"); +} + +/* ################################################################ + * # + * # Payload callback + * # + * ################################################################ */ + +static void print_list(cJSON *j_response, const char *arrayname, const char *keyname) +{ + cJSON *j_data, *j_array, *j_elem, *j_name; + + j_data = cJSON_GetObjectItem(j_response, "data"); + if(j_data == NULL) return; + + j_array = cJSON_GetObjectItem(j_data, arrayname); + if(j_array == NULL || !cJSON_IsArray(j_array)) return; + + cJSON_ArrayForEach(j_elem, j_array){ + if(cJSON_IsObject(j_elem)){ + j_name = cJSON_GetObjectItem(j_elem, keyname); + if(j_name && cJSON_IsString(j_name)){ + printf("%s\n", j_name->valuestring); + } + }else if(cJSON_IsString(j_elem)){ + printf("%s\n", j_elem->valuestring); + } + } +} + +static void dynsec__payload_callback(struct mosq_ctrl *ctrl, long payloadlen, const void *payload) +{ + cJSON *tree, *j_responses, *j_response, *j_command, *j_error; + + tree = cJSON_Parse(payload); + if(tree == NULL){ + fprintf(stderr, "Error: Payload not JSON.\n"); + return; + } + + j_responses = cJSON_GetObjectItem(tree, "responses"); + if(j_responses == NULL || !cJSON_IsArray(j_responses)){ + fprintf(stderr, "Error: Payload missing data.\n"); + cJSON_Delete(tree); + return; + } + + j_response = cJSON_GetArrayItem(j_responses, 0); + if(j_response == NULL){ + fprintf(stderr, "Error: Payload missing data.\n"); + cJSON_Delete(tree); + return; + } + + j_command = cJSON_GetObjectItem(j_response, "command"); + if(j_command == NULL){ + fprintf(stderr, "Error: Payload missing data.\n"); + cJSON_Delete(tree); + return; + } + + j_error = cJSON_GetObjectItem(j_response, "error"); + if(j_error){ + fprintf(stderr, "%s: Error: %s\n", j_command->valuestring, j_error->valuestring); + }else{ + if(!strcasecmp(j_command->valuestring, "listClients")){ + print_list(j_response, "clients", "username"); + }else if(!strcasecmp(j_command->valuestring, "listGroups")){ + print_list(j_response, "groups", "groupname"); + }else if(!strcasecmp(j_command->valuestring, "listRoles")){ + print_list(j_response, "roles", "rolename"); + }else{ + fprintf(stderr, "%s: Success\n", j_command->valuestring); + } + } + cJSON_Delete(tree); +} + +/* ################################################################ + * # + * # Default ACL access + * # + * ################################################################ */ + +static int dynsec__default_acl_access(int argc, char *argv[], cJSON *j_command) +{ + char *acltype, *access; + cJSON *j_acls, *j_acl; + + if(argc == 2){ + acltype = argv[0]; + access = argv[1]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(strcasecmp(acltype, "publishClientToBroker") + && strcasecmp(acltype, "publishBrokerToClient") + && strcasecmp(acltype, "subscribe") + && strcasecmp(acltype, "unsubscribe")){ + + return MOSQ_ERR_INVAL; + } + + if(strcasecmp(access, "allow") && strcasecmp(access, "deny")){ + fprintf(stderr, "Error: access must be \"allow\" or \"deny\".\n"); + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "setDefaultACLAccess") == NULL + || (j_acls = cJSON_AddArrayToObject(j_command, "acls")) == NULL + ){ + + return MOSQ_ERR_NOMEM; + } + + j_acl = cJSON_CreateObject(); + if(j_acl == NULL){ + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToArray(j_acls, j_acl); + if(cJSON_AddStringToObject(j_acl, "acltype", acltype) == NULL + || cJSON_AddStringToObject(j_acl, "access", access) == NULL + ){ + + return MOSQ_ERR_NOMEM; + } + + return MOSQ_ERR_SUCCESS; +} + +/* ################################################################ + * # + * # Init + * # + * ################################################################ */ + +static cJSON *init_add_acl_to_role(cJSON *j_acls, const char *type, const char *topic) +{ + cJSON *j_acl; + + j_acl = cJSON_CreateObject(); + if(j_acl == NULL) return NULL; + + if(cJSON_AddStringToObject(j_acl, "acltype", type) == NULL + || cJSON_AddStringToObject(j_acl, "topic", topic) == NULL + || cJSON_AddBoolToObject(j_acl, "allow", true) == NULL + ){ + + cJSON_Delete(j_acl); + return NULL; + } + cJSON_AddItemToArray(j_acls, j_acl); + return j_acl; +} + +static cJSON *init_add_role(const char *rolename) +{ + cJSON *j_role, *j_acls; + + j_role = cJSON_CreateObject(); + if(j_role == NULL){ + return NULL; + } + if(cJSON_AddStringToObject(j_role, "rolename", rolename) == NULL){ + cJSON_Delete(j_role); + return NULL; + } + + j_acls = cJSON_CreateArray(); + if(j_acls == NULL){ + cJSON_Delete(j_role); + return NULL; + } + cJSON_AddItemToObject(j_role, "acls", j_acls); + if(init_add_acl_to_role(j_acls, "publishClientToBroker", "$CONTROL/dynamic-security/#") == NULL + || init_add_acl_to_role(j_acls, "publishBrokerToClient", "$CONTROL/dynamic-security/#") == NULL + || init_add_acl_to_role(j_acls, "subscribePattern", "$CONTROL/dynamic-security/#") == NULL + || init_add_acl_to_role(j_acls, "unsubscribePattern", "#") == NULL + ){ + + cJSON_Delete(j_role); + return NULL; + } + return j_role; +} + +static cJSON *init_add_client(const char *username, const char *password, const char *rolename) +{ + cJSON *j_client, *j_roles, *j_role; + struct mosquitto_pw pw; + char *salt64 = NULL, *hash64 = NULL; + char buf[10]; + + memset(&pw, 0, sizeof(pw)); + pw.hashtype = pw_sha512_pbkdf2; + + if(pw__hash(password, &pw, true, PW_DEFAULT_ITERATIONS) != 0){ + return NULL; + } + if(base64__encode(pw.salt, sizeof(pw.salt), &salt64) + || base64__encode(pw.password_hash, sizeof(pw.password_hash), &hash64) + ){ + + fprintf(stderr, "dynsec init: Internal error while encoding password.\n"); + free(salt64); + free(hash64); + return NULL; + } + + j_client = cJSON_CreateObject(); + if(j_client == NULL) return NULL; + + snprintf(buf, sizeof(buf), "%d", PW_DEFAULT_ITERATIONS); + if(cJSON_AddStringToObject(j_client, "username", username) == NULL + || cJSON_AddStringToObject(j_client, "textName", "Dynsec admin user") == NULL + || cJSON_AddStringToObject(j_client, "password", hash64) == NULL + || cJSON_AddStringToObject(j_client, "salt", salt64) == NULL + || cJSON_AddRawToObject(j_client, "iterations", buf) == NULL + ){ + + free(salt64); + free(hash64); + cJSON_Delete(j_client); + return NULL; + } + free(salt64); + free(hash64); + + j_roles = cJSON_CreateArray(); + if(j_roles == NULL){ + cJSON_Delete(j_client); + return NULL; + } + cJSON_AddItemToObject(j_client, "roles", j_roles); + + j_role = cJSON_CreateObject(); + if(j_role == NULL){ + cJSON_Delete(j_client); + return NULL; + } + cJSON_AddItemToArray(j_roles, j_role); + if(cJSON_AddStringToObject(j_role, "rolename", rolename) == NULL){ + cJSON_Delete(j_client); + return NULL; + } + + return j_client; +} + +static cJSON *init_create(const char *username, const char *password, const char *rolename) +{ + cJSON *tree, *j_clients, *j_client, *j_roles, *j_role; + + tree = cJSON_CreateObject(); + if(tree == NULL) return NULL; + + if((j_clients = cJSON_AddArrayToObject(tree, "clients")) == NULL + || (j_roles = cJSON_AddArrayToObject(tree, "roles")) == NULL + ){ + + cJSON_Delete(tree); + return NULL; + } + + j_client = init_add_client(username, password, rolename); + if(j_client == NULL){ + cJSON_Delete(tree); + return NULL; + } + cJSON_AddItemToArray(j_clients, j_client); + + j_role = init_add_role(rolename); + if(j_role == NULL){ + cJSON_Delete(tree); + return NULL; + } + cJSON_AddItemToArray(j_roles, j_role); + + return tree; +} + +/* mosquitto_ctrl dynsec init [role-name] */ +int dynsec_init(int argc, char *argv[]) +{ + char *filename; + char *admin_user; + char *admin_password; + char *rolename = "admin"; + char *json_str; + cJSON *tree; + FILE *fptr; + + if(argc < 3){ + fprintf(stderr, "dynsec init: Not enough arguments - filename, admin-user or admin-password missing.\n"); + return MOSQ_ERR_INVAL; + } + + if(argc > 4){ + fprintf(stderr, "dynsec init: Too many arguments.\n"); + return MOSQ_ERR_INVAL; + } + + filename = argv[0]; + admin_user = argv[1]; + admin_password = argv[2]; + if(argc == 4){ + rolename = argv[3]; + } + + fptr = fopen(filename, "rb"); + if(fptr){ + fclose(fptr); + fprintf(stderr, "dynsec init: '%s' already exists. Use --force to overwrite.\n", filename); + return -1; + } + + tree = init_create(admin_user, admin_password, rolename); + if(tree == NULL){ + fprintf(stderr, "dynsec init: Out of memory.\n"); + return MOSQ_ERR_NOMEM; + } + json_str = cJSON_Print(tree); + cJSON_Delete(tree); + + fptr = fopen(filename, "wb"); + if(fptr){ + fprintf(fptr, "%s", json_str); + free(json_str); + }else{ + free(json_str); + fprintf(stderr, "dynsec init: Unable to open '%s' for writing.\n", filename); + return -1; + } + + return -1; /* Suppress client connection */ +} + +/* ################################################################ + * # + * # Main + * # + * ################################################################ */ + +int dynsec__main(int argc, char *argv[], struct mosq_ctrl *ctrl) +{ + cJSON *j_commands, *j_command; + if(!strcasecmp(argv[0], "help")){ + dynsec__print_usage(); + return -1; + }else if(!strcasecmp(argv[0], "init")){ + return dynsec_init(argc-1, &argv[1]); + } + + /* The remaining commands need a network connection and JSON command. */ + + ctrl->payload_callback = dynsec__payload_callback; + ctrl->request_topic = strdup("$CONTROL/dynamic-security/v1"); + ctrl->response_topic = strdup("$CONTROL/dynamic-security/v1/response"); + if(ctrl->request_topic == NULL || ctrl->response_topic == NULL){ + return MOSQ_ERR_NOMEM; + } + ctrl->j_tree = cJSON_CreateObject(); + if(ctrl->j_tree == NULL) return MOSQ_ERR_NOMEM; + j_commands = cJSON_AddArrayToObject(ctrl->j_tree, "commands"); + if(j_commands == NULL){ + cJSON_Delete(ctrl->j_tree); + ctrl->j_tree = NULL; + return MOSQ_ERR_NOMEM; + } + j_command = cJSON_CreateObject(); + if(j_command == NULL){ + cJSON_Delete(ctrl->j_tree); + ctrl->j_tree = NULL; + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToArray(j_commands, j_command); + + if(!strcasecmp(argv[0], "setDefaultACLAccess")){ + return dynsec__default_acl_access(argc-1, &argv[1], j_command); + + }else if(!strcasecmp(argv[0], "createClient")){ + return dynsec_client__create(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "deleteClient")){ + return dynsec_client__delete(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "getClient")){ + return dynsec_client__get(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "listClients")){ + return dynsec_client__list_all(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "setClientPassword")){ + return dynsec_client__set_password(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "addClientRole")){ + return dynsec_client__add_remove_role(argc-1, &argv[1], j_command, argv[0]); + }else if(!strcasecmp(argv[0], "removeClientRole")){ + return dynsec_client__add_remove_role(argc-1, &argv[1], j_command, argv[0]); + + }else if(!strcasecmp(argv[0], "createGroup")){ + return dynsec_group__create(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "deleteGroup")){ + return dynsec_group__delete(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "getGroup")){ + return dynsec_group__get(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "listGroups")){ + return dynsec_group__list_all(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "addGroupRole")){ + return dynsec_group__add_remove_role(argc-1, &argv[1], j_command, argv[0]); + }else if(!strcasecmp(argv[0], "removeGroupRole")){ + return dynsec_group__add_remove_role(argc-1, &argv[1], j_command, argv[0]); + }else if(!strcasecmp(argv[0], "addGroupClient")){ + return dynsec_group__add_remove_client(argc-1, &argv[1], j_command, argv[0]); + }else if(!strcasecmp(argv[0], "removeGroupClient")){ + return dynsec_group__add_remove_client(argc-1, &argv[1], j_command, argv[0]); + }else if(!strcasecmp(argv[0], "setAnonymousGroup")){ + return dynsec_group__set_anonymous(argc-1, &argv[1], j_command); + + }else if(!strcasecmp(argv[0], "createRole")){ + return dynsec_role__create(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "deleteRole")){ + return dynsec_role__delete(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "getRole")){ + return dynsec_role__get(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "listRoles")){ + return dynsec_role__list_all(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "addRoleACL")){ + return dynsec_role__add_acl(argc-1, &argv[1], j_command); + }else if(!strcasecmp(argv[0], "removeRoleACL")){ + return dynsec_role__remove_acl(argc-1, &argv[1], j_command); + + }else{ + fprintf(stderr, "Command '%s' not recognised.\n", argv[0]); + return MOSQ_ERR_UNKNOWN; + } + return 0; +} diff --git a/apps/mosquitto_ctrl/dynsec_client.c b/apps/mosquitto_ctrl/dynsec_client.c new file mode 100644 index 00000000..8df273fa --- /dev/null +++ b/apps/mosquitto_ctrl/dynsec_client.c @@ -0,0 +1,163 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include +#include +#include +#include + +#include "mosquitto.h" +#include "mosquitto_ctrl.h" +#include "password_mosq.h" + +int dynsec_client__create(int argc, char *argv[], cJSON *j_command) +{ + char *username = NULL, *password = NULL; + + if(argc == 1){ + username = argv[0]; + }else if(argc == 2){ + username = argv[0]; + password = argv[1]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "createClient") == NULL + || cJSON_AddStringToObject(j_command, "username", username) == NULL + || (password && cJSON_AddStringToObject(j_command, "password", password) == NULL) + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_client__delete(int argc, char *argv[], cJSON *j_command) +{ + char *username = NULL; + + if(argc == 1){ + username = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "deleteClient") == NULL + || cJSON_AddStringToObject(j_command, "username", username) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_client__set_password(int argc, char *argv[], cJSON *j_command) +{ + char *username = NULL, *password = NULL; + + if(argc == 2){ + username = argv[0]; + password = argv[1]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "setClientPassword") == NULL + || cJSON_AddStringToObject(j_command, "username", username) == NULL + || cJSON_AddStringToObject(j_command, "password", password) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_client__get(int argc, char *argv[], cJSON *j_command) +{ + char *username = NULL; + + if(argc == 1){ + username = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "getClient") == NULL + || cJSON_AddStringToObject(j_command, "username", username) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_client__add_remove_role(int argc, char *argv[], cJSON *j_command, const char *command) +{ + char *username = NULL, *rolename = NULL; + int priority = -1; + + if(argc == 2){ + username = argv[0]; + rolename = argv[1]; + }else if(argc == 3){ + username = argv[0]; + rolename = argv[1]; + priority = atoi(argv[2]); + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", command) == NULL + || cJSON_AddStringToObject(j_command, "username", username) == NULL + || cJSON_AddStringToObject(j_command, "rolename", rolename) == NULL + || (priority != -1 && cJSON_AddNumberToObject(j_command, "priority", priority) == NULL) + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_client__list_all(int argc, char *argv[], cJSON *j_command) +{ + int count = -1, offset = -1; + + if(argc == 0){ + /* All clients */ + }else if(argc == 1){ + count = atoi(argv[0]); + }else if(argc == 2){ + count = atoi(argv[0]); + offset = atoi(argv[1]); + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "listClients") == NULL + || (count > 0 && cJSON_AddNumberToObject(j_command, "count", count) == NULL) + || (offset > 0 && cJSON_AddNumberToObject(j_command, "offset", offset) == NULL) + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} diff --git a/apps/mosquitto_ctrl/dynsec_group.c b/apps/mosquitto_ctrl/dynsec_group.c new file mode 100644 index 00000000..2a9a9f27 --- /dev/null +++ b/apps/mosquitto_ctrl/dynsec_group.c @@ -0,0 +1,185 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include +#include +#include +#include + +#include "mosquitto.h" +#include "mosquitto_ctrl.h" +#include "password_mosq.h" + +int dynsec_group__create(int argc, char *argv[], cJSON *j_command) +{ + char *groupname = NULL; + + if(argc == 1){ + groupname = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "createGroup") == NULL + || cJSON_AddStringToObject(j_command, "groupname", groupname) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_group__delete(int argc, char *argv[], cJSON *j_command) +{ + char *groupname = NULL; + + if(argc == 1){ + groupname = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "deleteGroup") == NULL + || cJSON_AddStringToObject(j_command, "groupname", groupname) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_group__set_anonymous(int argc, char *argv[], cJSON *j_command) +{ + char *groupname = NULL; + + if(argc == 1){ + groupname = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "setAnonymousGroup") == NULL + || cJSON_AddStringToObject(j_command, "groupname", groupname) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_group__get(int argc, char *argv[], cJSON *j_command) +{ + char *groupname = NULL; + + if(argc == 1){ + groupname = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "getGroup") == NULL + || cJSON_AddStringToObject(j_command, "groupname", groupname) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_group__add_remove_role(int argc, char *argv[], cJSON *j_command, const char *command) +{ + char *groupname = NULL, *rolename = NULL; + int priority = -1; + + if(argc == 2){ + groupname = argv[0]; + rolename = argv[1]; + }else if(argc == 3){ + groupname = argv[0]; + rolename = argv[1]; + priority = atoi(argv[2]); + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", command) == NULL + || cJSON_AddStringToObject(j_command, "groupname", groupname) == NULL + || cJSON_AddStringToObject(j_command, "rolename", rolename) == NULL + || (priority != -1 && cJSON_AddNumberToObject(j_command, "priority", priority) == NULL) + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_group__list_all(int argc, char *argv[], cJSON *j_command) +{ + int count = -1, offset = -1; + + if(argc == 0){ + /* All groups */ + }else if(argc == 1){ + count = atoi(argv[0]); + }else if(argc == 2){ + count = atoi(argv[0]); + offset = atoi(argv[1]); + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "listGroups") == NULL + || (count > 0 && cJSON_AddNumberToObject(j_command, "count", count) == NULL) + || (offset > 0 && cJSON_AddNumberToObject(j_command, "offset", offset) == NULL) + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_group__add_remove_client(int argc, char *argv[], cJSON *j_command, const char *command) +{ + char *username, *groupname; + int priority = -1; + + if(argc == 2){ + username = argv[0]; + groupname = argv[1]; + }else if(argc == 3){ + username = argv[0]; + groupname = argv[1]; + priority = atoi(argv[2]); + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", command) == NULL + || cJSON_AddStringToObject(j_command, "username", username) == NULL + || cJSON_AddStringToObject(j_command, "groupname", groupname) == NULL + || (priority != -1 && cJSON_AddNumberToObject(j_command, "priority", priority) == NULL) + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} diff --git a/apps/mosquitto_ctrl/dynsec_role.c b/apps/mosquitto_ctrl/dynsec_role.c new file mode 100644 index 00000000..3b522381 --- /dev/null +++ b/apps/mosquitto_ctrl/dynsec_role.c @@ -0,0 +1,191 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#include +#include +#include +#include + +#include "mosquitto.h" +#include "mosquitto_ctrl.h" +#include "password_mosq.h" + +int dynsec_role__create(int argc, char *argv[], cJSON *j_command) +{ + char *rolename = NULL; + + if(argc == 1){ + rolename = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "createRole") == NULL + || cJSON_AddStringToObject(j_command, "rolename", rolename) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_role__delete(int argc, char *argv[], cJSON *j_command) +{ + char *rolename = NULL; + + if(argc == 1){ + rolename = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "deleteRole") == NULL + || cJSON_AddStringToObject(j_command, "rolename", rolename) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_role__get(int argc, char *argv[], cJSON *j_command) +{ + char *rolename = NULL; + + if(argc == 1){ + rolename = argv[0]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "getRole") == NULL + || cJSON_AddStringToObject(j_command, "rolename", rolename) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_role__list_all(int argc, char *argv[], cJSON *j_command) +{ + int count = -1, offset = -1; + + if(argc == 0){ + /* All roles */ + }else if(argc == 1){ + count = atoi(argv[0]); + }else if(argc == 2){ + count = atoi(argv[0]); + offset = atoi(argv[1]); + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "listRoles") == NULL + || (count > 0 && cJSON_AddNumberToObject(j_command, "count", count) == NULL) + || (offset > 0 && cJSON_AddNumberToObject(j_command, "offset", offset) == NULL) + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_role__add_acl(int argc, char *argv[], cJSON *j_command) +{ + char *rolename, *acltype, *topic, *action; + bool allow; + int priority = -1; + + if(argc == 5){ + rolename = argv[0]; + acltype = argv[1]; + topic = argv[2]; + action = argv[3]; + priority = atoi(argv[4]); + }else if(argc == 4){ + rolename = argv[0]; + acltype = argv[1]; + topic = argv[2]; + action = argv[3]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(strcasecmp(acltype, "publishClientToBroker") + && strcasecmp(acltype, "publishBrokerToClient") + && strcasecmp(acltype, "subscribe") + && strcasecmp(acltype, "unsubscribe")){ + + return MOSQ_ERR_INVAL; + } + if(!strcasecmp(action, "allow")){ + allow = true; + }else if(!strcasecmp(action, "deny")){ + allow = false; + }else{ + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "addACLToRole") == NULL + || cJSON_AddStringToObject(j_command, "rolename", rolename) == NULL + || cJSON_AddStringToObject(j_command, "acltype", acltype) == NULL + || cJSON_AddStringToObject(j_command, "topic", topic) == NULL + || cJSON_AddBoolToObject(j_command, "allow", allow) == NULL + || (priority != -1 && cJSON_AddNumberToObject(j_command, "priority", priority) == NULL) + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} + +int dynsec_role__remove_acl(int argc, char *argv[], cJSON *j_command) +{ + char *rolename, *acltype, *topic; + + if(argc == 3){ + rolename = argv[0]; + acltype = argv[1]; + topic = argv[2]; + }else{ + return MOSQ_ERR_INVAL; + } + + if(strcasecmp(acltype, "publishClientToBroker") + && strcasecmp(acltype, "publishBrokerToClient") + && strcasecmp(acltype, "subscribe") + && strcasecmp(acltype, "unsubscribe")){ + + return MOSQ_ERR_INVAL; + } + + if(cJSON_AddStringToObject(j_command, "command", "removeACLFromRole") == NULL + || cJSON_AddStringToObject(j_command, "rolename", rolename) == NULL + || cJSON_AddStringToObject(j_command, "acltype", acltype) == NULL + || cJSON_AddStringToObject(j_command, "topic", topic) == NULL + ){ + + return MOSQ_ERR_NOMEM; + }else{ + return MOSQ_ERR_SUCCESS; + } +} diff --git a/apps/mosquitto_ctrl/mosquitto_ctrl.c b/apps/mosquitto_ctrl/mosquitto_ctrl.c new file mode 100644 index 00000000..fe511456 --- /dev/null +++ b/apps/mosquitto_ctrl/mosquitto_ctrl.c @@ -0,0 +1,90 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "mosquitto.h" +#include "mosquitto_ctrl.h" + +void print_version(void) +{ + int major, minor, revision; + + mosquitto_lib_version(&major, &minor, &revision); + printf("mosquitto_ctrl version %s running on libmosquitto %d.%d.%d.\n", VERSION, major, minor, revision); +} + +void print_usage(void) +{ + printf("mosquitto_ctrl is a tool for administering certain Mosquitto features.\n"); + print_version(); + printf("\nGeneral usage: mosquitto_ctrl \n"); + printf("For module specific help use: mosquitto_ctrl help\n"); + printf("\nModules available: dynsec\n"); + printf("\nSee https://mosquitto.org/man/mosquitto_ctrl-1.html for more information.\n\n"); +} + + +int main(int argc, char *argv[]) +{ + struct mosq_ctrl ctrl; + int rc; + + if(argc == 1){ + print_usage(); + return 1; + } + + memset(&ctrl, 0, sizeof(ctrl)); + init_config(&ctrl.cfg); + + /* Shift program name out of args */ + argc--; + argv++; + + ctrl_config_parse(&ctrl.cfg, &argc, &argv); + + if(argc < 2){ + print_usage(); + return 1; + } + + if(!strcasecmp(argv[0], "dynsec")){ + rc = dynsec__main(argc-1, &argv[1], &ctrl); + if(rc < 0){ + /* Usage print */ + rc = 0; + }else if(rc == MOSQ_ERR_SUCCESS){ + rc = client_request_response(&ctrl); + }else if(rc == MOSQ_ERR_UNKNOWN){ + /* Message printed already */ + }else{ + fprintf(stderr, "Error: %s\n", mosquitto_strerror(rc)); + } + }else{ + fprintf(stderr, "Error: Module '%s' not supported.\n", argv[0]); + rc = MOSQ_ERR_NOT_SUPPORTED; + } + + client_config_cleanup(&ctrl.cfg); + return rc; +} diff --git a/apps/mosquitto_ctrl/mosquitto_ctrl.h b/apps/mosquitto_ctrl/mosquitto_ctrl.h new file mode 100644 index 00000000..fc7220b7 --- /dev/null +++ b/apps/mosquitto_ctrl/mosquitto_ctrl.h @@ -0,0 +1,109 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#ifndef MOSQUITTO_CTRL_H +#define MOSQUITTO_CTRL_H + +#include +#include + +#include "mosquitto.h" + +#define PORT_UNDEFINED -1 +#define PORT_UNIX 0 + +struct mosq_config { + char *id; + int protocol_version; + int keepalive; + char *host; + int port; + int qos; + char *bind_address; + bool debug; + bool quiet; + char *username; + char *password; + char *options_file; +#ifdef WITH_TLS + char *cafile; + char *capath; + char *certfile; + char *keyfile; + char *ciphers; + bool insecure; + char *tls_alpn; + char *tls_version; + 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 +}; + +struct mosq_ctrl { + struct mosq_config cfg; + char *request_topic; + char *response_topic; + cJSON *j_tree; + void (*payload_callback)(struct mosq_ctrl *, long , const void *); +}; + +void init_config(struct mosq_config *cfg); +int ctrl_config_parse(struct mosq_config *cfg, int *argc, char **argv[]); +int client_config_load(struct mosq_config *cfg); +void client_config_cleanup(struct mosq_config *cfg); + +int client_request_response(struct mosq_ctrl *ctrl); +int client_opts_set(struct mosquitto *mosq, struct mosq_config *cfg); +int client_connect(struct mosquitto *mosq, struct mosq_config *cfg); + +void dynsec__print_usage(void); +int dynsec__main(int argc, char *argv[], struct mosq_ctrl *ctrl); + +int dynsec_client__add_remove_role(int argc, char *argv[], cJSON *j_command, const char *command); +int dynsec_client__create(int argc, char *argv[], cJSON *j_command); +int dynsec_client__delete(int argc, char *argv[], cJSON *j_command); +int dynsec_client__get(int argc, char *argv[], cJSON *j_command); +int dynsec_client__list_all(int argc, char *argv[], cJSON *j_command); +int dynsec_client__set_password(int argc, char *argv[], cJSON *j_command); + +int dynsec_group__add_remove_client(int argc, char *argv[], cJSON *j_command, const char *command); +int dynsec_group__add_remove_role(int argc, char *argv[], cJSON *j_command, const char *command); +int dynsec_group__create(int argc, char *argv[], cJSON *j_command); +int dynsec_group__delete(int argc, char *argv[], cJSON *j_command); +int dynsec_group__get(int argc, char *argv[], cJSON *j_command); +int dynsec_group__list_all(int argc, char *argv[], cJSON *j_command); +int dynsec_group__set_anonymous(int argc, char *argv[], cJSON *j_command); + +int dynsec_role__create(int argc, char *argv[], cJSON *j_command); +int dynsec_role__delete(int argc, char *argv[], cJSON *j_command); +int dynsec_role__get(int argc, char *argv[], cJSON *j_command); +int dynsec_role__list_all(int argc, char *argv[], cJSON *j_command); +int dynsec_role__add_acl(int argc, char *argv[], cJSON *j_command); +int dynsec_role__remove_acl(int argc, char *argv[], cJSON *j_command); + +#endif diff --git a/apps/mosquitto_ctrl/options.c b/apps/mosquitto_ctrl/options.c new file mode 100644 index 00000000..42dd0195 --- /dev/null +++ b/apps/mosquitto_ctrl/options.c @@ -0,0 +1,898 @@ +/* +Copyright (c) 2014-2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include +#include +#include +#include +#ifndef WIN32 +#include +#include +#else +#include +#include +#define snprintf sprintf_s +#define strncasecmp _strnicmp +#endif + +#include +#include +#include "mosquitto_ctrl.h" + +#ifdef WITH_SOCKS +static int mosquitto__parse_socks_url(struct mosq_config *cfg, char *url); +#endif +static int client_config_line_proc(struct mosq_config *cfg, int *argc, char **argvp[]); + + +void init_config(struct mosq_config *cfg) +{ + cfg->qos = 1; + cfg->port = PORT_UNDEFINED; + cfg->protocol_version = MQTT_PROTOCOL_V5; +} + +void client_config_cleanup(struct mosq_config *cfg) +{ + free(cfg->id); + free(cfg->host); + free(cfg->bind_address); + free(cfg->username); + free(cfg->password); + free(cfg->options_file); +#ifdef WITH_TLS + free(cfg->cafile); + free(cfg->capath); + free(cfg->certfile); + free(cfg->keyfile); + free(cfg->ciphers); + free(cfg->tls_alpn); + free(cfg->tls_version); + free(cfg->tls_engine); + free(cfg->tls_engine_kpass_sha1); + free(cfg->keyform); +# ifdef FINAL_WITH_TLS_PSK + free(cfg->psk); + free(cfg->psk_identity); +# endif +#endif +#ifdef WITH_SOCKS + free(cfg->socks5_host); + free(cfg->socks5_username); + free(cfg->socks5_password); +#endif +} + +int ctrl_config_parse(struct mosq_config *cfg, int *argc, char **argv[]) +{ + int rc; + + init_config(cfg); + + /* Deal with real argc/argv */ + rc = client_config_line_proc(cfg, argc, argv); + if(rc) return rc; + + rc = client_config_load(cfg); + if(rc) return rc; + +#ifdef WITH_TLS + if((cfg->certfile && !cfg->keyfile) || (cfg->keyfile && !cfg->certfile)){ + fprintf(stderr, "Error: Both certfile and keyfile must be provided if one of them is set.\n"); + return 1; + } + if((cfg->keyform && !cfg->keyfile)){ + fprintf(stderr, "Error: If keyform is set, keyfile must be also specified.\n"); + return 1; + } + if((cfg->tls_engine_kpass_sha1 && (!cfg->keyform || !cfg->tls_engine))){ + fprintf(stderr, "Error: when using tls-engine-kpass-sha1, both tls-engine and keyform must also be provided.\n"); + return 1; + } +#endif +#ifdef FINAL_WITH_TLS_PSK + if((cfg->cafile || cfg->capath) && cfg->psk){ + fprintf(stderr, "Error: Only one of --psk or --cafile/--capath may be used at once.\n"); + return 1; + } + if(cfg->psk && !cfg->psk_identity){ + fprintf(stderr, "Error: --psk-identity required if --psk used.\n"); + return 1; + } +#endif + + if(!cfg->host){ + cfg->host = strdup("localhost"); + if(!cfg->host){ + fprintf(stderr, "Error: Out of memory.\n"); + return 1; + } + } + + return MOSQ_ERR_SUCCESS; +} + +/* Process a tokenised single line from a file or set of real argc/argv */ +static int client_config_line_proc(struct mosq_config *cfg, int *argc, char **argvp[]) +{ + char **argv = *argvp; + + while((*argc) && argv[0][0] == '-'){ + if(!strcmp(argv[0], "-A")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -A argument given but no address specified.\n\n"); + return 1; + }else{ + cfg->bind_address = strdup(argv[1]); + } + argv++; + (*argc)--; +#ifdef WITH_TLS + }else if(!strcmp(argv[0], "--cafile")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --cafile argument given but no file specified.\n\n"); + return 1; + }else{ + cfg->cafile = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--capath")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --capath argument given but no directory specified.\n\n"); + return 1; + }else{ + cfg->capath = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--cert")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --cert argument given but no file specified.\n\n"); + return 1; + }else{ + cfg->certfile = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--ciphers")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --ciphers argument given but no ciphers specified.\n\n"); + return 1; + }else{ + cfg->ciphers = strdup(argv[1]); + } + argv++; + (*argc)--; +#endif + }else if(!strcmp(argv[0], "-d") || !strcmp(argv[0], "--debug")){ + cfg->debug = true; + }else if(!strcmp(argv[0], "--help")){ + return 2; + }else if(!strcmp(argv[0], "-h") || !strcmp(argv[0], "--host")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -h argument given but no host specified.\n\n"); + return 1; + }else{ + cfg->host = strdup(argv[1]); + } + argv++; + (*argc)--; +#ifdef WITH_TLS + }else if(!strcmp(argv[0], "--insecure")){ + cfg->insecure = true; +#endif + }else if(!strcmp(argv[0], "-i") || !strcmp(argv[0], "--id")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -i argument given but no id specified.\n\n"); + return 1; + }else{ + cfg->id = strdup(argv[1]); + } + argv++; + (*argc)--; +#ifdef WITH_TLS + }else if(!strcmp(argv[0], "--key")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --key argument given but no file specified.\n\n"); + return 1; + }else{ + cfg->keyfile = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--keyform")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --keyform argument given but no keyform specified.\n\n"); + return 1; + }else{ + cfg->keyform = strdup(argv[1]); + } + argv++; + (*argc)--; +#endif + }else if(!strcmp(argv[0], "-L") || !strcmp(argv[0], "--url")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -L argument given but no URL specified.\n\n"); + return 1; + } else { + char *url = argv[1]; + char *topic; + char *tmp; + + if(!strncasecmp(url, "mqtt://", 7)) { + url += 7; + cfg->port = 1883; + } else if(!strncasecmp(url, "mqtts://", 8)) { + url += 8; + cfg->port = 8883; + } else { + fprintf(stderr, "Error: unsupported URL scheme.\n\n"); + return 1; + } + topic = strchr(url, '/'); + if(!topic){ + fprintf(stderr, "Error: Invalid URL for -L argument specified - topic missing.\n"); + return 1; + } + *topic++ = 0; + + tmp = strchr(url, '@'); + if(tmp) { + *tmp++ = 0; + char *colon = strchr(url, ':'); + if(colon) { + *colon = 0; + cfg->password = strdup(colon + 1); + } + cfg->username = strdup(url); + url = tmp; + } + cfg->host = url; + + tmp = strchr(url, ':'); + if(tmp) { + *tmp++ = 0; + cfg->port = atoi(tmp); + } + /* Now we've removed the port, time to get the host on the heap */ + cfg->host = strdup(cfg->host); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "-o")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -o argument given but no options file specified.\n\n"); + return 1; + }else{ + cfg->options_file = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "-p") || !strcmp(argv[0], "--port")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -p argument given but no port specified.\n\n"); + return 1; + }else{ + cfg->port = atoi(argv[1]); + if(cfg->port<0 || cfg->port>65535){ + fprintf(stderr, "Error: Invalid port given: %d\n", cfg->port); + return 1; + } + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "-P") || !strcmp(argv[0], "--pw")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -P argument given but no password specified.\n\n"); + return 1; + }else{ + cfg->password = strdup(argv[1]); + } + argv++; + (*argc)--; +#ifdef WITH_SOCKS + }else if(!strcmp(argv[0], "--proxy")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --proxy argument given but no proxy url specified.\n\n"); + return 1; + }else{ + if(mosquitto__parse_socks_url(cfg, argv[1])){ + return 1; + } + } + argv++; + (*argc)--; +#endif +#ifdef FINAL_WITH_TLS_PSK + }else if(!strcmp(argv[0], "--psk")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --psk argument given but no key specified.\n\n"); + return 1; + }else{ + cfg->psk = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--psk-identity")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --psk-identity argument given but no identity specified.\n\n"); + return 1; + }else{ + cfg->psk_identity = strdup(argv[1]); + } + argv++; + (*argc)--; +#endif + }else if(!strcmp(argv[0], "-q") || !strcmp(argv[0], "--qos")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -q argument given but no QoS specified.\n\n"); + return 1; + }else{ + cfg->qos = atoi(argv[1]); + if(cfg->qos<0 || cfg->qos>2){ + fprintf(stderr, "Error: Invalid QoS given: %d\n", cfg->qos); + return 1; + } + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--quiet")){ + cfg->quiet = true; +#ifdef WITH_TLS + }else if(!strcmp(argv[0], "--tls-alpn")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --tls-alpn argument given but no protocol specified.\n\n"); + return 1; + }else{ + cfg->tls_alpn = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--tls-engine")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --tls-engine argument given but no engine_id specified.\n\n"); + return 1; + }else{ + cfg->tls_engine = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--tls-engine-kpass-sha1")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --tls-engine-kpass-sha1 argument given but no kpass sha1 specified.\n\n"); + return 1; + }else{ + cfg->tls_engine_kpass_sha1 = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--tls-version")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --tls-version argument given but no version specified.\n\n"); + return 1; + }else{ + cfg->tls_version = strdup(argv[1]); + } + argv++; + (*argc)--; +#endif + }else if(!strcmp(argv[0], "-u") || !strcmp(argv[0], "--username")){ + if((*argc) == 1){ + fprintf(stderr, "Error: -u argument given but no username specified.\n\n"); + return 1; + }else{ + cfg->username = strdup(argv[1]); + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "--unix")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --unix argument given but no socket path specified.\n\n"); + return 1; + }else{ + cfg->host = strdup(argv[1]); + cfg->port = 0; + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "-V") || !strcmp(argv[0], "--protocol-version")){ + if((*argc) == 1){ + fprintf(stderr, "Error: --protocol-version argument given but no version specified.\n\n"); + return 1; + }else{ + if(!strcmp(argv[1], "mqttv31") || !strcmp(argv[1], "31")){ + cfg->protocol_version = MQTT_PROTOCOL_V31; + }else if(!strcmp(argv[1], "mqttv311") || !strcmp(argv[1], "311")){ + cfg->protocol_version = MQTT_PROTOCOL_V311; + }else if(!strcmp(argv[1], "mqttv5") || !strcmp(argv[1], "5")){ + cfg->protocol_version = MQTT_PROTOCOL_V5; + }else{ + fprintf(stderr, "Error: Invalid protocol version argument given.\n\n"); + return 1; + } + } + argv++; + (*argc)--; + }else if(!strcmp(argv[0], "-v") || !strcmp(argv[0], "--verbose")){ + cfg->verbose = 1; + }else if(!strcmp(argv[0], "--version")){ + return 3; + }else{ + goto unknown_option; + } + argv++; + (*argc)--; + } + *argvp = argv; + + return MOSQ_ERR_SUCCESS; + +unknown_option: + fprintf(stderr, "Error: Unknown option '%s'.\n",argv[0]); + return 1; +} + +static char *get_default_cfg_location(void) +{ + char *loc = NULL; + size_t len; +#ifndef WIN32 + char *env; +#else + char env[1024]; +#endif + +#ifndef WIN32 + env = getenv("XDG_CONFIG_HOME"); + if(env){ + len = strlen(env) + strlen("/mosquitto_ctrl") + 1; + loc = malloc(len); + if(!loc){ + fprintf(stderr, "Error: Out of memory.\n"); + return NULL; + } + snprintf(loc, len, "%s/mosquitto_ctrl", env); + loc[len-1] = '\0'; + }else{ + env = getenv("HOME"); + if(env){ + len = strlen(env) + strlen("/.config/mosquitto_ctrl") + 1; + loc = malloc(len); + if(!loc){ + fprintf(stderr, "Error: Out of memory.\n"); + return NULL; + } + snprintf(loc, len, "%s/.config/mosquitto_ctrl", env); + loc[len-1] = '\0'; + } + } + +#else + rc = GetEnvironmentVariable("USERPROFILE", env, 1024); + if(rc > 0 && rc < 1024){ + len = strlen(env) + strlen("\\mosquitto_ctrl.conf") + 1; + loc = malloc(len); + if(!loc){ + fprintf(stderr, "Error: Out of memory.\n"); + return NULL; + } + snprintf(loc, len, "%s\\mosquitto_ctrl.conf", env); + loc[len-1] = '\0'; + } +#endif + return loc; +} + +int client_config_load(struct mosq_config *cfg) +{ + int rc; + FILE *fptr = NULL; + char line[1024]; + int count; + char **local_args, **args; + char *default_cfg; + + if(cfg->options_file){ + fptr = fopen(cfg->options_file, "rt"); + }else{ + default_cfg = get_default_cfg_location(); + if(default_cfg){ + fptr = fopen(default_cfg, "rt"); + free(default_cfg); + } + } + + if(fptr){ + local_args = malloc(3*sizeof(char *)); + if(local_args == NULL){ + fprintf(stderr, "Error: Out of memory.\n"); + return 1; + } + while(fgets(line, 1024, fptr)){ + if(line[0] == '#') continue; /* Comments */ + + while(line[strlen(line)-1] == 10 || line[strlen(line)-1] == 13){ + line[strlen(line)-1] = 0; + } + local_args[0] = strtok(line, " "); + if(local_args[0]){ + local_args[1] = strtok(NULL, " "); + if(local_args[1]){ + count = 2; + }else{ + count = 1; + } + args = local_args; + rc = client_config_line_proc(cfg, &count, &args); + if(rc){ + fclose(fptr); + free(local_args); + return rc; + } + } + } + fclose(fptr); + free(local_args); + } + return 0; +} + + +int client_opts_set(struct mosquitto *mosq, struct mosq_config *cfg) +{ +#if defined(WITH_TLS) || defined(WITH_SOCKS) + int rc; +#endif + + mosquitto_int_option(mosq, MOSQ_OPT_PROTOCOL_VERSION, cfg->protocol_version); + + if((cfg->username || cfg->password) && mosquitto_username_pw_set(mosq, cfg->username, cfg->password)){ + fprintf(stderr, "Error: Problem setting username and/or password.\n"); + mosquitto_lib_cleanup(); + return 1; + } +#ifdef WITH_TLS + if(cfg->cafile || cfg->capath){ + rc = mosquitto_tls_set(mosq, cfg->cafile, cfg->capath, cfg->certfile, cfg->keyfile, NULL); + if(rc){ + if(rc == MOSQ_ERR_INVAL){ + fprintf(stderr, "Error: Problem setting TLS options: File not found.\n"); + }else{ + fprintf(stderr, "Error: Problem setting TLS options: %s.\n", mosquitto_strerror(rc)); + } + mosquitto_lib_cleanup(); + return 1; + } + } + if(cfg->insecure && mosquitto_tls_insecure_set(mosq, true)){ + fprintf(stderr, "Error: Problem setting TLS insecure option.\n"); + mosquitto_lib_cleanup(); + return 1; + } + if(cfg->tls_engine && mosquitto_string_option(mosq, MOSQ_OPT_TLS_ENGINE, cfg->tls_engine)){ + fprintf(stderr, "Error: Problem setting TLS engine, is %s a valid engine?\n", cfg->tls_engine); + mosquitto_lib_cleanup(); + return 1; + } + if(cfg->keyform && mosquitto_string_option(mosq, MOSQ_OPT_TLS_KEYFORM, cfg->keyform)){ + fprintf(stderr, "Error: Problem setting key form, it must be one of 'pem' or 'engine'.\n"); + mosquitto_lib_cleanup(); + return 1; + } + if(cfg->tls_engine_kpass_sha1 && mosquitto_string_option(mosq, MOSQ_OPT_TLS_ENGINE_KPASS_SHA1, cfg->tls_engine_kpass_sha1)){ + fprintf(stderr, "Error: Problem setting TLS engine key pass sha, is it a 40 character hex string?\n"); + mosquitto_lib_cleanup(); + return 1; + } + if(cfg->tls_alpn && mosquitto_string_option(mosq, MOSQ_OPT_TLS_ALPN, cfg->tls_alpn)){ + fprintf(stderr, "Error: Problem setting TLS ALPN protocol.\n"); + mosquitto_lib_cleanup(); + return 1; + } +# ifdef FINAL_WITH_TLS_PSK + if(cfg->psk && mosquitto_tls_psk_set(mosq, cfg->psk, cfg->psk_identity, NULL)){ + fprintf(stderr, "Error: Problem setting TLS-PSK options.\n"); + mosquitto_lib_cleanup(); + return 1; + } +# endif + if((cfg->tls_version || cfg->ciphers) && mosquitto_tls_opts_set(mosq, 1, cfg->tls_version, cfg->ciphers)){ + fprintf(stderr, "Error: Problem setting TLS options, check the options are valid.\n"); + mosquitto_lib_cleanup(); + return 1; + } +#endif +#ifdef WITH_SOCKS + if(cfg->socks5_host){ + rc = mosquitto_socks5_set(mosq, cfg->socks5_host, cfg->socks5_port, cfg->socks5_username, cfg->socks5_password); + if(rc){ + mosquitto_lib_cleanup(); + return rc; + } + } +#endif + return MOSQ_ERR_SUCCESS; +} + + +int client_connect(struct mosquitto *mosq, struct mosq_config *cfg) +{ +#ifndef WIN32 + char *err; +#else + char err[1024]; +#endif + int rc; + int port; + + if(cfg->port == PORT_UNDEFINED){ +#ifdef WITH_TLS + if(cfg->cafile || cfg->capath +# ifdef FINAL_WITH_TLS_PSK + || cfg->psk +# endif + ){ + port = 8883; + }else +#endif + { + port = 1883; + } + }else{ + port = cfg->port; + } + + rc = mosquitto_connect_bind_v5(mosq, cfg->host, port, 60, cfg->bind_address, NULL); + if(rc>0){ + if(rc == MOSQ_ERR_ERRNO){ +#ifndef WIN32 + err = strerror(errno); +#else + FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM, NULL, errno, 0, (LPTSTR)&err, 1024, NULL); +#endif + fprintf(stderr, "Error: %s\n", err); + }else{ + fprintf(stderr, "Unable to connect (%s).\n", mosquitto_strerror(rc)); + } + mosquitto_lib_cleanup(); + return rc; + } + return MOSQ_ERR_SUCCESS; +} + +#ifdef WITH_SOCKS +/* Convert %25 -> %, %3a, %3A -> :, %40 -> @ */ +static int mosquitto__urldecode(char *str) +{ + int i, j; + size_t len; + if(!str) return 0; + + if(!strchr(str, '%')) return 0; + + len = strlen(str); + for(i=0; i= len){ + return 1; + } + if(str[i+1] == '2' && str[i+2] == '5'){ + str[i] = '%'; + len -= 2; + for(j=i+1; j start){ + len = i-start; + if(host){ + /* Have already seen a @ , so this must be of form + * socks5h://username[:password]@host:port */ + port = malloc(len + 1); + if(!port){ + fprintf(stderr, "Error: Out of memory.\n"); + goto cleanup; + } + memcpy(port, &(str[start]), len); + port[len] = '\0'; + }else if(username_or_host){ + /* Haven't seen a @ before, so must be of form + * socks5h://host:port */ + host = username_or_host; + username_or_host = NULL; + port = malloc(len + 1); + if(!port){ + fprintf(stderr, "Error: Out of memory.\n"); + goto cleanup; + } + memcpy(port, &(str[start]), len); + port[len] = '\0'; + }else{ + host = malloc(len + 1); + if(!host){ + fprintf(stderr, "Error: Out of memory.\n"); + goto cleanup; + } + memcpy(host, &(str[start]), len); + host[len] = '\0'; + } + } + + if(!host){ + fprintf(stderr, "Error: Invalid proxy.\n"); + goto cleanup; + } + + if(mosquitto__urldecode(username)){ + goto cleanup; + } + if(mosquitto__urldecode(password)){ + goto cleanup; + } + if(port){ + port_int = atoi(port); + if(port_int < 1 || port_int > 65535){ + fprintf(stderr, "Error: Invalid proxy port %d\n", port_int); + goto cleanup; + } + free(port); + }else{ + port_int = 1080; + } + + cfg->socks5_username = username; + cfg->socks5_password = password; + cfg->socks5_host = host; + cfg->socks5_port = port_int; + + return 0; +cleanup: + free(username_or_host); + free(username); + free(password); + free(host); + free(port); + return 1; +} +#endif diff --git a/client/CMakeLists.txt b/client/CMakeLists.txt index 08506f31..62dfd3c8 100644 --- a/client/CMakeLists.txt +++ b/client/CMakeLists.txt @@ -1,5 +1,3 @@ -FIND_PACKAGE(cJSON) - set(shared_src client_shared.c client_shared.h client_props.c) if (WITH_SRV) @@ -13,10 +11,8 @@ set( CLIENT_INC ${mosquitto_SOURCE_DIR} ${mosquitto_SOURCE_DIR}/include set( CLIENT_DIR ${mosquitto_BINARY_DIR}/lib) if (CJSON_FOUND) - message(STATUS ${CJSON_FOUND}) set( CLIENT_DIR "${CLIENT_DIR} ${CJSON_DIR}" ) set( CLIENT_INC "${CLIENT_INC};${CJSON_INCLUDE_DIRS}" ) - add_definitions("-DWITH_CJSON") endif() include_directories(${CLIENT_INC}) diff --git a/client/args.txt b/client/args.txt index e9f2a126..edf88af7 100644 --- a/client/args.txt +++ b/client/args.txt @@ -27,7 +27,7 @@ m - PUB,RR (message input) N - RR,SUB (no end of line) n - PUB,RR (null message) O -o +o - CTRL (options file) P - PUB,RR,SUB (password) p - PUB,RR,SUB (port) Q diff --git a/cmake/FindcJSON.cmake b/cmake/FindcJSON.cmake index e4d2b732..9ec113a2 100644 --- a/cmake/FindcJSON.cmake +++ b/cmake/FindcJSON.cmake @@ -12,6 +12,7 @@ FIND_PATH( HINTS CJSON_DIR /usr/include/cjson + /usr/local/include/cjson ) FIND_LIBRARY( CJSON_LIBRARY diff --git a/config.mk b/config.mk index 8a03b8f1..05b1a898 100644 --- a/config.mk +++ b/config.mk @@ -145,21 +145,19 @@ endif STATIC_LIB_DEPS:= APP_CPPFLAGS=$(CPPFLAGS) -I. -I../../ -I../../include -I../../src -I../../lib -APP_CFLAGS=$(CFLAGS) +APP_CFLAGS=$(CFLAGS) -DVERSION=\""${VERSION}\"" +APP_LDFLAGS:=$(LDFLAGS) LIB_CPPFLAGS=$(CPPFLAGS) -I. -I.. -I../include -I../../include -ifeq ($(WITH_BUNDLED_DEPS),yes) - LIB_CPPFLAGS:=$(LIB_CPPFLAGS) -I../deps -endif LIB_CFLAGS:=$(CFLAGS) LIB_CXXFLAGS:=$(CXXFLAGS) LIB_LDFLAGS:=$(LDFLAGS) LIB_LIBADD:=$(LIBADD) -BROKER_CPPFLAGS:=$(LIB_CPPFLAGS) -I../lib -I/usr/include/cjson -I/usr/local/include/cjson +BROKER_CPPFLAGS:=$(LIB_CPPFLAGS) -I../lib BROKER_CFLAGS:=${CFLAGS} -DVERSION="\"${VERSION}\"" -DWITH_BROKER BROKER_LDFLAGS:=${LDFLAGS} -BROKER_LDADD:=-lcjson +BROKER_LDADD:= CLIENT_CPPFLAGS:=$(CPPFLAGS) -I.. -I../include CLIENT_CFLAGS:=${CFLAGS} -DVERSION="\"${VERSION}\"" @@ -169,6 +167,8 @@ CLIENT_LDADD:= PASSWD_LDADD:= PLUGIN_CPPFLAGS:=$(CPPFLAGS) -I../.. -I../../include +PLUGIN_CFLAGS:=$(CFLAGS) -fPIC +PLUGIN_LDFLAGS:=$(LDFLAGS) ifneq ($(or $(findstring $(UNAME),FreeBSD), $(findstring $(UNAME),OpenBSD), $(findstring $(UNAME),NetBSD)),) BROKER_LDADD:=$(BROKER_LDADD) -lm @@ -337,6 +337,8 @@ endif ifeq ($(WITH_BUNDLED_DEPS),yes) BROKER_CPPFLAGS:=$(BROKER_CPPFLAGS) -I../deps + LIB_CPPFLAGS:=$(LIB_CPPFLAGS) -I../deps + PLUGIN_CPPFLAGS:=$(PLUGIN_CPPFLAGS) -I../../deps endif ifeq ($(WITH_COVERAGE),yes) diff --git a/docker/local/Dockerfile b/docker/local/Dockerfile index 18913daa..90b705e7 100644 --- a/docker/local/Dockerfile +++ b/docker/local/Dockerfile @@ -3,7 +3,8 @@ FROM alpine:3.12 LABEL maintainer="Roger Light " \ description="Eclipse Mosquitto MQTT Broker" -ENV LWS_VERSION=2.4.2 +ENV LWS_VERSION=2.4.2 \ + CJSON_VERSION=1.7.14 COPY mosq.tar.gz /tmp @@ -32,13 +33,20 @@ RUN set -x && \ -DLWS_WITH_ZLIB=OFF && \ make -j "$(nproc)" && \ rm -rf /root/.cmake && \ + wget https://github.com/DaveGamble/cJSON/archive/v${CJSON_VERSION}.tar.gz -O /tmp/cjson.tar.gz && \ + mkdir -p /build/cjson && \ + tar --strip=1 -xf /tmp/cjson.tar.gz -C /build/cjson && \ + rm /tmp/cjson.tar.gz && \ + cd /build/cjson && \ + make -j "$(nproc)" libcjson.a && \ mkdir -p /build/mosq && \ tar --strip=1 -xf /tmp/mosq.tar.gz -C /build/mosq && \ rm /tmp/mosq.tar.gz && \ make -C /build/mosq -j "$(nproc)" \ - CFLAGS="-Wall -O2 -I/build/lws/include" \ - LDFLAGS="-L/build/lws/lib" \ + CFLAGS="-Wall -O2 -I/build/lws/include -I/build/cjson" \ + LDFLAGS="-L/build/lws/lib -L/build/cjson" \ WITH_ADNS=no \ + WITH_CJSON=yes \ WITH_DOCS=no \ WITH_SHARED_LIBRARIES=yes \ WITH_SRV=no \ @@ -55,7 +63,9 @@ RUN set -x && \ install -s -m755 /build/mosq/client/mosquitto_sub /usr/bin/mosquitto_sub && \ install -s -m644 /build/mosq/lib/libmosquitto.so.1 /usr/lib/libmosquitto.so.1 && \ install -s -m755 /build/mosq/src/mosquitto /usr/sbin/mosquitto && \ - install -s -m755 /build/mosq/src/mosquitto_passwd /usr/bin/mosquitto_passwd && \ + install -s -m755 /build/mosq/apps/mosquitto_passwd/mosquitto_passwd /usr/bin/mosquitto_passwd && \ + install -s -m755 /build/mosq/apps/mosquitto_ctrl/mosquitto_ctrl /usr/bin/mosquitto_ctrl && \ + install -s -m755 /build/mosq/plugins/dynamic-security/mosquitto_dynamic_security.so /usr/lib/mosquitto_dynamic_security.so && \ install -m644 /build/mosq/mosquitto.conf /mosquitto/config/mosquitto.conf && \ chown -R mosquitto:mosquitto /mosquitto && \ apk --no-cache add \ diff --git a/man/Makefile b/man/Makefile index 94aff2c5..c02736f1 100644 --- a/man/Makefile +++ b/man/Makefile @@ -7,6 +7,7 @@ MANPAGES = \ mosquitto-tls.7 \ mosquitto.8 \ mosquitto.conf.5 \ + mosquitto_ctrl.1 \ mosquitto_passwd.1 \ mosquitto_pub.1 \ mosquitto_rr.1 \ @@ -29,6 +30,7 @@ install : $(INSTALL) -d "${DESTDIR}$(mandir)/man5" $(INSTALL) -m 644 mosquitto.conf.5 "${DESTDIR}${mandir}/man5/mosquitto.conf.5" $(INSTALL) -d "${DESTDIR}$(mandir)/man1" + $(INSTALL) -m 644 mosquitto_ctrl.1 "${DESTDIR}${mandir}/man1/mosquitto_ctrl.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" @@ -42,6 +44,7 @@ install : uninstall : -rm -f "${DESTDIR}${mandir}/man8/mosquitto.8" -rm -f "${DESTDIR}${mandir}/man5/mosquitto.conf.5" + -rm -f "${DESTDIR}${mandir}/man1/mosquitto_ctrl.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" @@ -56,6 +59,9 @@ mosquitto.8 : mosquitto.8.xml manpage.xsl mosquitto.conf.5 : mosquitto.conf.5.xml manpage.xsl $(XSLTPROC) $< +mosquitto_ctrl.1 : mosquitto_ctrl.1.xml manpage.xsl + $(XSLTPROC) $< + mosquitto_passwd.1 : mosquitto_passwd.1.xml manpage.xsl $(XSLTPROC) $< @@ -87,6 +93,7 @@ html : *.xml potgen : xml2po -o po/mosquitto/mosquitto.8.pot mosquitto.8.xml 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_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.8.xml b/man/mosquitto.8.xml index 5238e0c0..e00bdd6d 100644 --- a/man/mosquitto.8.xml +++ b/man/mosquitto.8.xml @@ -511,6 +511,12 @@ 5 + + + mosquitto_ctrl + 1 + + mosquitto_passwd diff --git a/man/mosquitto_ctrl.1.meta b/man/mosquitto_ctrl.1.meta new file mode 100644 index 00000000..0d173e91 --- /dev/null +++ b/man/mosquitto_ctrl.1.meta @@ -0,0 +1,5 @@ +.. title: mosquitto_ctrl man page +.. slug: mosquitto_ctrl-1 +.. category: man +.. type: man +.. pretty_url: False diff --git a/man/mosquitto_ctrl.1.xml b/man/mosquitto_ctrl.1.xml new file mode 100644 index 00000000..9f519ba0 --- /dev/null +++ b/man/mosquitto_ctrl.1.xml @@ -0,0 +1,638 @@ + + + + + + mosquitto_ctrl + 1 + Mosquitto Project + Commands + + + + mosquitto_ctrl + a tool for initialising/configuring a Mosquitto broker instance + + + + + mosquitto_ctrl + connection-options + module + module-command + command-options + + + mosquitto_ctrl + + + hostname + socket path + port-number + username + password + + URL + + bind-address + + + client-id + message-QoS + + protocol-version + + + + file + dir + + file + file + ciphers + version + protocol + engine + + + pem + engine + + kpass-sha1 + + + + hex-key + identity + ciphers + version + + + socks-url + + + mosquitto_ctrl + + + + + + + + Description + mosquitto_ctrl is a simple MQTT version 5/3.1.1 + client that will publish a single message on a topic and + exit. + + + + 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 when using x509 certificates, one of + either or must + be provided as an option. + To enable TLS connections when using TLS-PSK, you must use the + and the + options. + + + + Dynamic Security Module + The dynamic security plugin is an optional authentication and + access control plugin for Mosquitto that allows on the fly + control of users, groups, and roles. The mosquitto_ctrl + dynsec module allows you to interact with + a broker using the dynamic security plugin. + + + Configuration file initialisation + + + mosquitto_ctrl dynsec init filename admin-user admin-password [admin-role] + + + + + + Connection Options + The options below may be given on the command line, but may also + be placed in a config file located at + or + with one pair of + + per line. The values in the config file will be used as defaults + and can be overridden by using the command line. The exceptions to + this are the message type options, of which only one can be + specified. Note also that currently some options cannot be negated, + e.g. . Config file lines that have a + as the first character are treated as comments + and not processed any further. + + + + + Bind the outgoing connection to a local ip + address/hostname. Use this argument if you need to + restrict network communication to a particular + interface. + + + + + + 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 . + + + + + + An openssl compatible list of TLS ciphers to support + in the client. See + ciphers1 + for more information. + + + + + + + Enable debug messages. + + + + + + + Use an MQTT v5 property with this publish. If you use + this option, the client will be set to be an MQTT v5 + client. This option has two forms: + + + is the MQTT command/packet + identifier and can be one of CONNECT, PUBLISH, PUBREL, + DISCONNECT, AUTH, or WILL. The properties available for + each command are listed in the + Properties + section. + + is the name of the + property to add. This is as described in the + specification, but with '-' as a word separator. For + example: + . More details + are in the Properties + section. + + is the value of the property + to add, with a data type that is property + specific. + + is only used for the + property as the first of + the two strings in the string pair. In that case, + is the second of the strings in + the pair. + + + + + + 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. + + This option cannot be used at the same time as the + argument. + + + + + + When using certificate based encryption, this option + disables verification of the server hostname in the + server certificate. This can be useful when testing + initial server configurations but makes it possible for + a malicious third party to impersonate your server + through DNS spoofing, for example. Use this option in + testing only. If you need to + resort to using this option in a production + environment, your setup is at fault and there is no + point using encryption. + + + + + + Define the path to a file containing a PEM encoded + private key for this client, if required by the + server. + See also . + + + + + + Specifies the type of private key in use when making + TLS connections.. This can be "pem" or "engine". This + parameter is useful when a TPM module is being used and + the private key has been created with it. Defaults to + "pem", which means normal private key files are + used. + See also . + + + + + + + Specify specify user, password, hostname, port and + topic at once as a URL. The URL must be in the form: + mqtt(s)://[username[:password]@]host[:port]/topic + If the scheme is mqtt:// then the port defaults to + 1883. If the scheme is mqtts:// then the port defaults + to 8883. + + + + + + Disable Nagle's algorithm for the socket. This means + that latency of sent messages is reduced, which is + particularly noticable for small, reasonably infrequent + messages. Using this option may result in more packets + being sent than would normally be necessary. + + + + + + + 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. + + + + + + Specify a SOCKS5 proxy to connect through. "None" and + "username" authentication types are supported. The + must be of the form + . + The protocol prefix means that + hostnames are resolved by the proxy. The symbols %25, + %3A and %40 are URL decoded into %, : and @ + respectively, if present in the username or + password. + If username is not given, then no authentication is + attempted. If the port is not given, then the default + of 1080 is used. + More SOCKS versions may be available in the future, + depending on demand, and will use different protocol + prefixes as described in + curl + 1 . + + + + + + Provide the hexadecimal (no leading 0x) + pre-shared-key matching the one used on the broker to + use TLS-PSK encryption support. + must also be provided + to enable TLS-PSK. + + + + + + The client identity to use with TLS-PSK support. This + may be used instead of a username if the broker is + configured to do so. + + + + + + + Specify the quality of service to use for messages, from 0, 1 and 2. Defaults to 1. + + + + + + If this argument is given, no runtime errors will be + printed. This excludes any error messages given in case of + invalid user input (e.g. using without a + port). + + + + + + Provide a protocol to use when connecting to a broker + that has multiple protocols available on a single port, + e.g. MQTT and WebSockets. + + + + + + A valid openssl engine id. These can be listed with + openssl engine command. + See also . + + + + + + SHA1 of the private key password when using an TLS + engine. Some TLS engines such as the TPM engine may + require the use of a password in order to be accessed. + This option allows a hex encoded SHA1 hash of the + password to the engine directly, instead of the user + being prompted for the password. + See also . + + + + + + Choose which TLS protocol version to use when + communicating with the broker. Valid options are + , and + . The default value is + . Must match the protocol + version used by the broker. + + + + + + + Provide a username to be used for authenticating with + the broker. See also the + argument. + + + + + + Connect to a broker through a local unix domain socket + instead of a TCP socket. This is a replacement for + and . For example: + + + See the option in + + mosquitto.conf + 5 + to configure Mosquitto to listen on a unix socket. + + + + + + + Specify which version of the MQTT protocol should be + used when connecting to the rmeote broker. Can be + , , + , or the more verbose + , , or + . + Defaults to . + + + + + + + Properties + The / option + allows adding properties to different stages of the mosquitto_ctrl + run. The properties supported for each command are as + follows: + + + Connect + + (binary data - note treated as a string in mosquitto_ctrl) + (UTF-8 string pair) + (32-bit unsigned integer) + (16-bit unsigned integer) + (8-bit unsigned integer) + (8-bit unsigned integer) + (32-bit unsigned integer, note use instead) + (16-bit unsigned integer) + (UTF-8 string pair) + + + + + Publish + + (UTF-8 string) + (binary data - note treated as a string in mosquitto_ctrl) + (32-bit unsigned integer) + (8-bit unsigned integer) + (UTF-8 string) + (16-bit unsigned integer) + (UTF-8 string pair) + + + + + Disconnect + + (32-bit unsigned integer) + (UTF-8 string pair) + + + + + Will properties + + (UTF-8 string) + (binary data - note treated as a string in mosquitto_ctrl) + (32-bit unsigned integer) + (8-bit unsigned integer) + (UTF-8 string) + (UTF-8 string pair) + (32-bit unsigned integer) + + + + + + Exit Status + + mosquitto_sub returns zero on success, or non-zero on error. If + the connection is refused by the broker at the MQTT level, then + the exit code is the CONNACK reason code. If another error + occurs, the exit code is a libmosquitto return value. + + + MQTT v3.1.1 CONNACK codes: + + Success + Connection refused: Bad protocol version + Connection refused: Identifier rejected + Connection refused: Server unavailable + Connection refused: Bad username/password + Connection refused: Not authorized + + + MQTT v5 CONNACK codes: + + Success + Unspecified error + Malformed packet + Protocol error + Implementation specific error + Unsupported protocol version + Client ID not valid + Bad username or password + Not authorized + Server unavailable + Server busy + Banned + Server shutting down + Bad authentication method + Keep alive timeout + Session taken over + Topic filter invalid + Topic name invalid + Receive maximum exceeded + Topic alias invalid + Packet too large + Message rate too high + Quota exceeded + Administrative action + Payload format invalid + Retain not supported + QoS not supported + Use another server + Server moved + Shared subscriptions not supported + Connection rate exceeded + Maximum connect time + Subscription IDs not supported + Wildcard subscriptions not supported + + + + + Bugs + mosquitto bug information can be found at + + + + + See Also + + + + mqtt + 7 + + + + + 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/CMakeLists.txt b/plugins/CMakeLists.txt index c50372d6..5045a4fe 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -1,2 +1,3 @@ +add_subdirectory(dynamic-security) add_subdirectory(message-timestamp) add_subdirectory(payload-modification) diff --git a/plugins/Makefile b/plugins/Makefile index c9ca7256..5e8745ac 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -1,4 +1,5 @@ DIRS= \ + dynamic-security \ message-timestamp \ payload-modification diff --git a/plugins/README.md b/plugins/README.md index ec1df0cb..3053eb90 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -2,6 +2,11 @@ This directory contains plugins for use with Mosquitto. +## Dynamic security +This is a fully functioning plugin that implements authentication and access +control, with configuration via a $CONTROL topic. See the readme in +dynamic-security for more information. + ## Message timestamp This is an **example** plugin to demonstrate how it is possible to attach MQTT v5 properties to messages after they have been received, and before they are sent on to subscribers. @@ -12,4 +17,4 @@ This is an **example** plugin to demonstrate how it is possible to modify the pa If you are considering using this feature, you should be very certain you have verified the payload is the correct format before modifying it. -This plugin adds the text string "hello " to the beginning of each payload, so with anything other than simple plain text messages it will corrupt the payload contents. \ No newline at end of file +This plugin adds the text string "hello " to the beginning of each payload, so with anything other than simple plain text messages it will corrupt the payload contents. diff --git a/plugins/dynamic-security/CMakeLists.txt b/plugins/dynamic-security/CMakeLists.txt new file mode 100644 index 00000000..9049c1d0 --- /dev/null +++ b/plugins/dynamic-security/CMakeLists.txt @@ -0,0 +1,36 @@ +set( CLIENT_INC ${mosquitto_SOURCE_DIR} ${mosquitto_SOURCE_DIR}/include + ${STDBOOL_H_PATH} ${STDINT_H_PATH} ${PTHREAD_INCLUDE_DIR} + ${OPENSSL_INCLUDE_DIR}) + +set( CLIENT_DIR ${mosquitto_BINARY_DIR}/lib) + +if (CJSON_FOUND) + set( CLIENT_DIR "${CLIENT_DIR} ${CJSON_DIR}" ) + set( CLIENT_INC "${CLIENT_INC};${CJSON_INCLUDE_DIRS}" ) +endif() + +include_directories(${CLIENT_INC}) +link_directories(${CLIENT_DIR}) + +add_library(mosquitto_dynamic_security SHARED + acl.c + auth.c + clients.c + dynamic_security.h + groups.c + json_help.c + json_help.h + plugin.c + roles.c + sub_matches_sub.c) + +set_target_properties(mosquitto_dynamic_security PROPERTIES + POSITION_INDEPENDENT_CODE 1 +) +set_target_properties(mosquitto_dynamic_security PROPERTIES PREFIX "") + +if (CJSON_FOUND) + target_link_libraries(mosquitto_dynamic_security ${CJSON_LIBRARIES}) +endif() + +install(TARGETS mosquitto_dynamic_security RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") diff --git a/plugins/dynamic-security/Makefile b/plugins/dynamic-security/Makefile new file mode 100644 index 00000000..45099c6b --- /dev/null +++ b/plugins/dynamic-security/Makefile @@ -0,0 +1,60 @@ +include ../../config.mk + +.PHONY : all binary check clean reallyclean test install uninstall + +PLUGIN_NAME=mosquitto_dynamic_security +LOCAL_CPPFLAGS=-I/usr/include/cjson -I/usr/local/include/cjson -I../../src/ + +OBJS= \ + acl.o \ + auth.o \ + clients.o \ + groups.o \ + json_help.o \ + plugin.o \ + roles.o \ + sub_matches_sub.o + +all : binary +binary : ${PLUGIN_NAME}.so + +${PLUGIN_NAME}.so : ${OBJS} + ${CROSS_COMPILE}${CC} $(PLUGIN_LDFLAGS) -fPIC -shared $^ -o $@ -lcjson + +acl.o : acl.c dynamic_security.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) -c $< -o $@ + +auth.o : auth.c dynamic_security.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) -c $< -o $@ + +clients.o : clients.c dynamic_security.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) -c $< -o $@ + +groups.o : groups.c dynamic_security.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) -c $< -o $@ + +json_help.o : json_help.c dynamic_security.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) -c $< -o $@ + +plugin.o : plugin.c dynamic_security.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) -c $< -o $@ + +roles.o : roles.c dynamic_security.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) -c $< -o $@ + +sub_matches_sub.o : sub_matches_sub.c dynamic_security.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) -c $< -o $@ + +reallyclean : clean +clean: + -rm -f *.o ${PLUGIN_NAME}.so *.gcda *.gcno + +check: test +test: + +install: ${PLUGIN_NAME}.so + $(INSTALL) -d "${DESTDIR}$(prefix)/lib" + $(INSTALL) ${STRIP_OPTS} ${PLUGIN_NAME}.so "${DESTDIR}${prefix}/lib/${PLUGIN_NAME}.so" + +uninstall : + -rm -f "${DESTDIR}${prefix}/lib/${PLUGIN_NAME}.so" diff --git a/plugins/dynamic-security/README.md b/plugins/dynamic-security/README.md new file mode 100644 index 00000000..3e2af8bb --- /dev/null +++ b/plugins/dynamic-security/README.md @@ -0,0 +1,579 @@ +# Mosquitto Dynamic Security + +This document describes a topic based mechanism for controlling security in +Mosquitto. JSON commands are published to topics like `$CONTROL//v1` + +## Clients + +When a client connects to Mosquitto, it can optionally provide a username. The +username maps the client instance to a client on the broker, if it exists. +Multiple clients can make use of the same username, and hence the same broker +client. + +## Groups + +Broker clients can be defined as belonging to zero or more broker groups. + +## Roles + +Roles can be applied to a client or a group, and define what that client/group +is allowed to do, for example what topics it may or may not publish or +subscribe to. + +## Commands + +### Set default ACL access + +Sets the default access behaviour for the different ACL types, assuming there +are no matching ACLs for a topic. + +By default, all ACL types default to deny. + +Command: +``` +{ + "commands":[ + { + "command": "setDefaultACLAccess", + "acls":[ + { "acltype": "publishClientToBroker", "allow": false }, + { "acltype": "publishBrokerToClient", "allow": false }, + { "acltype": "subscribe", "allow": false }, + { "acltype": "unsubscribe", "allow": false } + ] + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec setDefaultACLAccess subscribe deny +``` + +## Create Client + +Command: +``` +{ + "commands":[ + { + "command": "createClient", + "username": "new username", + "password": "new password", + "clientid": "", # Optional + "textname": "", # Optional + "textdescription": "", # Optional + "groups": [ + { "groupname": "group", "priority": 1 } + ], # Optional, groups must exist + "roles": [ + { "rolename": "role", "priority": -1 } + ] # Optional, roles must exist + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec createClient username password +``` + +## Delete Client + +Command: +``` +{ + "commands":[ + { + "command": "deleteClient", + "username": "username to delete" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec deleteClient username +``` + +## Get Client + +Command: +``` +{ + "commands":[ + { + "command": "getClient", + "username": "required username" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec getClient username +``` + +## List Clients + +Command: +``` +{ + "commands":[ + { + "command": "listClients", + "verbose": false, + "count": -1, # -1 for all, or a positive integer for a limited count + "offset": 0 # Where in the list to start + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec listClients 10 20 +``` + +## Modify Existing Client + +Command: +``` +{ + "commands":[ + { + "command": "modifyClient", + "username": "username to modify" + "textname": "", # Optional + "textdescription": "", # Optional + "roles": [ + { "rolename": "role", "priority": 1 } + ], # Optional + "groups": [ + { "groupname": "group", "priority": 1 } + ], # Optional + } + ] +} +``` + +Modifying clients isn't currently possible with mosquitto_ctrl. + +## Set Client Password + +Command: +``` +{ + "commands":[ + { + "command": "setClientPassword", + "username": "username to change", + "password": "new password" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec setClientPassword username password +``` + +## Add Client Role + +Command: +``` +{ + "commands":[ + { + "command": "addClientRole", + "username": "client to add role to", + "rolename": "role to add", + "priority": -1 # Optional priority + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec addClientRole username rolename +``` + +## Remove Client Role + +Command: +``` +{ + "commands":[ + { + "command": "removeClientRole", + "username": "client to remove role from", + "rolename": "role to remove" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec removeClientRole username rolename +``` + +## Add Client to a Group + +Command: +``` +{ + "commands":[ + { + "command": "addGroupClient", + "groupname": "group to add client to", + "username": "client to add to group", + "priority": -1 # Priority of the group for the client + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec addGroupClient groupname username +``` + +## Create Group + +Command: +``` +{ + "commands":[ + { + "command": "createGroup", + "groupname": "new group", + "roles": [ + { "rolename": "role", "priority": 1 } + ] # Optional, roles must exist + + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec createGroup groupname +``` + +## Delete Group + +Command: +``` +{ + "commands":[ + { + "command": "deleteGroup", + "groupname: "group to delete" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec deleteGroup groupname +``` + +## Get Group + +Command: +``` +{ + "commands":[ + { + "command": "getGroup", + "groupname: "group to get" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec getGroup groupname +``` + +## List Groups + +Command: +``` +{ + "commands":[ + { + "command": "listGroups", + "verbose": false, + "count": -1, # -1 for all, or a positive integer for a limited count + "offset": 0 # Where in the list to start + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec listGroups +``` + +## Modify Group + +Command: +``` +{ + "commands":[ + { + "command": "modifyGroup", + "groupname": "group to modify", + "textname": "", # Optional + "textdescription": "", # Optional + "roles": [ + { "rolename": "role", "priority": 1 } + ], # Optional + "clients": [ + { "username": "client", "priority": 1 } + ] # Optional + + } + ] +} +``` + +Modifying groups isn't currently possible with mosquitto_ctrl. + +## Remove Client from a Group + +Command: +``` +{ + "commands":[ + { + "command": "removeGroupClient", + "groupname": "group to remove client from", + "username": "client to remove from group" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec removeGroupClient groupname username +``` + +## Add Group Role + +Command: +``` +{ + "commands":[ + { + "command": "addGroupRole", + "groupname": "group to add role to", + "rolename": "role to add", + "priority": -1 # Optional priority + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec addGroupRole groupname rolename +``` + +## Remove Group Role + +Command: +``` +{ + "commands":[ + { + "command": "removeGroupRole", + "groupname": "group", + "rolename": "role" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec removeGroupRole groupname rolename +``` + +## Set Group for Anonymous Clients + +Command: +``` +{ + "commands":[ + { + "command": "setAnonymousGroup", + "groupname": "group" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec setAnonymousGroup groupname +``` + +## Create Role + +Command: +``` +{ + "commands":[ + { + "command": "createRole", + "rolename": "new role", + "textname": "", # Optional + "textdescription": "", # Optional + "acls": [ + { "acltype": "subscribePattern", "topic": "topic/#", "priority": -1, "allow": true} + ] # Optional + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec createRole rolename +``` + +## Get Role + +Command: +``` +{ + "commands":[ + { + "command": "getRole", + "rolename": "role", + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec getRole rolename +``` + +## List Roles + +Command: +``` +{ + "commands":[ + { + "command": "listRoles", + "verbose": false, + "count": -1, # -1 for all, or a positive integer for a limited count + "offset": 0 # Where in the list to start + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec listRoles +``` + +## Modify Role + +Command: +``` +{ + "commands":[ + { + "command": "modifyRole", + "rolename": "role to modify" + "textname": "", # Optional + "textdescription": "", # Optional + "acls": [ + { "acltype": "subscribePattern", "topic": "topic/#", "priority": -1, "allow": true } + ] # Optional + } + ] +} +``` + +Modifying roles isn't currently possible with mosquitto_ctrl. + +## Delete Role + +Command: +``` +{ + "commands":[ + { + "command": "deleteRole", + "rolename": "role" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec deleteRole rolename +``` + +## Add Role ACL + +Command: +``` +{ + "commands":[ + { + "command": "addRoleACL", + "acltype": "subscribePattern", + "topic": "topic/#", + "priority": -1, + "allow": true + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec addRoleACL rolename subscribeLiteral topic/# deny +``` + +## Remove Role ACL + +Command: +``` +{ + "commands":[ + { + "command": "removeRoleACL", + "acltype": "subscribePattern", + "topic": "topic/#" + } + ] +} +``` + +mosquitto_ctrl example: +``` +mosquitto_ctrl dynsec removeRoleACL rolename subscribeLiteral topic/# +``` diff --git a/plugins/dynamic-security/acl.c b/plugins/dynamic-security/acl.c new file mode 100644 index 00000000..415f7e56 --- /dev/null +++ b/plugins/dynamic-security/acl.c @@ -0,0 +1,248 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include "dynamic_security.h" +#include "mosquitto.h" +#include "mosquitto_broker.h" +#include "mosquitto_plugin.h" + +typedef int (*MOSQ_FUNC_acl_check)(struct mosquitto_evt_acl_check *, struct dynsec__rolelist *); + +/* FIXME - CACHE! */ + +/* ################################################################ + * # + * # ACL check - publish broker to client + * # + * ################################################################ */ + +static int acl_check_publish_b2c(struct mosquitto_evt_acl_check *ed, struct dynsec__rolelist *base_rolelist) +{ + struct dynsec__rolelist *rolelist, *rolelist_tmp; + struct dynsec__acl *acl, *acl_tmp; + bool result; + + HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ + HASH_ITER(hh, rolelist->role->acls.publish_b2c, acl, acl_tmp){ + mosquitto_topic_matches_sub(acl->topic, ed->topic, &result); + if(result){ + if(acl->allow){ + return MOSQ_ERR_SUCCESS; + }else{ + return MOSQ_ERR_ACL_DENIED; + } + } + } + } + return MOSQ_ERR_NOT_FOUND; +} + + +/* ################################################################ + * # + * # ACL check - publish client to broker + * # + * ################################################################ */ + +static int acl_check_publish_c2b(struct mosquitto_evt_acl_check *ed, struct dynsec__rolelist *base_rolelist) +{ + struct dynsec__rolelist *rolelist, *rolelist_tmp; + struct dynsec__acl *acl, *acl_tmp; + bool result; + + HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ + HASH_ITER(hh, rolelist->role->acls.publish_c2b, acl, acl_tmp){ + mosquitto_topic_matches_sub(acl->topic, ed->topic, &result); + if(result){ + if(acl->allow){ + return MOSQ_ERR_SUCCESS; + }else{ + return MOSQ_ERR_ACL_DENIED; + } + } + } + } + return MOSQ_ERR_NOT_FOUND; +} + + +/* ################################################################ + * # + * # ACL check - subscribe + * # + * ################################################################ */ + +static int acl_check_subscribe(struct mosquitto_evt_acl_check *ed, struct dynsec__rolelist *base_rolelist) +{ + struct dynsec__rolelist *rolelist, *rolelist_tmp; + struct dynsec__acl *acl, *acl_tmp; + int len; + + len = strlen(ed->topic); + + HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ + HASH_FIND(hh, rolelist->role->acls.subscribe_literal, ed->topic, len, acl); + if(acl){ + if(acl->allow){ + return MOSQ_ERR_SUCCESS; + }else{ + return MOSQ_ERR_ACL_DENIED; + } + } + HASH_ITER(hh, rolelist->role->acls.subscribe_pattern, acl, acl_tmp){ + if(sub_acl_check(acl->topic, ed->topic)){ + if(acl->allow){ + return MOSQ_ERR_SUCCESS; + }else{ + return MOSQ_ERR_ACL_DENIED; + } + } + } + } + return MOSQ_ERR_NOT_FOUND; +} + + +/* ################################################################ + * # + * # ACL check - unsubscribe + * # + * ################################################################ */ + +static int acl_check_unsubscribe(struct mosquitto_evt_acl_check *ed, struct dynsec__rolelist *base_rolelist) +{ + struct dynsec__rolelist *rolelist, *rolelist_tmp; + struct dynsec__acl *acl, *acl_tmp; + int len; + + len = strlen(ed->topic); + + HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ + HASH_FIND(hh, rolelist->role->acls.unsubscribe_literal, ed->topic, len, acl); + if(acl){ + if(acl->allow){ + return MOSQ_ERR_SUCCESS; + }else{ + return MOSQ_ERR_ACL_DENIED; + } + } + HASH_ITER(hh, rolelist->role->acls.unsubscribe_pattern, acl, acl_tmp){ + if(sub_acl_check(acl->topic, ed->topic)){ + if(acl->allow){ + return MOSQ_ERR_SUCCESS; + }else{ + return MOSQ_ERR_ACL_DENIED; + } + } + } + } + return MOSQ_ERR_NOT_FOUND; +} + + +/* ################################################################ + * # + * # ACL check - generic check + * # + * ################################################################ */ + +static int acl_check(struct mosquitto_evt_acl_check *ed, MOSQ_FUNC_acl_check check, bool default_access) +{ + struct dynsec__client *client; + struct dynsec__grouplist *grouplist, *grouplist_tmp; + const char *username; + int rc; + + username = mosquitto_client_username(ed->client); + + if(username){ + client = dynsec_clients__find(username); + if(client == NULL) return MOSQ_ERR_PLUGIN_DEFER; + + /* Client roles */ + rc = check(ed, client->rolelist); + if(rc != MOSQ_ERR_NOT_FOUND){ + return rc; + } + + HASH_ITER(hh, client->grouplist, grouplist, grouplist_tmp){ + rc = check(ed, grouplist->group->rolelist); + if(rc != MOSQ_ERR_NOT_FOUND){ + return rc; + } + } + }else if(dynsec_anonymous_group){ + /* If we have a group for anonymous users, use that for checking. */ + rc = check(ed, dynsec_anonymous_group->rolelist); + if(rc != MOSQ_ERR_NOT_FOUND){ + return rc; + } + } + + if(default_access == false){ + return MOSQ_ERR_PLUGIN_DEFER; + }else{ + if(!strncmp(ed->topic, "$CONTROL", strlen("$CONTROL"))){ + /* We never give fall through access to $CONTROL topics, they must + * be granted explicitly. */ + return MOSQ_ERR_PLUGIN_DEFER; + }else{ + return MOSQ_ERR_SUCCESS; + } + } +} + + +/* ################################################################ + * # + * # ACL check - plugin callback + * # + * ################################################################ */ + +int dynsec__acl_check_callback(int event, void *event_data, void *userdata) +{ + struct mosquitto_evt_acl_check *ed = event_data; + + /* ACL checks are made in the order below until a match occurs, at which + * point the decision is made. + * + * User roles in priority order highest to lowest. + * Roles have their ACLs checked in priority order, highest to lowest + * Groups are processed in priority order highest to lowest + * Group roles are processed in priority order, highest to lowest + * Roles have their ACLs checked in priority order, highest to lowest + */ + + switch(ed->access){ + case MOSQ_ACL_SUBSCRIBE: + return acl_check(event_data, acl_check_subscribe, default_access.subscribe); + break; + case MOSQ_ACL_UNSUBSCRIBE: + return acl_check(event_data, acl_check_unsubscribe, default_access.unsubscribe); + break; + case MOSQ_ACL_WRITE: /* Client to broker */ + return acl_check(event_data, acl_check_publish_c2b, default_access.publish_c2b); + break; + case MOSQ_ACL_READ: + return acl_check(event_data, acl_check_publish_b2c, default_access.publish_b2c); + break; + default: + return MOSQ_ERR_PLUGIN_DEFER; + } + return MOSQ_ERR_PLUGIN_DEFER; +} diff --git a/plugins/dynamic-security/auth.c b/plugins/dynamic-security/auth.c new file mode 100644 index 00000000..0be173b6 --- /dev/null +++ b/plugins/dynamic-security/auth.c @@ -0,0 +1,193 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include +#include + +#include "dynamic_security.h" +#include "mosquitto.h" +#include "mosquitto_broker.h" + + +/* ################################################################ + * # + * # Base64 encoding/decoding + * # + * ################################################################ */ + +int dynsec_auth__base64_encode(unsigned char *in, unsigned int in_len, char **encoded) +{ + BIO *bmem, *b64; + BUF_MEM *bptr; + + b64 = BIO_new(BIO_f_base64()); + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + bmem = BIO_new(BIO_s_mem()); + b64 = BIO_push(b64, bmem); + BIO_write(b64, in, in_len); + if(BIO_flush(b64) != 1){ + BIO_free_all(b64); + return 1; + } + BIO_get_mem_ptr(b64, &bptr); + *encoded = mosquitto_malloc(bptr->length+1); + if(!(*encoded)){ + BIO_free_all(b64); + return 1; + } + memcpy(*encoded, bptr->data, bptr->length); + (*encoded)[bptr->length] = '\0'; + BIO_free_all(b64); + + return 0; +} + + +int dynsec_auth__base64_decode(char *in, unsigned char **decoded, unsigned int *decoded_len) +{ + BIO *bmem, *b64; + int slen; + + slen = strlen(in); + + b64 = BIO_new(BIO_f_base64()); + if(!b64){ + return 1; + } + BIO_set_flags(b64, BIO_FLAGS_BASE64_NO_NL); + + bmem = BIO_new(BIO_s_mem()); + if(!bmem){ + BIO_free_all(b64); + return 1; + } + b64 = BIO_push(b64, bmem); + BIO_write(bmem, in, slen); + + if(BIO_flush(bmem) != 1){ + BIO_free_all(b64); + return 1; + } + *decoded = mosquitto_calloc(slen, 1); + if(!(*decoded)){ + BIO_free_all(b64); + return 1; + } + *decoded_len = BIO_read(b64, *decoded, slen); + BIO_free_all(b64); + + if(*decoded_len <= 0){ + mosquitto_free(*decoded); + *decoded = NULL; + *decoded_len = 0; + return 1; + } + + return 0; +} + + +/* ################################################################ + * # + * # Password functions + * # + * ################################################################ */ + +int dynsec_auth__pw_hash(struct dynsec__client *client, const char *password, unsigned char *password_hash, int password_hash_len, bool new_password) +{ + const EVP_MD *digest; + int iterations; + + if(new_password){ + if(RAND_bytes(client->pw.salt, sizeof(client->pw.salt)) != 1){ + return MOSQ_ERR_UNKNOWN; + } + iterations = PW_DEFAULT_ITERATIONS; + }else{ + iterations = client->pw.iterations; + } + if(iterations < 1){ + return MOSQ_ERR_INVAL; + } + client->pw.iterations = iterations; + + digest = EVP_get_digestbyname("sha512"); + if(!digest){ + return MOSQ_ERR_UNKNOWN; + } + + return !PKCS5_PBKDF2_HMAC(password, strlen(password), + client->pw.salt, sizeof(client->pw.salt), iterations, + digest, password_hash_len, password_hash); +} + + +/* ################################################################ + * # + * # Username/password check + * # + * ################################################################ */ + +static int memcmp_const(const void *a, const void *b, size_t len) +{ + size_t i; + int rc = 0; + + if(!a || !b) return 1; + + for(i=0; iusername == NULL || ed->password == NULL) return MOSQ_ERR_PLUGIN_DEFER; + + client = dynsec_clients__find(ed->username); + if(client){ + if(client->clientid){ + clientid = mosquitto_client_id(ed->client); + if(clientid == NULL || strcmp(client->clientid, clientid)){ + return MOSQ_ERR_AUTH; + } + } + if(dynsec_auth__pw_hash(client, ed->password, password_hash, sizeof(password_hash), false) == MOSQ_ERR_SUCCESS){ + if(memcmp_const(client->pw.password_hash, password_hash, sizeof(password_hash)) == 0){ + return MOSQ_ERR_SUCCESS; + }else{ + return MOSQ_ERR_AUTH; + } + }else{ + return MOSQ_ERR_PLUGIN_DEFER; + } + }else{ + return MOSQ_ERR_PLUGIN_DEFER; + } +} diff --git a/plugins/dynamic-security/clients.c b/plugins/dynamic-security/clients.c new file mode 100644 index 00000000..07d3282f --- /dev/null +++ b/plugins/dynamic-security/clients.c @@ -0,0 +1,868 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include + +#include "mosquitto.h" +#include "mosquitto_broker.h" +#include "json_help.h" + +#include "dynamic_security.h" + +/* ################################################################ + * # + * # Function declarations + * # + * ################################################################ */ + +static int dynsec__remove_client_from_all_groups(const char *username); + +/* ################################################################ + * # + * # Local variables + * # + * ################################################################ */ + +static struct dynsec__client *local_clients = NULL; + + +/* ################################################################ + * # + * # Utility functions + * # + * ################################################################ */ + +static int client_cmp(void *a, void *b) +{ + struct dynsec__client *client_a = a; + struct dynsec__client *client_b = b; + + return strcmp(client_a->username, client_b->username); +} + +static void client__free_item(struct dynsec__client *client) +{ + if(client == NULL) return; + + HASH_DEL(local_clients, client); + dynsec_rolelists__free_all(&client->rolelist); + dynsec__remove_client_from_all_groups(client->username); + mosquitto_free(client->text_name); + mosquitto_free(client->text_description); + mosquitto_free(client->clientid); + mosquitto_free(client->username); + mosquitto_free(client); +} + +struct dynsec__client *dynsec_clients__find(const char *username) +{ + struct dynsec__client *client = NULL; + + if(username){ + HASH_FIND(hh, local_clients, username, strlen(username), client); + } + return client; +} + + +void dynsec_clients__cleanup(void) +{ + struct dynsec__client *client, *client_tmp; + + HASH_ITER(hh, local_clients, client, client_tmp){ + client__free_item(client); + } +} + +/* ################################################################ + * # + * # Config file load and save + * # + * ################################################################ */ + +int dynsec_clients__config_load(cJSON *tree) +{ + cJSON *j_clients, *j_client, *jtmp, *j_roles, *j_role; + struct dynsec__client *client; + struct dynsec__role *role; + unsigned char *buf; + unsigned int buf_len; + int priority; + int iterations; + + j_clients = cJSON_GetObjectItem(tree, "clients"); + if(j_clients == NULL){ + return 0; + } + + if(cJSON_IsArray(j_clients) == false){ + return 1; + } + + cJSON_ArrayForEach(j_client, j_clients){ + if(cJSON_IsObject(j_client) == true){ + client = mosquitto_calloc(1, sizeof(struct dynsec__client)); + if(client == NULL){ + // FIXME log + return MOSQ_ERR_NOMEM; + } + + /* Username */ + jtmp = cJSON_GetObjectItem(j_client, "username"); + if(jtmp == NULL || !cJSON_IsString(jtmp)){ + // FIXME log + mosquitto_free(client); + continue; + } + client->username = mosquitto_strdup(jtmp->valuestring); + if(client->username == NULL){ + // FIXME log + mosquitto_free(client); + continue; + } + + /* Hash iterations */ + jtmp = cJSON_GetObjectItem(j_client, "iterations"); + if(jtmp == NULL || !cJSON_IsNumber(jtmp)){ + // FIXME log + mosquitto_free(client->username); + mosquitto_free(client); + continue; + } + iterations = jtmp->valuedouble; + if(iterations < 1){ + // FIXME log + mosquitto_free(client->username); + mosquitto_free(client); + continue; + }else{ + client->pw.iterations = iterations; + } + + /* Salt */ + jtmp = cJSON_GetObjectItem(j_client, "salt"); + if(jtmp == NULL || !cJSON_IsString(jtmp)){ + // FIXME log + mosquitto_free(client->username); + mosquitto_free(client); + continue; + } + if(dynsec_auth__base64_decode(jtmp->valuestring, &buf, &buf_len) != MOSQ_ERR_SUCCESS + || buf_len != sizeof(client->pw.salt)){ + + // FIXME log + mosquitto_free(client->username); + mosquitto_free(client); + continue; + } + memcpy(client->pw.salt, buf, buf_len); + mosquitto_free(buf); + + /* Password */ + jtmp = cJSON_GetObjectItem(j_client, "password"); + if(jtmp == NULL || !cJSON_IsString(jtmp)){ + // FIXME log + mosquitto_free(client->username); + mosquitto_free(client); + continue; + } + if(dynsec_auth__base64_decode(jtmp->valuestring, &buf, &buf_len) != MOSQ_ERR_SUCCESS + || buf_len != sizeof(client->pw.password_hash)){ + + // FIXME log + mosquitto_free(client->username); + mosquitto_free(client); + continue; + } + memcpy(client->pw.password_hash, buf, buf_len); + mosquitto_free(buf); + + /* Client id */ + jtmp = cJSON_GetObjectItem(j_client, "clientid"); + if(jtmp != NULL && cJSON_IsString(jtmp)){ + client->clientid = mosquitto_strdup(jtmp->valuestring); + if(client->clientid == NULL){ + // FIXME log + mosquitto_free(client->username); + mosquitto_free(client); + continue; + } + } + + /* Text name */ + jtmp = cJSON_GetObjectItem(j_client, "textname"); + if(jtmp != NULL && cJSON_IsString(jtmp)){ + client->text_name = mosquitto_strdup(jtmp->valuestring); + if(client->text_name == NULL){ + // FIXME log + mosquitto_free(client->clientid); + mosquitto_free(client->username); + mosquitto_free(client); + continue; + } + } + + /* Text description */ + jtmp = cJSON_GetObjectItem(j_client, "textdescription"); + if(jtmp != NULL && cJSON_IsString(jtmp)){ + client->text_description = mosquitto_strdup(jtmp->valuestring); + if(client->text_description == NULL){ + // FIXME log + mosquitto_free(client->text_name); + mosquitto_free(client->clientid); + mosquitto_free(client->username); + mosquitto_free(client); + continue; + } + } + + /* Roles */ + j_roles = cJSON_GetObjectItem(j_client, "roles"); + if(j_roles && cJSON_IsArray(j_roles)){ + cJSON_ArrayForEach(j_role, j_roles){ + if(cJSON_IsObject(j_role)){ + jtmp = cJSON_GetObjectItem(j_role, "rolename"); + if(jtmp && cJSON_IsString(jtmp)){ + json_get_int(j_role, "priority", &priority, true, -1); + role = dynsec_roles__find(jtmp->valuestring); + dynsec_rolelists__add_role(&client->rolelist, role, priority); + } + } + } + } + + HASH_ADD_KEYPTR(hh, local_clients, client->username, strlen(client->username), client); + } + } + HASH_SORT(local_clients, client_cmp); + + return 0; +} + + +static int dynsec__config_add_clients(cJSON *j_clients) +{ + struct dynsec__client *client, *client_tmp; + cJSON *j_client, *j_roles, *jtmp; + char *buf; + char s_iterations[10]; + + HASH_ITER(hh, local_clients, client, client_tmp){ + j_client = cJSON_CreateObject(); + if(j_client == NULL) return 1; + cJSON_AddItemToArray(j_clients, j_client); + + if(cJSON_AddStringToObject(j_client, "username", client->username) == NULL + || (client->clientid && cJSON_AddStringToObject(j_client, "clientid", client->clientid) == NULL) + || (client->text_name && cJSON_AddStringToObject(j_client, "textname", client->text_name) == NULL) + || (client->text_description && cJSON_AddStringToObject(j_client, "textdescription", client->text_description) == NULL) + ){ + + return 1; + } + + j_roles = dynsec_rolelists__all_to_json(client->rolelist); + if(j_roles == NULL){ + return 1; + } + cJSON_AddItemToObject(j_client, "roles", j_roles); + + if(dynsec_auth__base64_encode(client->pw.password_hash, sizeof(client->pw.password_hash), &buf) != MOSQ_ERR_SUCCESS){ + return 1; + } + jtmp = cJSON_CreateString(buf); + mosquitto_free(buf); + if(jtmp == NULL) return 1; + cJSON_AddItemToObject(j_client, "password", jtmp); + + if(dynsec_auth__base64_encode(client->pw.salt, sizeof(client->pw.salt), &buf) != MOSQ_ERR_SUCCESS){ + return 1; + } + + jtmp = cJSON_CreateString(buf); + mosquitto_free(buf); + if(jtmp == NULL) return 1; + cJSON_AddItemToObject(j_client, "salt", jtmp); + + snprintf(s_iterations, sizeof(s_iterations), "%d", client->pw.iterations); + if(cJSON_AddRawToObject(j_client, "iterations", s_iterations) == NULL){ + return 1; + } + } + + return 0; +} + + +int dynsec_clients__config_save(cJSON *tree) +{ + cJSON *j_clients; + + j_clients = cJSON_CreateArray(); + if(j_clients == NULL){ + return 1; + } + + cJSON_AddItemToObject(tree, "clients", j_clients); + if(dynsec__config_add_clients(j_clients)){ + return 1; + } + + return 0; +} + + +int dynsec_clients__process_create(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username, *password, *clientid; + char *text_name, *text_description; + struct dynsec__client *client; + int rc; + cJSON *j_groups, *j_group, *jtmp; + int priority; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createClient", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "password", &password, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createClient", "Invalid/missing password", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "clientid", &clientid, true) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createClient", "Invalid/missing client id", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textname", &text_name, true) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createClient", "Invalid/missing textname", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textdescription", &text_description, true) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createClient", "Invalid/missing textdescription", correlation_data); + return MOSQ_ERR_INVAL; + } + + client = dynsec_clients__find(username); + if(client){ + dynsec__command_reply(j_responses, context, "createClient", "Client already exists", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + client = mosquitto_calloc(1, sizeof(struct dynsec__client)); + if(client == NULL){ + dynsec__command_reply(j_responses, context, "createClient", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + client->username = mosquitto_strdup(username); + if(client->username == NULL){ + dynsec__command_reply(j_responses, context, "createClient", "Internal error", correlation_data); + client__free_item(client); + return MOSQ_ERR_NOMEM; + } + if(text_name){ + client->text_name = mosquitto_strdup(text_name); + if(client->text_name == NULL){ + dynsec__command_reply(j_responses, context, "createClient", "Internal error", correlation_data); + client__free_item(client); + return MOSQ_ERR_NOMEM; + } + } + if(text_description){ + client->text_description = mosquitto_strdup(text_description); + if(client->text_description == NULL){ + dynsec__command_reply(j_responses, context, "createClient", "Internal error", correlation_data); + client__free_item(client); + return MOSQ_ERR_NOMEM; + } + } + + if(dynsec_auth__pw_hash(client, password, client->pw.password_hash, sizeof(client->pw.password_hash), true)){ + dynsec__command_reply(j_responses, context, "createClient", "Internal error", correlation_data); + client__free_item(client); + return MOSQ_ERR_NOMEM; + } + if(clientid){ + client->clientid = mosquitto_strdup(clientid); + if(client->clientid == NULL){ + dynsec__command_reply(j_responses, context, "createClient", "Internal error", correlation_data); + client__free_item(client); + return MOSQ_ERR_NOMEM; + } + } + + rc = dynsec_rolelists__load_from_json(command, &client->rolelist); + if(rc == MOSQ_ERR_SUCCESS || rc == ERR_LIST_NOT_FOUND){ + }else if(rc == MOSQ_ERR_NOT_FOUND){ + dynsec__command_reply(j_responses, context, "createClient", "Role not found", correlation_data); + client__free_item(client); + return MOSQ_ERR_INVAL; + }else{ + dynsec__command_reply(j_responses, context, "createClient", "Internal error", correlation_data); + client__free_item(client); + return MOSQ_ERR_INVAL; + } + + j_groups = cJSON_GetObjectItem(command, "groups"); + if(j_groups && cJSON_IsArray(j_groups)){ + cJSON_ArrayForEach(j_group, j_groups){ + if(cJSON_IsObject(j_group)){ + jtmp = cJSON_GetObjectItem(j_group, "groupname"); + if(jtmp && cJSON_IsString(jtmp)){ + json_get_int(j_group, "priority", &priority, true, -1); + dynsec_groups__add_client(username, jtmp->valuestring, priority, false); + } + } + } + } + + HASH_ADD_KEYPTR_INORDER(hh, local_clients, client->username, strlen(client->username), client, client_cmp); + + dynsec__config_save(); + + dynsec__command_reply(j_responses, context, "createClient", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_clients__process_delete(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username; + struct dynsec__client *client; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "deleteClient", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + client = dynsec_clients__find(username); + if(client){ + dynsec__remove_client_from_all_groups(username); + client__free_item(client); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "deleteClient", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; + }else{ + dynsec__command_reply(j_responses, context, "deleteClient", "Client not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } +} + + +int dynsec_clients__process_set_password(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username, *password; + struct dynsec__client *client; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "setClientPassword", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "password", &password, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "setClientPassword", "Invalid/missing password", correlation_data); + return MOSQ_ERR_INVAL; + } + + client = dynsec_clients__find(username); + if(client == NULL){ + dynsec__command_reply(j_responses, context, "setClientPassword", "Client not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + if(dynsec_auth__pw_hash(client, password, client->pw.password_hash, sizeof(client->pw.password_hash), true) == MOSQ_ERR_SUCCESS){ + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "setClientPassword", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; + }else{ + dynsec__command_reply(j_responses, context, "setClientPassword", "Internal error", correlation_data); + // FIXME - this should fail safe without modifying the existing password + return MOSQ_ERR_NOMEM; + } +} + + + +int dynsec_clients__process_modify(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username; + char *text_name, *text_description; + struct dynsec__client *client; + struct dynsec__rolelist *rolelist = NULL; + char *str; + int rc; + int priority; + cJSON *j_group, *j_groups, *jtmp; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "modifyClient", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + client = dynsec_clients__find(username); + if(client == NULL){ + dynsec__command_reply(j_responses, context, "modifyClient", "Client does not exist", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textname", &text_name, true) == MOSQ_ERR_SUCCESS){ + str = mosquitto_strdup(text_name); + if(str == NULL){ + dynsec__command_reply(j_responses, context, "modifyClient", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + mosquitto_free(client->text_name); + client->text_name = str; + } + + if(json_get_string(command, "textdescription", &text_description, true) == MOSQ_ERR_SUCCESS){ + str = mosquitto_strdup(text_description); + if(str == NULL){ + dynsec__command_reply(j_responses, context, "modifyClient", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + mosquitto_free(client->text_description); + client->text_description = str; + } + + rc = dynsec_rolelists__load_from_json(command, &rolelist); + if(rc == MOSQ_ERR_SUCCESS){ + dynsec_rolelists__free_all(&client->rolelist); + client->rolelist = rolelist; + }else if(rc == MOSQ_ERR_NOT_FOUND){ + dynsec__command_reply(j_responses, context, "modifyClient", "Role not found", correlation_data); + dynsec_rolelists__free_all(&rolelist); + return MOSQ_ERR_INVAL; + }else if(rc == ERR_LIST_NOT_FOUND){ + /* There was no list in the JSON, so no modification */ + }else{ + dynsec__command_reply(j_responses, context, "modifyClient", "Internal error", correlation_data); + dynsec_rolelists__free_all(&rolelist); + return MOSQ_ERR_INVAL; + } + + j_groups = cJSON_GetObjectItem(command, "groups"); + if(j_groups && cJSON_IsArray(j_groups)){ + dynsec__remove_client_from_all_groups(username); + + cJSON_ArrayForEach(j_group, j_groups){ + if(cJSON_IsObject(j_group)){ + jtmp = cJSON_GetObjectItem(j_group, "groupname"); + if(jtmp && cJSON_IsString(jtmp)){ + json_get_int(j_group, "priority", &priority, true, -1); + dynsec_groups__add_client(username, jtmp->valuestring, priority, false); + } + } + } + } + + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "modifyClient", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} + + +static int dynsec__remove_client_from_all_groups(const char *username) +{ + struct dynsec__grouplist *grouplist, *grouplist_tmp; + struct dynsec__client *client; + + client = dynsec_clients__find(username); + if(client){ + HASH_ITER(hh, client->grouplist, grouplist, grouplist_tmp){ + dynsec_groups__remove_client(username, grouplist->groupname, false); + } + } + + return MOSQ_ERR_SUCCESS; +} + + +static cJSON *add_client_to_json(struct dynsec__client *client, bool verbose) +{ + cJSON *j_client = NULL, *j_groups, *j_roles; + + if(verbose){ + j_client = cJSON_CreateObject(); + if(j_client == NULL){ + return NULL; + } + + if(cJSON_AddStringToObject(j_client, "username", client->username) == NULL + || (client->clientid && cJSON_AddStringToObject(j_client, "clientid", client->clientid) == NULL) + || (client->text_name && cJSON_AddStringToObject(j_client, "textname", client->text_name) == NULL) + || (client->text_description && cJSON_AddStringToObject(j_client, "textdescription", client->text_description) == NULL) + || (j_groups = cJSON_AddArrayToObject(j_client, "groups")) == NULL + ){ + + cJSON_Delete(j_client); + return NULL; + } + + j_roles = dynsec_rolelists__all_to_json(client->rolelist); + if(j_roles == NULL){ + cJSON_Delete(j_client); + return NULL; + } + cJSON_AddItemToObject(j_client, "roles", j_roles); + + j_groups = dynsec_grouplists__all_to_json(client->grouplist); + if(j_groups == NULL){ + cJSON_Delete(j_client); + return NULL; + } + cJSON_AddItemToObject(j_client, "groups", j_groups); + }else{ + j_client = cJSON_CreateString(client->username); + if(j_client == NULL){ + return NULL; + } + } + return j_client; +} + + +int dynsec_clients__process_get(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username; + struct dynsec__client *client; + cJSON *tree, *j_client, *jtmp, *j_data; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "getClient", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + client = dynsec_clients__find(username); + if(client == NULL){ + dynsec__command_reply(j_responses, context, "getClient", "Client not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + tree = cJSON_CreateObject(); + if(tree == NULL){ + dynsec__command_reply(j_responses, context, "getClient", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + + jtmp = cJSON_CreateString("getClient"); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getClient", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "command", jtmp); + + j_data = cJSON_CreateObject(); + if(j_data == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getClient", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "data", j_data); + + j_client = add_client_to_json(client, true); + if(j_client == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getClient", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(j_data, "client", j_client); + + if(correlation_data){ + jtmp = cJSON_CreateString(correlation_data); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getClient", "Internal error", correlation_data); + return 1; + } + cJSON_AddItemToObject(tree, "correlationData", jtmp); + } + + cJSON_AddItemToArray(j_responses, tree); + + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_clients__process_list(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + bool verbose; + struct dynsec__client *client, *client_tmp; + cJSON *tree, *j_clients, *j_client, *jtmp, *j_data; + int i, count, offset; + char buf[30]; + + json_get_bool(command, "verbose", &verbose, true, false); + json_get_int(command, "count", &count, true, -1); + json_get_int(command, "offset", &offset, true, 0); + + tree = cJSON_CreateObject(); + if(tree == NULL){ + dynsec__command_reply(j_responses, context, "listClients", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + + jtmp = cJSON_CreateString("listClients"); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listClients", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "command", jtmp); + + j_data = cJSON_CreateObject(); + if(j_data == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listClients", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "data", j_data); + + snprintf(buf, sizeof(buf), "%d", HASH_CNT(hh, local_clients)); + cJSON_AddRawToObject(j_data, "totalCount", buf); + + j_clients = cJSON_CreateArray(); + if(j_clients == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listClients", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(j_data, "clients", j_clients); + + i = 0; + HASH_ITER(hh, local_clients, client, client_tmp){ + if(i>=offset){ + j_client = add_client_to_json(client, verbose); + if(j_client == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listClients", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToArray(j_clients, j_client); + + if(count >= 0){ + count--; + if(count <= 0){ + break; + } + } + } + i++; + } + if(correlation_data){ + jtmp = cJSON_CreateString(correlation_data); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listClients", "Internal error", correlation_data); + return 1; + } + cJSON_AddItemToObject(tree, "correlationData", jtmp); + } + + cJSON_AddItemToArray(j_responses, tree); + + return MOSQ_ERR_SUCCESS; +} + + +void dynsec_clients__remove_role_from_all(const struct dynsec__role *role) +{ + struct dynsec__client *client, *client_tmp; + + HASH_ITER(hh, local_clients, client, client_tmp){ + dynsec_rolelists__remove_role(&client->rolelist, role); + } +} + + +int dynsec_clients__process_add_role(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username, *role_name; + struct dynsec__client *client; + struct dynsec__role *role; + int priority; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "addClientRole", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "rolename", &role_name, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "addClientRole", "Invalid/missing rolename", correlation_data); + return MOSQ_ERR_INVAL; + } + json_get_int(command, "priority", &priority, true, -1); + + client = dynsec_clients__find(username); + if(client == NULL){ + dynsec__command_reply(j_responses, context, "addClientRole", "Client not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + role = dynsec_roles__find(role_name); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "addClientRole", "Role not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + dynsec_rolelists__add_role(&client->rolelist, role, priority); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "addClientRole", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_clients__process_remove_role(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username, *rolename; + struct dynsec__client *client; + struct dynsec__role *role; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "removeClientRole", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "rolename", &rolename, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "removeGroupRole", "Invalid/missing rolename", correlation_data); + return MOSQ_ERR_INVAL; + } + + client = dynsec_clients__find(username); + if(client == NULL){ + dynsec__command_reply(j_responses, context, "removeClientRole", "Client not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + role = dynsec_roles__find(rolename); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "addClientRole", "Role not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + dynsec_rolelists__remove_role(&client->rolelist, role); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "removeClientRole", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} diff --git a/plugins/dynamic-security/dynamic_security.h b/plugins/dynamic-security/dynamic_security.h new file mode 100644 index 00000000..a4ad780b --- /dev/null +++ b/plugins/dynamic-security/dynamic_security.h @@ -0,0 +1,223 @@ +#ifndef DYNAMIC_SECURITY_H +#define DYNAMIC_SECURITY_H +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include +#include +#include "mosquitto.h" +#include "password_mosq.h" + +/* ################################################################ + * # + * # Error codes + * # + * ################################################################ */ + +#define ERR_USER_NOT_FOUND 10000 +#define ERR_GROUP_NOT_FOUND 10001 +#define ERR_LIST_NOT_FOUND 10002 + +/* ################################################################ + * # + * # Datatypes + * # + * ################################################################ */ + +struct dynsec__clientlist{ + UT_hash_handle hh; + char *username; + struct dynsec__client *client; + int priority; +}; + +struct dynsec__grouplist{ + UT_hash_handle hh; + char *groupname; + struct dynsec__group *group; + int priority; +}; + +struct dynsec__rolelist{ + UT_hash_handle hh; + char *rolename; + struct dynsec__role *role; + int priority; +}; + +struct dynsec__client{ + UT_hash_handle hh; + struct mosquitto_pw pw; + struct dynsec__rolelist *rolelist; + struct dynsec__grouplist *grouplist; + char *username; + char *clientid; + char *text_name; + char *text_description; +}; + +struct dynsec__group{ + UT_hash_handle hh; + struct dynsec__rolelist *rolelist; + struct dynsec__clientlist *clientlist; + char *groupname; + char *text_name; + char *text_description; +}; + + +struct dynsec__acl{ + UT_hash_handle hh; + char *topic; + int priority; + bool allow; +}; + +struct dynsec__acls{ + struct dynsec__acl *publish_c2b; + struct dynsec__acl *publish_b2c; + struct dynsec__acl *subscribe_literal; + struct dynsec__acl *subscribe_pattern; + struct dynsec__acl *unsubscribe_literal; + struct dynsec__acl *unsubscribe_pattern; +}; + +struct dynsec__role{ + UT_hash_handle hh; + struct dynsec__acls acls; + char *rolename; + char *text_name; + char *text_description; +}; + +struct dynsec__acl_default_access{ + bool publish_c2b; + bool publish_b2c; + bool subscribe; + bool unsubscribe; +}; + +extern struct dynsec__group *dynsec_anonymous_group; +extern struct dynsec__acl_default_access default_access; + +/* ################################################################ + * # + * # Plugin Functions + * # + * ################################################################ */ + +void dynsec__config_save(void); +int dynsec__handle_control(cJSON *j_responses, struct mosquitto *context, cJSON *commands); +void dynsec__command_reply(cJSON *j_responses, struct mosquitto *context, const char *command, const char *error, const char *correlation_data); + + +/* ################################################################ + * # + * # ACL Functions + * # + * ################################################################ */ + +int dynsec__acl_check_callback(int event, void *event_data, void *userdata); +bool sub_acl_check(const char *acl, const char *sub); + + +/* ################################################################ + * # + * # Auth Functions + * # + * ################################################################ */ + +int dynsec_auth__base64_encode(unsigned char *in, unsigned int in_len, char **encoded); +int dynsec_auth__base64_decode(char *in, unsigned char **decoded, unsigned int *decoded_len); +int dynsec_auth__pw_hash(struct dynsec__client *client, const char *password, unsigned char *password_hash, int password_hash_len, bool new_password); +int dynsec_auth__basic_auth_callback(int event, void *event_data, void *userdata); + + +/* ################################################################ + * # + * # Client Functions + * # + * ################################################################ */ + +void dynsec_clients__cleanup(void); +int dynsec_clients__config_load(cJSON *tree); +int dynsec_clients__config_save(cJSON *tree); +int dynsec_clients__process_add_role(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_clients__process_create(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_clients__process_delete(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_clients__process_get(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_clients__process_list(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_clients__process_modify(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_clients__process_remove_role(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_clients__process_set_password(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +struct dynsec__client *dynsec_clients__find(const char *username); +void dynsec_clients__remove_role_from_all(const struct dynsec__role *role); + +cJSON *dynsec_clientlists__all_to_json(struct dynsec__clientlist *base_clientlist); + + + +/* ################################################################ + * # + * # Group Functions + * # + * ################################################################ */ + +void dynsec_groups__cleanup(void); +int dynsec_groups__config_load(cJSON *tree); +int dynsec_groups__add_client(const char *username, const char *groupname, int priority, bool update_config); +int dynsec_groups__config_save(cJSON *tree); +int dynsec_groups__process_add_client(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_add_role(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_create(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_delete(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_get(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_list(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_modify(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_remove_client(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_remove_role(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__process_set_anonymous_group(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_groups__remove_client(const char *username, const char *groupname, bool update_config); +struct dynsec__group *dynsec_groups__find(const char *groupname); +void dynsec_groups__remove_role_from_all(const struct dynsec__role *role); + +cJSON *dynsec_grouplists__all_to_json(struct dynsec__grouplist *base_grouplist); + +/* ################################################################ + * # + * # Role Functions + * # + * ################################################################ */ + +void dynsec_roles__cleanup(void); +int dynsec_roles__config_load(cJSON *tree); +int dynsec_roles__config_save(cJSON *tree); +int dynsec_roles__process_add_acl(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_roles__process_create(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_roles__process_delete(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_roles__process_get(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_roles__process_list(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_roles__process_modify(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +int dynsec_roles__process_remove_acl(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data); +struct dynsec__role *dynsec_roles__find(const char *rolename); + +int dynsec_rolelists__add_role(struct dynsec__rolelist **base_rolelist, struct dynsec__role *role, int priority); +int dynsec_rolelists__load_from_json(cJSON *command, struct dynsec__rolelist **rolelist); +int dynsec_rolelists__remove_role(struct dynsec__rolelist **base_rolelist, const struct dynsec__role *role); +void dynsec_rolelists__free_all(struct dynsec__rolelist **base_rolelist); +cJSON *dynsec_rolelists__all_to_json(struct dynsec__rolelist *base_rolelist); + +#endif diff --git a/plugins/dynamic-security/groups.c b/plugins/dynamic-security/groups.c new file mode 100644 index 00000000..f1daccaa --- /dev/null +++ b/plugins/dynamic-security/groups.c @@ -0,0 +1,1023 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include + +#include "mosquitto.h" +#include "mosquitto_broker.h" +#include "json_help.h" + +#include "dynamic_security.h" + +/* ################################################################ + * # + * # Plugin global variables + * # + * ################################################################ */ + +struct dynsec__group *dynsec_anonymous_group = NULL; + + +/* ################################################################ + * # + * # Function declarations + * # + * ################################################################ */ + +static int dynsec__remove_all_clients_from_group(struct dynsec__group *group); +static cJSON *add_group_to_json(struct dynsec__group *group); + + +/* ################################################################ + * # + * # Local variables + * # + * ################################################################ */ + +static struct dynsec__group *local_groups = NULL; + + +/* ################################################################ + * # + * # Utility functions + * # + * ################################################################ */ + +static int group_cmp(void *a, void *b) +{ + struct dynsec__group *group_a = a; + struct dynsec__group *group_b = b; + + return strcmp(group_a->groupname, group_b->groupname); +} + +static int grouplist_cmp(void *a, void *b) +{ + struct dynsec__grouplist *grouplist_a = a; + struct dynsec__grouplist *grouplist_b = b; + + return grouplist_b->priority - grouplist_a->priority; +} + +static int clientlist_cmp(void *a, void *b) +{ + struct dynsec__clientlist *clientlist_a = a; + struct dynsec__clientlist *clientlist_b = b; + + return strcmp(clientlist_a->username, clientlist_b->username); +} + +cJSON *dynsec_clientlists__all_to_json(struct dynsec__clientlist *base_clientlist) +{ + struct dynsec__clientlist *clientlist, *clientlist_tmp; + cJSON *j_clients, *j_client; + char buf[30]; + + j_clients = cJSON_CreateArray(); + if(j_clients == NULL) return NULL; + + HASH_ITER(hh, base_clientlist, clientlist, clientlist_tmp){ + j_client = cJSON_CreateObject(); + if(j_client == NULL){ + cJSON_Delete(j_clients); + return NULL; + } + cJSON_AddItemToArray(j_clients, j_client); + + if(clientlist->priority != -1){ + snprintf(buf, sizeof(buf), "%d", clientlist->priority); + } + if(cJSON_AddStringToObject(j_client, "username", clientlist->client->username) == NULL + || (clientlist->priority != -1 && cJSON_AddRawToObject(j_client, "priority", buf) == NULL) + ){ + + cJSON_Delete(j_clients); + return NULL; + } + } + return j_clients; +} + + +cJSON *dynsec_grouplists__all_to_json(struct dynsec__grouplist *base_grouplist) +{ + struct dynsec__grouplist *grouplist, *grouplist_tmp; + cJSON *j_groups, *j_group; + char buf[30]; + + j_groups = cJSON_CreateArray(); + if(j_groups == NULL) return NULL; + + HASH_ITER(hh, base_grouplist, grouplist, grouplist_tmp){ + j_group = cJSON_CreateObject(); + if(j_group == NULL){ + cJSON_Delete(j_groups); + return NULL; + } + cJSON_AddItemToArray(j_groups, j_group); + + if(grouplist->priority != -1){ + snprintf(buf, sizeof(buf), "%d", grouplist->priority); + } + if(cJSON_AddStringToObject(j_group, "groupname", grouplist->group->groupname) == NULL + || (grouplist->priority != -1 && cJSON_AddRawToObject(j_group, "priority", buf) == NULL) + ){ + + cJSON_Delete(j_groups); + return NULL; + } + } + return j_groups; +} + + +static void group__free_item(struct dynsec__group *group) +{ + if(group == NULL) return; + + HASH_DEL(local_groups, group); + dynsec__remove_all_clients_from_group(group); + mosquitto_free(group->text_name); + mosquitto_free(group->text_description); + mosquitto_free(group->groupname); + dynsec_rolelists__free_all(&group->rolelist); + mosquitto_free(group); +} + +struct dynsec__group *dynsec_groups__find(const char *groupname) +{ + struct dynsec__group *group = NULL; + + if(groupname){ + HASH_FIND(hh, local_groups, groupname, strlen(groupname), group); + } + return group; +} + +int dynsec_groups__process_add_role(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *groupname, *role_name; + struct dynsec__group *group; + struct dynsec__role *role; + int priority; + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "addGroupRole", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "roleName", &role_name, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "addGroupRole", "Invalid/missing roleName", correlation_data); + return MOSQ_ERR_INVAL; + } + json_get_int(command, "priority", &priority, true, -1); + + group = dynsec_groups__find(groupname); + if(group == NULL){ + dynsec__command_reply(j_responses, context, "addGroupRole", "Group not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + role = dynsec_roles__find(role_name); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "addGroupRole", "Role not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + dynsec_rolelists__add_role(&group->rolelist, role, priority); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "addGroupRole", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} + + +void dynsec_groups__cleanup(void) +{ + struct dynsec__group *group, *group_tmp; + + HASH_ITER(hh, local_groups, group, group_tmp){ + group__free_item(group); + } +} + + +/* ################################################################ + * # + * # Config file load + * # + * ################################################################ */ + +int dynsec_groups__config_load(cJSON *tree) +{ + cJSON *j_groups, *j_group; + cJSON *j_clientlist, *j_client, *j_username; + cJSON *j_roles, *j_role, *j_rolename; + + struct dynsec__group *group; + struct dynsec__role *role; + char *str; + int priority; + + j_groups = cJSON_GetObjectItem(tree, "groups"); + if(j_groups == NULL){ + return 0; + } + + if(cJSON_IsArray(j_groups) == false){ + return 1; + } + + cJSON_ArrayForEach(j_group, j_groups){ + if(cJSON_IsObject(j_group) == true){ + group = mosquitto_calloc(1, sizeof(struct dynsec__group)); + if(group == NULL){ + // FIXME log + return MOSQ_ERR_NOMEM; + } + + /* Group name */ + if(json_get_string(j_group, "groupname", &str, false) != MOSQ_ERR_SUCCESS){ + // FIXME log + mosquitto_free(group); + continue; + } + group->groupname = strdup(str); + if(group->groupname == NULL){ + // FIXME log + mosquitto_free(group); + continue; + } + + /* Text name */ + if(json_get_string(j_group, "textname", &str, true) == MOSQ_ERR_SUCCESS){ + if(str){ + group->text_name = strdup(str); + if(group->text_name == NULL){ + // FIXME log + mosquitto_free(group->groupname); + mosquitto_free(group); + continue; + } + } + } + + /* Text description */ + if(json_get_string(j_group, "textdescription", &str, true) == MOSQ_ERR_SUCCESS){ + if(str){ + group->text_description = strdup(str); + if(group->text_description == NULL){ + // FIXME log + mosquitto_free(group->text_name); + mosquitto_free(group->groupname); + mosquitto_free(group); + continue; + } + } + } + + /* Roles */ + j_roles = cJSON_GetObjectItem(j_group, "roles"); + if(j_roles && cJSON_IsArray(j_roles)){ + cJSON_ArrayForEach(j_role, j_roles){ + if(cJSON_IsObject(j_role)){ + j_rolename = cJSON_GetObjectItem(j_role, "roleName"); + if(j_rolename && cJSON_IsString(j_rolename)){ + json_get_int(j_role, "priority", &priority, true, -1); + role = dynsec_roles__find(j_rolename->valuestring); + dynsec_rolelists__add_role(&group->rolelist, role, priority); + } + } + } + } + + /* This must go before clients are loaded, otherwise the group won't be found */ + HASH_ADD_KEYPTR(hh, local_groups, group->groupname, strlen(group->groupname), group); + + /* Clients */ + j_clientlist = cJSON_GetObjectItem(j_group, "clients"); + if(j_clientlist && cJSON_IsArray(j_clientlist)){ + cJSON_ArrayForEach(j_client, j_clientlist){ + if(cJSON_IsObject(j_client)){ + j_username = cJSON_GetObjectItem(j_client, "username"); + if(j_username && cJSON_IsString(j_username)){ + json_get_int(j_client, "priority", &priority, true, -1); + dynsec_groups__add_client(j_username->valuestring, group->groupname, priority, false); + } + } + } + } + } + } + HASH_SORT(local_groups, group_cmp); + + j_group = cJSON_GetObjectItem(tree, "anonymousGroup"); + if(j_group && cJSON_IsString(j_group)){ + dynsec_anonymous_group = dynsec_groups__find(j_group->valuestring); + } + + return 0; +} + + +/* ################################################################ + * # + * # Config load and save + * # + * ################################################################ */ + + +static int dynsec__config_add_groups(cJSON *j_groups) +{ + struct dynsec__group *group, *group_tmp; + cJSON *j_group, *j_clients, *j_roles; + + HASH_ITER(hh, local_groups, group, group_tmp){ + j_group = cJSON_CreateObject(); + if(j_group == NULL) return 1; + cJSON_AddItemToArray(j_groups, j_group); + + if(cJSON_AddStringToObject(j_group, "groupname", group->groupname) == NULL + || (group->text_name && cJSON_AddStringToObject(j_group, "textname", group->text_name) == NULL) + || (group->text_description && cJSON_AddStringToObject(j_group, "textdescription", group->text_description) == NULL) + ){ + + return 1; + } + + j_roles = dynsec_rolelists__all_to_json(group->rolelist); + if(j_roles == NULL){ + return 1; + } + cJSON_AddItemToObject(j_group, "roles", j_roles); + + j_clients = dynsec_clientlists__all_to_json(group->clientlist); + if(j_clients == NULL){ + return 1; + } + cJSON_AddItemToObject(j_group, "clients", j_clients); + } + + return 0; +} + + +int dynsec_groups__config_save(cJSON *tree) +{ + cJSON *j_groups; + + j_groups = cJSON_CreateArray(); + if(j_groups == NULL){ + return 1; + } + cJSON_AddItemToObject(tree, "groups", j_groups); + if(dynsec__config_add_groups(j_groups)){ + return 1; + } + + if(dynsec_anonymous_group + && cJSON_AddStringToObject(tree, "anonymousGroup", dynsec_anonymous_group->groupname) == NULL){ + + return 1; + } + + return 0; +} + + +int dynsec_groups__process_create(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *groupname, *text_name, *text_description; + struct dynsec__group *group = NULL; + int rc = MOSQ_ERR_SUCCESS; + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createGroup", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textname", &text_name, true) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createGroup", "Invalid/missing textname", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textdescription", &text_description, true) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createGroup", "Invalid/missing textdescription", correlation_data); + return MOSQ_ERR_INVAL; + } + + group = dynsec_groups__find(groupname); + if(group){ + dynsec__command_reply(j_responses, context, "createGroup", "Group already exists", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + group = mosquitto_calloc(1, sizeof(struct dynsec__group)); + if(group == NULL){ + dynsec__command_reply(j_responses, context, "createGroup", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + group->groupname = strdup(groupname); + if(group->groupname == NULL){ + dynsec__command_reply(j_responses, context, "createGroup", "Internal error", correlation_data); + group__free_item(group); + return MOSQ_ERR_NOMEM; + } + if(text_name){ + group->text_name = strdup(text_name); + if(group->text_name == NULL){ + dynsec__command_reply(j_responses, context, "createGroup", "Internal error", correlation_data); + group__free_item(group); + return MOSQ_ERR_NOMEM; + } + } + if(text_description){ + group->text_description = strdup(text_description); + if(group->text_description == NULL){ + dynsec__command_reply(j_responses, context, "createGroup", "Internal error", correlation_data); + group__free_item(group); + return MOSQ_ERR_NOMEM; + } + } + + rc = dynsec_rolelists__load_from_json(command, &group->rolelist); + if(rc == MOSQ_ERR_SUCCESS || rc == ERR_LIST_NOT_FOUND){ + }else if(rc == MOSQ_ERR_NOT_FOUND){ + dynsec__command_reply(j_responses, context, "createGroup", "Role not found", correlation_data); + group__free_item(group); + return MOSQ_ERR_INVAL; + }else{ + dynsec__command_reply(j_responses, context, "createGroup", "Internal error", correlation_data); + group__free_item(group); + return MOSQ_ERR_INVAL; + } + + HASH_ADD_KEYPTR_INORDER(hh, local_groups, group->groupname, strlen(group->groupname), group, group_cmp); + + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "createGroup", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_groups__process_delete(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *groupname; + struct dynsec__group *group; + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "deleteGroup", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + + group = dynsec_groups__find(groupname); + if(group){ + group__free_item(group); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "deleteGroup", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; + }else{ + dynsec__command_reply(j_responses, context, "deleteGroup", "Group not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } +} + + +int dynsec_groups__add_client(const char *username, const char *groupname, int priority, bool update_config) +{ + struct dynsec__client *client; + struct dynsec__clientlist *clientlist; + struct dynsec__group *group; + struct dynsec__grouplist *grouplist; + + client = dynsec_clients__find(username); + if(client == NULL){ + return ERR_USER_NOT_FOUND; + } + + group = dynsec_groups__find(groupname); + if(group == NULL){ + return ERR_GROUP_NOT_FOUND; + } + + HASH_FIND(hh, group->clientlist, username, strlen(username), clientlist); + if(clientlist != NULL){ + /* Client is already in the group */ + return MOSQ_ERR_SUCCESS; + } + + clientlist = mosquitto_malloc(sizeof(struct dynsec__clientlist)); + grouplist = mosquitto_malloc(sizeof(struct dynsec__grouplist)); + if(clientlist == NULL || grouplist == NULL){ + mosquitto_free(clientlist); + mosquitto_free(grouplist); + return MOSQ_ERR_UNKNOWN; + } + + clientlist->username = client->username; + clientlist->client = client; + clientlist->priority = priority; + HASH_ADD_KEYPTR_INORDER(hh, group->clientlist, clientlist->username, strlen(clientlist->username), clientlist, clientlist_cmp); + + grouplist->groupname = group->groupname; + grouplist->group = group; + grouplist->priority = priority; + HASH_ADD_KEYPTR_INORDER(hh, client->grouplist, grouplist->groupname, strlen(grouplist->groupname), grouplist, grouplist_cmp); + + if(update_config){ + dynsec__config_save(); + } + + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_groups__process_add_client(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username, *groupname; + int rc; + int priority; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "addGroupClient", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "addGroupClient", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + json_get_int(command, "priority", &priority, true, -1); + + rc = dynsec_groups__add_client(username, groupname, priority, true); + if(rc == MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "addGroupClient", NULL, correlation_data); + }else if(rc == ERR_USER_NOT_FOUND){ + dynsec__command_reply(j_responses, context, "addGroupClient", "Client not found", correlation_data); + }else if(rc == ERR_GROUP_NOT_FOUND){ + dynsec__command_reply(j_responses, context, "addGroupClient", "Group not found", correlation_data); + }else{ + dynsec__command_reply(j_responses, context, "addGroupClient", "Internal error", correlation_data); + } + return rc; +} + + +static void dynsec_clientlists__remove(struct dynsec__clientlist **base_clientlist, const char *username) +{ + struct dynsec__clientlist *clientlist; + + HASH_FIND(hh, *base_clientlist, username, strlen(username), clientlist); + if(clientlist){ + HASH_DELETE(hh, *base_clientlist, clientlist); + mosquitto_free(clientlist); + } +} + +static void dynsec_grouplists__remove(struct dynsec__grouplist **base_grouplist, const char *groupname) +{ + struct dynsec__grouplist *grouplist; + + HASH_FIND(hh, *base_grouplist, groupname, strlen(groupname), grouplist); + if(grouplist){ + HASH_DELETE(hh, *base_grouplist, grouplist); + mosquitto_free(grouplist); + } +} + +static int dynsec__remove_all_clients_from_group(struct dynsec__group *group) +{ + struct dynsec__clientlist *clientlist, *clientlist_tmp; + + HASH_ITER(hh, group->clientlist, clientlist, clientlist_tmp){ + /* Remove client stored group reference */ + dynsec_grouplists__remove(&clientlist->client->grouplist, group->groupname); + + HASH_DELETE(hh, group->clientlist, clientlist); + mosquitto_free(clientlist); + } + + return MOSQ_ERR_SUCCESS; +} + +int dynsec_groups__remove_client(const char *username, const char *groupname, bool update_config) +{ + struct dynsec__client *client; + struct dynsec__group *group; + + client = dynsec_clients__find(username); + if(client == NULL){ + return ERR_USER_NOT_FOUND; + } + + group = dynsec_groups__find(groupname); + if(group == NULL){ + return ERR_GROUP_NOT_FOUND; + } + + dynsec_clientlists__remove(&group->clientlist, username); + dynsec_grouplists__remove(&client->grouplist, groupname); + + if(update_config){ + dynsec__config_save(); + } + return MOSQ_ERR_SUCCESS; +} + +int dynsec_groups__process_remove_client(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *username, *groupname; + int rc; + + if(json_get_string(command, "username", &username, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "removeGroupClient", "Invalid/missing username", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "removeGroupClient", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + + rc = dynsec_groups__remove_client(username, groupname, true); + if(rc == MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "removeGroupClient", NULL, correlation_data); + }else if(rc == ERR_USER_NOT_FOUND){ + dynsec__command_reply(j_responses, context, "removeGroupClient", "Client not found", correlation_data); + }else if(rc == ERR_GROUP_NOT_FOUND){ + dynsec__command_reply(j_responses, context, "removeGroupClient", "Group not found", correlation_data); + }else{ + dynsec__command_reply(j_responses, context, "removeGroupClient", "Internal error", correlation_data); + } + return rc; +} + + +static cJSON *add_group_to_json(struct dynsec__group *group) +{ + cJSON *j_group, *jtmp, *j_clientlist, *j_client, *j_rolelist; + struct dynsec__clientlist *clientlist, *clientlist_tmp; + + j_group = cJSON_CreateObject(); + if(j_group == NULL){ + return NULL; + } + + if(cJSON_AddStringToObject(j_group, "groupname", group->groupname) == NULL + || (group->text_name && cJSON_AddStringToObject(j_group, "textname", group->text_name) == NULL) + || (group->text_description && cJSON_AddStringToObject(j_group, "textdescription", group->text_description) == NULL) + || (j_clientlist = cJSON_AddArrayToObject(j_group, "clients")) == NULL + ){ + + cJSON_Delete(j_group); + return NULL; + } + + HASH_ITER(hh, group->clientlist, clientlist, clientlist_tmp){ + j_client = cJSON_CreateObject(); + if(j_client == NULL){ + cJSON_Delete(j_group); + return NULL; + } + cJSON_AddItemToArray(j_clientlist, j_client); + + jtmp = cJSON_CreateStringReference(clientlist->username); + if(jtmp == NULL){ + cJSON_Delete(j_group); + return NULL; + } + cJSON_AddItemToObject(j_client, "username", jtmp); + } + + j_rolelist = dynsec_rolelists__all_to_json(group->rolelist); + if(j_rolelist == NULL){ + cJSON_Delete(j_group); + return NULL; + } + cJSON_AddItemToObject(j_group, "roles", j_rolelist); + + return j_group; +} + + +int dynsec_groups__process_list(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + bool verbose; + cJSON *tree, *j_groups, *j_group, *jtmp, *j_data; + struct dynsec__group *group, *group_tmp; + int i, count, offset; + char buf[30]; + + json_get_bool(command, "verbose", &verbose, true, false); + json_get_int(command, "count", &count, true, -1); + json_get_int(command, "offset", &offset, true, 0); + + tree = cJSON_CreateObject(); + if(tree == NULL){ + dynsec__command_reply(j_responses, context, "listGroups", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + + jtmp = cJSON_CreateString("listGroups"); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listGroups", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "command", jtmp); + + j_data = cJSON_CreateObject(); + if(j_data == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listGroups", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "data", j_data); + + snprintf(buf, sizeof(buf), "%d", HASH_CNT(hh, local_groups)); + cJSON_AddRawToObject(j_data, "totalCount", buf); + + j_groups = cJSON_CreateArray(); + if(j_groups == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listGroups", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(j_data, "groups", j_groups); + + i = 0; + HASH_ITER(hh, local_groups, group, group_tmp){ + if(i>=offset){ + if(verbose){ + j_group = add_group_to_json(group); + if(j_group == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listGroups", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToArray(j_groups, j_group); + + }else{ + j_group = cJSON_CreateString(group->groupname); + if(j_group){ + cJSON_AddItemToArray(j_groups, j_group); + }else{ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listGroups", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + } + + if(count >= 0){ + count--; + if(count <= 0){ + break; + } + } + } + i++; + } + if(correlation_data){ + jtmp = cJSON_CreateString(correlation_data); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listGroups", "Internal error", correlation_data); + return 1; + } + cJSON_AddItemToObject(tree, "correlationData", jtmp); + } + + cJSON_AddItemToArray(j_responses, tree); + + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_groups__process_get(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *groupname; + cJSON *tree, *j_group, *jtmp, *j_data; + struct dynsec__group *group; + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "getGroup", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + + tree = cJSON_CreateObject(); + if(tree == NULL){ + dynsec__command_reply(j_responses, context, "getGroup", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + + jtmp = cJSON_CreateString("getGroup"); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getGroup", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "command", jtmp); + + j_data = cJSON_CreateObject(); + if(j_data == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getGroup", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "data", j_data); + + group = dynsec_groups__find(groupname); + if(group){ + j_group = add_group_to_json(group); + if(j_group == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getGroup", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(j_data, "group", j_group); + } + if(correlation_data){ + jtmp = cJSON_CreateString(correlation_data); + if(jtmp == NULL){ + dynsec__command_reply(j_responses, context, "getGroup", "Internal error", correlation_data); + cJSON_Delete(tree); + return 1; + } + cJSON_AddItemToObject(tree, "correlationData", jtmp); + } + + cJSON_AddItemToArray(j_responses, tree); + + return MOSQ_ERR_SUCCESS; +} + + +void dynsec_groups__remove_role_from_all(const struct dynsec__role *role) +{ + struct dynsec__group *group, *group_tmp; + struct dynsec__rolelist *rolelist; + + HASH_ITER(hh, local_groups, group, group_tmp){ + HASH_FIND(hh, group->rolelist, role->rolename, strlen(role->rolename), rolelist); + if(rolelist){ + HASH_DELETE(hh, group->rolelist, rolelist); + mosquitto_free(rolelist->rolename); + mosquitto_free(rolelist); + } + } +} + + +int dynsec_groups__process_remove_role(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *groupname, *rolename; + struct dynsec__group *group; + struct dynsec__role *role; + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "removeGroupRole", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "roleName", &rolename, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "removeGroupRole", "Invalid/missing roleName", correlation_data); + return MOSQ_ERR_INVAL; + } + + group = dynsec_groups__find(groupname); + if(group == NULL){ + dynsec__command_reply(j_responses, context, "removeGroupRole", "Group not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + role = dynsec_roles__find(rolename); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "removeGroupRole", "Role not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + dynsec_rolelists__remove_role(&group->rolelist, role); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "removeGroupRole", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_groups__process_modify(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *groupname; + char *text_name, *text_description; + struct dynsec__group *group; + struct dynsec__rolelist *rolelist = NULL; + char *str; + int rc; + int priority; + cJSON *j_client, *j_clients, *jtmp; + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "modifyGroup", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + + group = dynsec_groups__find(groupname); + if(group == NULL){ + dynsec__command_reply(j_responses, context, "modifyGroup", "Group does not exist", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textname", &text_name, true) == MOSQ_ERR_SUCCESS){ + str = mosquitto_strdup(text_name); + if(str == NULL){ + dynsec__command_reply(j_responses, context, "modifyGroup", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + mosquitto_free(group->text_name); + group->text_name = str; + } + + if(json_get_string(command, "textdescription", &text_description, true) == MOSQ_ERR_SUCCESS){ + str = mosquitto_strdup(text_description); + if(str == NULL){ + dynsec__command_reply(j_responses, context, "modifyGroup", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + mosquitto_free(group->text_description); + group->text_description = str; + } + + rc = dynsec_rolelists__load_from_json(command, &rolelist); + if(rc == MOSQ_ERR_SUCCESS){ + dynsec_rolelists__free_all(&group->rolelist); + group->rolelist = rolelist; + }else if(rc == MOSQ_ERR_NOT_FOUND){ + dynsec__command_reply(j_responses, context, "modifyGroup", "Role not found", correlation_data); + dynsec_rolelists__free_all(&rolelist); + return MOSQ_ERR_INVAL; + }else if(rc == ERR_LIST_NOT_FOUND){ + /* There was no list in the JSON, so no modification */ + }else{ + dynsec__command_reply(j_responses, context, "modifyGroup", "Internal error", correlation_data); + dynsec_rolelists__free_all(&rolelist); + return MOSQ_ERR_INVAL; + } + + j_clients = cJSON_GetObjectItem(command, "clients"); + if(j_clients && cJSON_IsArray(j_clients)){ + dynsec__remove_all_clients_from_group(group); + + cJSON_ArrayForEach(j_client, j_clients){ + if(cJSON_IsObject(j_client)){ + jtmp = cJSON_GetObjectItem(j_client, "username"); + if(jtmp && cJSON_IsString(jtmp)){ + json_get_int(j_client, "priority", &priority, true, -1); + dynsec_groups__add_client(jtmp->valuestring, groupname, priority, false); + } + } + } + } + + dynsec__config_save(); + + dynsec__command_reply(j_responses, context, "modifyGroup", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_groups__process_set_anonymous_group(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *groupname; + struct dynsec__group *group = NULL; + + if(json_get_string(command, "groupname", &groupname, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "setAnonymousGroup", "Invalid/missing groupname", correlation_data); + return MOSQ_ERR_INVAL; + } + + group = dynsec_groups__find(groupname); + if(group == NULL){ + dynsec__command_reply(j_responses, context, "setAnonymousGroup", "Group not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + dynsec_anonymous_group = group; + + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "setAnonymousGroup", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} diff --git a/src/json_help.c b/plugins/dynamic-security/json_help.c similarity index 91% rename from src/json_help.c rename to plugins/dynamic-security/json_help.c index 6516bdd6..16506c08 100644 --- a/src/json_help.c +++ b/plugins/dynamic-security/json_help.c @@ -18,8 +18,9 @@ Contributors: #include #include +#include -#include "mosquitto_broker_internal.h" +#include "mosquitto.h" int json_get_bool(cJSON *json, const char *name, bool *value, bool optional, bool default_value) @@ -30,7 +31,7 @@ int json_get_bool(cJSON *json, const char *name, bool *value, bool optional, boo *value = default_value; } - jtmp = cJSON_GetObjectItemCaseSensitive(json, name); + jtmp = cJSON_GetObjectItem(json, name); if(jtmp){ if(cJSON_IsBool(jtmp) == false){ return MOSQ_ERR_INVAL; @@ -53,7 +54,7 @@ int json_get_int(cJSON *json, const char *name, int *value, bool optional, int d *value = default_value; } - jtmp = cJSON_GetObjectItemCaseSensitive(json, name); + jtmp = cJSON_GetObjectItem(json, name); if(jtmp){ if(cJSON_IsNumber(jtmp) == false){ return MOSQ_ERR_INVAL; @@ -74,7 +75,7 @@ int json_get_string(cJSON *json, const char *name, char **value, bool optional) *value = NULL; - jtmp = cJSON_GetObjectItemCaseSensitive(json, name); + jtmp = cJSON_GetObjectItem(json, name); if(jtmp){ if(cJSON_IsString(jtmp) == false){ return MOSQ_ERR_INVAL; diff --git a/src/json_help.h b/plugins/dynamic-security/json_help.h similarity index 100% rename from src/json_help.h rename to plugins/dynamic-security/json_help.h diff --git a/plugins/dynamic-security/plugin.c b/plugins/dynamic-security/plugin.c new file mode 100644 index 00000000..8ca5506e --- /dev/null +++ b/plugins/dynamic-security/plugin.c @@ -0,0 +1,485 @@ +/* +Copyright (c) 2020 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "json_help.h" +#include "mosquitto.h" +#include "mosquitto_broker.h" +#include "mosquitto_plugin.h" + +#include "dynamic_security.h" + +static mosquitto_plugin_id_t *plg_id = NULL; +static char *config_file = NULL; +struct dynsec__acl_default_access default_access = {false, false, false, false}; + +void dynsec__command_reply(cJSON *j_responses, struct mosquitto *context, const char *command, const char *error, const char *correlation_data) +{ + cJSON *j_response; + + j_response = cJSON_CreateObject(); + if(j_response == NULL) return; + + if(cJSON_AddStringToObject(j_response, "command", command) == NULL + || (error && cJSON_AddStringToObject(j_response, "error", error) == NULL) + || (correlation_data && cJSON_AddStringToObject(j_response, "correlationData", correlation_data) == NULL) + ){ + + cJSON_Delete(j_response); + return; + } + + cJSON_AddItemToArray(j_responses, j_response); +} + + +static void send_response(cJSON *tree) +{ + char *payload; + + payload = cJSON_PrintUnformatted(tree); + cJSON_Delete(tree); + if(payload == NULL) return; + + mosquitto_broker_publish(NULL, "$CONTROL/dynamic-security/v1/response", + strlen(payload), payload, 0, 0, NULL); +} + + +static int dynsec_control_callback(int event, void *event_data, void *userdata) +{ + struct mosquitto_evt_control *ed = event_data; + cJSON *tree, *commands; + cJSON *j_response_tree, *j_responses; + + /* Create object for responses */ + j_response_tree = cJSON_CreateObject(); + if(j_response_tree == NULL){ + return MOSQ_ERR_NOMEM; + } + j_responses = cJSON_CreateArray(); + if(j_responses == NULL){ + cJSON_Delete(j_response_tree); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(j_response_tree, "responses", j_responses); + + + /* Parse cJSON tree */ + tree = cJSON_ParseWithLength(ed->payload, ed->payloadlen); + if(tree == NULL){ + dynsec__command_reply(j_responses, ed->client, "Unknown command", "Payload not valid JSON", NULL); + send_response(j_response_tree); + return MOSQ_ERR_SUCCESS; + } + commands = cJSON_GetObjectItem(tree, "commands"); + if(commands == NULL || !cJSON_IsArray(commands)){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, ed->client, "Unknown command", "Invalid/missing commands", NULL); + send_response(j_response_tree); + return MOSQ_ERR_SUCCESS; + } + + /* Handle commands */ + dynsec__handle_control(j_responses, ed->client, commands); + cJSON_Delete(tree); + + send_response(j_response_tree); + + return MOSQ_ERR_SUCCESS; +} + +int dynsec__process_default_acl_access(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + cJSON *j_actions, *j_action, *j_acltype, *j_allow; + bool allow; + + j_actions = cJSON_GetObjectItem(command, "acls"); + if(j_actions == NULL || !cJSON_IsArray(j_actions)){ + dynsec__command_reply(j_responses, context, "setDefaultACLAccess", "Missing/invalid actions array", correlation_data); + return MOSQ_ERR_INVAL; + } + + cJSON_ArrayForEach(j_action, j_actions){ + j_acltype = cJSON_GetObjectItem(j_action, "acltype"); + j_allow = cJSON_GetObjectItem(j_action, "allow"); + if(j_acltype && cJSON_IsString(j_acltype) + && j_allow && cJSON_IsBool(j_allow)){ + + allow = cJSON_IsTrue(j_allow); + + if(!strcasecmp(j_acltype->valuestring, "publishClientToBroker")){ + default_access.publish_c2b = allow; + }else if(!strcasecmp(j_acltype->valuestring, "publishBrokerToClient")){ + default_access.publish_b2c = allow; + }else if(!strcasecmp(j_acltype->valuestring, "subscribe")){ + default_access.subscribe = allow; + }else if(!strcasecmp(j_acltype->valuestring, "unsubscribe")){ + default_access.unsubscribe = allow; + } + } + } + + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "setDefaultACLAccess", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} + + +int mosquitto_plugin_version(int supported_version_count, const int *supported_versions) +{ + int i; + + for(i=0; i + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License v1.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + http://www.eclipse.org/legal/epl-v10.html +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include +#include +#include + +#include "dynamic_security.h" +#include "json_help.h" +#include "mosquitto.h" +#include "mosquitto_broker.h" + + +static cJSON *add_role_to_json(struct dynsec__role *role, bool verbose); + +/* ################################################################ + * # + * # Local variables + * # + * ################################################################ */ + +static struct dynsec__role *local_roles = NULL; + + +/* ################################################################ + * # + * # Utility functions + * # + * ################################################################ */ + +static int role_cmp(void *a, void *b) +{ + struct dynsec__role *role_a = a; + struct dynsec__role *role_b = b; + + return strcmp(role_a->rolename, role_b->rolename); +} + +static int rolelist_cmp(void *a, void *b) +{ + struct dynsec__rolelist *rolelist_a = a; + struct dynsec__rolelist *rolelist_b = b; + + return rolelist_b->priority - rolelist_a->priority; +} + + +void dynsec_rolelists__free_item(struct dynsec__rolelist **base_rolelist, struct dynsec__rolelist *rolelist) +{ + HASH_DELETE(hh, *base_rolelist, rolelist); + mosquitto_free(rolelist->rolename); + mosquitto_free(rolelist); +} + +void dynsec_rolelists__free_all(struct dynsec__rolelist **base_rolelist) +{ + struct dynsec__rolelist *rolelist, *rolelist_tmp; + + HASH_ITER(hh, *base_rolelist, rolelist, rolelist_tmp){ + dynsec_rolelists__free_item(base_rolelist, rolelist); + } +} + +int dynsec_rolelists__remove_role(struct dynsec__rolelist **base_rolelist, const struct dynsec__role *role) +{ + struct dynsec__rolelist *found_rolelist; + + HASH_FIND(hh, *base_rolelist, role->rolename, strlen(role->rolename), found_rolelist); + if(found_rolelist){ + dynsec_rolelists__free_item(base_rolelist, found_rolelist); + return MOSQ_ERR_SUCCESS; + }else{ + return MOSQ_ERR_NOT_FOUND; + } +} + + +int dynsec_rolelists__add_role(struct dynsec__rolelist **base_rolelist, struct dynsec__role *role, int priority) +{ + struct dynsec__rolelist *rolelist; + + if(role == NULL) return MOSQ_ERR_INVAL; + + HASH_FIND(hh, *base_rolelist, role->rolename, strlen(role->rolename), rolelist); + if(rolelist){ + return MOSQ_ERR_ALREADY_EXISTS; + }else{ + rolelist = mosquitto_calloc(1, sizeof(struct dynsec__rolelist)); + if(rolelist == NULL) return MOSQ_ERR_NOMEM; + + rolelist->role = role; + rolelist->priority = priority; + rolelist->rolename = mosquitto_strdup(role->rolename); + if(rolelist->rolename == NULL){ + mosquitto_free(rolelist); + return MOSQ_ERR_NOMEM; + } + HASH_ADD_KEYPTR_INORDER(hh, *base_rolelist, role->rolename, strlen(role->rolename), rolelist, rolelist_cmp); + return MOSQ_ERR_SUCCESS; + } +} + + +int dynsec_rolelists__load_from_json(cJSON *command, struct dynsec__rolelist **rolelist) +{ + cJSON *j_roles, *j_role, *j_rolename; + int priority; + struct dynsec__role *role; + + j_roles = cJSON_GetObjectItem(command, "roles"); + if(j_roles && cJSON_IsArray(j_roles)){ + cJSON_ArrayForEach(j_role, j_roles){ + j_rolename = cJSON_GetObjectItem(j_role, "rolename"); + if(j_rolename && cJSON_IsString(j_rolename)){ + json_get_int(j_role, "priority", &priority, true, -1); + role = dynsec_roles__find(j_rolename->valuestring); + if(role){ + dynsec_rolelists__add_role(rolelist, role, priority); + }else{ + dynsec_rolelists__free_all(rolelist); + return MOSQ_ERR_NOT_FOUND; + } + } + } + return MOSQ_ERR_SUCCESS; + }else{ + return ERR_LIST_NOT_FOUND; + } +} + + +cJSON *dynsec_rolelists__all_to_json(struct dynsec__rolelist *base_rolelist) +{ + struct dynsec__rolelist *rolelist, *rolelist_tmp; + cJSON *j_roles, *j_role; + char buf[30]; + + j_roles = cJSON_CreateArray(); + if(j_roles == NULL) return NULL; + + HASH_ITER(hh, base_rolelist, rolelist, rolelist_tmp){ + j_role = cJSON_CreateObject(); + if(j_role == NULL){ + cJSON_Delete(j_roles); + return NULL; + } + cJSON_AddItemToArray(j_roles, j_role); + + if(rolelist->priority != -1){ + snprintf(buf, sizeof(buf), "%d", rolelist->priority); + } + if(cJSON_AddStringToObject(j_role, "rolename", rolelist->role->rolename) == NULL + || (rolelist->priority != -1 && cJSON_AddRawToObject(j_role, "priority", buf) == NULL) + ){ + + cJSON_Delete(j_roles); + return NULL; + } + } + return j_roles; +} + + +static void role__free_acl(struct dynsec__acl **acl, struct dynsec__acl *item) +{ + HASH_DELETE(hh, *acl, item); + mosquitto_free(item->topic); + mosquitto_free(item); +} + +static void role__free_all_acls(struct dynsec__acl **acl) +{ + struct dynsec__acl *iter, *tmp; + + HASH_ITER(hh, *acl, iter, tmp){ + role__free_acl(acl, iter); + } +} + +static void role__free_item(struct dynsec__role *role, bool remove_from_hash) +{ + if(remove_from_hash){ + HASH_DEL(local_roles, role); + } + mosquitto_free(role->text_name); + mosquitto_free(role->text_description); + mosquitto_free(role->rolename); + role__free_all_acls(&role->acls.publish_c2b); + role__free_all_acls(&role->acls.publish_b2c); + role__free_all_acls(&role->acls.subscribe_literal); + role__free_all_acls(&role->acls.subscribe_pattern); + role__free_all_acls(&role->acls.unsubscribe_literal); + role__free_all_acls(&role->acls.unsubscribe_pattern); + mosquitto_free(role); +} + +struct dynsec__role *dynsec_roles__find(const char *rolename) +{ + struct dynsec__role *role = NULL; + + if(rolename){ + HASH_FIND(hh, local_roles, rolename, strlen(rolename), role); + } + return role; +} + + +void dynsec_roles__cleanup(void) +{ + struct dynsec__role *role, *role_tmp; + + HASH_ITER(hh, local_roles, role, role_tmp){ + role__free_item(role, true); + } +} + + +/* ################################################################ + * # + * # Config file load and save + * # + * ################################################################ */ + + +static int add_single_acl_to_json(cJSON *j_array, const char *acl_type, struct dynsec__acl *acl) +{ + struct dynsec__acl *iter, *tmp; + cJSON *j_acl; + + HASH_ITER(hh, acl, iter, tmp){ + j_acl = cJSON_CreateObject(); + if(j_acl == NULL){ + return 1; + } + cJSON_AddItemToArray(j_array, j_acl); + + if(cJSON_AddStringToObject(j_acl, "acltype", acl_type) == NULL + || cJSON_AddStringToObject(j_acl, "topic", iter->topic) == NULL + || cJSON_AddNumberToObject(j_acl, "priority", iter->priority) == NULL + || cJSON_AddBoolToObject(j_acl, "allow", iter->allow) == NULL + ){ + + return 1; + } + } + + + return 0; +} + +static int add_acls_to_json(cJSON *j_role, struct dynsec__role *role) +{ + cJSON *j_acls; + + if((j_acls = cJSON_AddArrayToObject(j_role, "acls")) == NULL){ + return 1; + } + + if(add_single_acl_to_json(j_acls, "publishClientToBroker", role->acls.publish_c2b) != MOSQ_ERR_SUCCESS + || add_single_acl_to_json(j_acls, "publishBrokerToClient", role->acls.publish_b2c) != MOSQ_ERR_SUCCESS + || add_single_acl_to_json(j_acls, "subscribeLiteral", role->acls.subscribe_literal) != MOSQ_ERR_SUCCESS + || add_single_acl_to_json(j_acls, "subscribePattern", role->acls.subscribe_pattern) != MOSQ_ERR_SUCCESS + || add_single_acl_to_json(j_acls, "unsubscribeLiteral", role->acls.unsubscribe_literal) != MOSQ_ERR_SUCCESS + || add_single_acl_to_json(j_acls, "unsubscribePattern", role->acls.unsubscribe_pattern) != MOSQ_ERR_SUCCESS + ){ + + return 1; + } + return 0; +} + +int dynsec_roles__config_save(cJSON *tree) +{ + cJSON *j_roles, *j_role; + struct dynsec__role *role, *role_tmp; + + j_roles = cJSON_CreateArray(); + if(j_roles == NULL){ + return 1; + } + cJSON_AddItemToObject(tree, "roles", j_roles); + + HASH_ITER(hh, local_roles, role, role_tmp){ + j_role = add_role_to_json(role, true); + if(j_role == NULL){ + return 1; + } + cJSON_AddItemToArray(j_roles, j_role); + } + + return 0; +} + + +static int insert_acl_cmp(struct dynsec__acl *a, struct dynsec__acl *b) +{ + return b->priority - a->priority; +} + + +int dynsec_roles__acl_load(cJSON *j_acls, const char *key, struct dynsec__acl **acllist) +{ + cJSON *j_acl, *j_type, *jtmp; + struct dynsec__acl *acl; + + cJSON_ArrayForEach(j_acl, j_acls){ + j_type = cJSON_GetObjectItem(j_acl, "acltype"); + if(j_type == NULL || !cJSON_IsString(j_type) || strcasecmp(j_type->valuestring, key) != 0){ + continue; + } + acl = mosquitto_calloc(1, sizeof(struct dynsec__acl)); + if(acl == NULL){ + return 1; + } + + json_get_int(j_acl, "priority", &acl->priority, true, 0); + json_get_bool(j_acl, "allow", &acl->allow, true, false); + + jtmp = cJSON_GetObjectItem(j_acl, "allow"); + if(jtmp && cJSON_IsBool(jtmp)){ + acl->allow = cJSON_IsTrue(jtmp); + } + + jtmp = cJSON_GetObjectItem(j_acl, "topic"); + if(jtmp && cJSON_IsString(jtmp)){ + acl->topic = mosquitto_strdup(jtmp->valuestring); + } + + if(acl->topic == NULL){ + mosquitto_free(acl); + continue; + } + + HASH_ADD_KEYPTR_INORDER(hh, *acllist, acl->topic, strlen(acl->topic), acl, insert_acl_cmp); + } + + return 0; +} + + +int dynsec_roles__config_load(cJSON *tree) +{ + cJSON *j_roles, *j_role, *jtmp, *j_acls; + struct dynsec__role *role; + + j_roles = cJSON_GetObjectItem(tree, "roles"); + if(j_roles == NULL){ + return 0; + } + + if(cJSON_IsArray(j_roles) == false){ + return 1; + } + + cJSON_ArrayForEach(j_role, j_roles){ + if(cJSON_IsObject(j_role) == true){ + role = mosquitto_calloc(1, sizeof(struct dynsec__role)); + if(role == NULL){ + // FIXME log + return MOSQ_ERR_NOMEM; + } + + /* Role name */ + jtmp = cJSON_GetObjectItem(j_role, "rolename"); + if(jtmp == NULL){ + // FIXME log + mosquitto_free(role); + continue; + } + role->rolename = mosquitto_strdup(jtmp->valuestring); + if(role->rolename == NULL){ + // FIXME log + mosquitto_free(role); + continue; + } + + /* Text name */ + jtmp = cJSON_GetObjectItem(j_role, "textname"); + if(jtmp != NULL){ + role->text_name = mosquitto_strdup(jtmp->valuestring); + if(role->text_name == NULL){ + // FIXME log + mosquitto_free(role->rolename); + mosquitto_free(role); + continue; + } + } + + /* Text description */ + jtmp = cJSON_GetObjectItem(j_role, "textdescription"); + if(jtmp != NULL){ + role->text_description = mosquitto_strdup(jtmp->valuestring); + if(role->text_description == NULL){ + // FIXME log + mosquitto_free(role->text_name); + mosquitto_free(role->rolename); + mosquitto_free(role); + continue; + } + } + + /* ACLs */ + j_acls = cJSON_GetObjectItem(j_role, "acls"); + if(j_acls && cJSON_IsArray(j_acls)){ + if(dynsec_roles__acl_load(j_acls, "publishClientToBroker", &role->acls.publish_c2b) != 0 + || dynsec_roles__acl_load(j_acls, "publishBrokerToClient", &role->acls.publish_b2c) != 0 + || dynsec_roles__acl_load(j_acls, "subscribeLiteral", &role->acls.subscribe_literal) != 0 + || dynsec_roles__acl_load(j_acls, "subscribePattern", &role->acls.subscribe_pattern) != 0 + || dynsec_roles__acl_load(j_acls, "unsubscribeLiteral", &role->acls.unsubscribe_literal) != 0 + || dynsec_roles__acl_load(j_acls, "unsubscribePattern", &role->acls.unsubscribe_pattern) != 0 + ){ + + // FIXME log + mosquitto_free(role->rolename); + mosquitto_free(role); + continue; + } + } + + HASH_ADD_KEYPTR(hh, local_roles, role->rolename, strlen(role->rolename), role); + } + } + HASH_SORT(local_roles, role_cmp); + + return 0; +} + + +int dynsec_roles__process_create(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *rolename; + char *text_name, *text_description; + struct dynsec__role *role; + int rc = MOSQ_ERR_SUCCESS; + cJSON *j_acls; + + if(json_get_string(command, "rolename", &rolename, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createRole", "Invalid/missing rolename", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textname", &text_name, true) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createRole", "Invalid/missing textname", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textdescription", &text_description, true) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createRole", "Invalid/missing textdescription", correlation_data); + return MOSQ_ERR_INVAL; + } + + role = dynsec_roles__find(rolename); + if(role){ + dynsec__command_reply(j_responses, context, "createRole", "Role already exists", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + role = mosquitto_calloc(1, sizeof(struct dynsec__role)); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "createRole", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + role->rolename = mosquitto_strdup(rolename); + if(role->rolename == NULL){ + dynsec__command_reply(j_responses, context, "createRole", "Internal error", correlation_data); + rc = MOSQ_ERR_NOMEM; + goto error; + } + if(text_name){ + role->text_name = mosquitto_strdup(text_name); + if(role->text_name == NULL){ + dynsec__command_reply(j_responses, context, "createRole", "Internal error", correlation_data); + rc = MOSQ_ERR_NOMEM; + goto error; + } + } + if(text_description){ + role->text_description = mosquitto_strdup(text_description); + if(role->text_description == NULL){ + dynsec__command_reply(j_responses, context, "createRole", "Internal error", correlation_data); + rc = MOSQ_ERR_NOMEM; + goto error; + } + } + + /* ACLs */ + j_acls = cJSON_GetObjectItem(command, "acls"); + if(j_acls && cJSON_IsArray(j_acls)){ + if(dynsec_roles__acl_load(j_acls, "publishClientToBroker", &role->acls.publish_c2b) != 0 + || dynsec_roles__acl_load(j_acls, "publishBrokerToClient", &role->acls.publish_b2c) != 0 + || dynsec_roles__acl_load(j_acls, "subscribeLiteral", &role->acls.subscribe_literal) != 0 + || dynsec_roles__acl_load(j_acls, "subscribePattern", &role->acls.subscribe_pattern) != 0 + || dynsec_roles__acl_load(j_acls, "unsubscribeLiteral", &role->acls.unsubscribe_literal) != 0 + || dynsec_roles__acl_load(j_acls, "unsubscribePattern", &role->acls.unsubscribe_pattern) != 0 + ){ + + dynsec__command_reply(j_responses, context, "createRole", "Internal error", correlation_data); + rc = MOSQ_ERR_NOMEM; + goto error; + } + } + + + HASH_ADD_KEYPTR_INORDER(hh, local_roles, role->rolename, strlen(role->rolename), role, role_cmp); + + dynsec__config_save(); + + dynsec__command_reply(j_responses, context, "createRole", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +error: + if(role){ + role__free_item(role, false); + } + return rc; +} + + +int dynsec_roles__process_delete(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *rolename; + struct dynsec__role *role; + + if(json_get_string(command, "rolename", &rolename, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "deleteRole", "Invalid/missing rolename", correlation_data); + return MOSQ_ERR_INVAL; + } + + role = dynsec_roles__find(rolename); + if(role){ + dynsec_clients__remove_role_from_all(role); + dynsec_groups__remove_role_from_all(role); + role__free_item(role, true); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "deleteRole", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; + }else{ + dynsec__command_reply(j_responses, context, "deleteRole", "Role not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } +} + + +static cJSON *add_role_to_json(struct dynsec__role *role, bool verbose) +{ + cJSON *j_role = NULL; + + if(verbose){ + j_role = cJSON_CreateObject(); + if(j_role == NULL){ + return NULL; + } + + if(cJSON_AddStringToObject(j_role, "rolename", role->rolename) == NULL + || (role->text_name && cJSON_AddStringToObject(j_role, "textname", role->text_name) == NULL) + || (role->text_description && cJSON_AddStringToObject(j_role, "textdescription", role->text_description) == NULL) + ){ + + cJSON_Delete(j_role); + return NULL; + } + if(add_acls_to_json(j_role, role)){ + cJSON_Delete(j_role); + return NULL; + } + }else{ + j_role = cJSON_CreateString(role->rolename); + if(j_role == NULL){ + return NULL; + } + } + return j_role; +} + +int dynsec_roles__process_list(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + bool verbose; + struct dynsec__role *role, *role_tmp; + cJSON *tree, *j_roles, *j_role, *jtmp, *j_data; + int i, count, offset; + char buf[30]; + + json_get_bool(command, "verbose", &verbose, true, false); + json_get_int(command, "count", &count, true, -1); + json_get_int(command, "offset", &offset, true, 0); + + tree = cJSON_CreateObject(); + if(tree == NULL){ + dynsec__command_reply(j_responses, context, "listRoles", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + + jtmp = cJSON_CreateString("listRoles"); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listRoles", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "command", jtmp); + + j_data = cJSON_CreateObject(); + if(j_data == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listRoles", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "data", j_data); + + snprintf(buf, sizeof(buf), "%d", HASH_CNT(hh, local_roles)); + cJSON_AddRawToObject(j_data, "totalCount", buf); + + j_roles = cJSON_CreateArray(); + if(j_roles == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listRoles", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(j_data, "roles", j_roles); + + i = 0; + HASH_ITER(hh, local_roles, role, role_tmp){ + if(i>=offset){ + j_role = add_role_to_json(role, verbose); + if(j_role == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listRoles", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToArray(j_roles, j_role); + + if(count >= 0){ + count--; + if(count <= 0){ + break; + } + } + } + i++; + } + if(correlation_data){ + jtmp = cJSON_CreateString(correlation_data); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "listRoles", "Internal error", correlation_data); + return 1; + } + cJSON_AddItemToObject(tree, "correlationData", jtmp); + } + + cJSON_AddItemToArray(j_responses, tree); + + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_roles__process_add_acl(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *rolename; + char *topic; + struct dynsec__role *role; + cJSON *jtmp; + struct dynsec__acl **acllist, *acl; + + if(json_get_string(command, "rolename", &rolename, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "addRoleACL", "Invalid/missing rolename", correlation_data); + return MOSQ_ERR_INVAL; + } + role = dynsec_roles__find(rolename); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "addRoleACL", "Role not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + jtmp = cJSON_GetObjectItem(command, "acltype"); + if(jtmp == NULL || !cJSON_IsString(jtmp)){ + dynsec__command_reply(j_responses, context, "addRoleACL", "Invalid/missing acltype", correlation_data); + return MOSQ_ERR_SUCCESS; + } + if(!strcasecmp(jtmp->valuestring, "publishClientToBroker")){ + acllist = &role->acls.publish_c2b; + }else if(!strcasecmp(jtmp->valuestring, "publishBrokerToClient")){ + acllist = &role->acls.publish_b2c; + }else if(!strcasecmp(jtmp->valuestring, "subscribeLiteral")){ + acllist = &role->acls.subscribe_literal; + }else if(!strcasecmp(jtmp->valuestring, "subscribePattern")){ + acllist = &role->acls.subscribe_pattern; + }else if(!strcasecmp(jtmp->valuestring, "unsubscribeLiteral")){ + acllist = &role->acls.unsubscribe_literal; + }else if(!strcasecmp(jtmp->valuestring, "unsubscribePattern")){ + acllist = &role->acls.unsubscribe_pattern; + }else{ + dynsec__command_reply(j_responses, context, "addRoleACL", "Unknown acltype", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + jtmp = cJSON_GetObjectItem(command, "topic"); + if(jtmp && cJSON_IsString(jtmp)){ + topic = mosquitto_strdup(jtmp->valuestring); + if(topic == NULL){ + dynsec__command_reply(j_responses, context, "addRoleACL", "Internal error", correlation_data); + return MOSQ_ERR_SUCCESS; + } + }else{ + dynsec__command_reply(j_responses, context, "addRoleACL", "Invalid/missing topic", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + HASH_FIND(hh, *acllist, topic, strlen(topic), acl); + if(acl){ + mosquitto_free(topic); + dynsec__command_reply(j_responses, context, "addRoleACL", "ACL with this topic already exists", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + acl = mosquitto_calloc(1, sizeof(struct dynsec__acl)); + if(acl == NULL){ + dynsec__command_reply(j_responses, context, "addRoleACL", "Internal error", correlation_data); + return MOSQ_ERR_SUCCESS; + } + acl->topic = topic; + + json_get_int(command, "priority", &acl->priority, true, 0); + json_get_bool(command, "allow", &acl->allow, true, false); + + HASH_ADD_KEYPTR_INORDER(hh, *acllist, acl->topic, strlen(acl->topic), acl, insert_acl_cmp); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "addRoleACL", NULL, correlation_data); + + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_roles__process_remove_acl(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *rolename; + struct dynsec__role *role; + struct dynsec__acl **acllist, *acl; + char *topic; + cJSON *jtmp; + + if(json_get_string(command, "rolename", &rolename, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "createRole", "Invalid/missing rolename", correlation_data); + return MOSQ_ERR_INVAL; + } + role = dynsec_roles__find(rolename); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "removeRoleACL", "Role not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + jtmp = cJSON_GetObjectItem(command, "acltype"); + if(jtmp == NULL || !cJSON_IsString(jtmp)){ + dynsec__command_reply(j_responses, context, "removeRoleACL", "Invalid/missing acltype", correlation_data); + return MOSQ_ERR_SUCCESS; + } + if(!strcasecmp(jtmp->valuestring, "publishClientToBroker")){ + acllist = &role->acls.publish_c2b; + }else if(!strcasecmp(jtmp->valuestring, "publishBrokerToClient")){ + acllist = &role->acls.publish_b2c; + }else if(!strcasecmp(jtmp->valuestring, "subscribeLiteral")){ + acllist = &role->acls.subscribe_literal; + }else if(!strcasecmp(jtmp->valuestring, "subscribePattern")){ + acllist = &role->acls.subscribe_pattern; + }else if(!strcasecmp(jtmp->valuestring, "unsubscribeLiteral")){ + acllist = &role->acls.unsubscribe_literal; + }else if(!strcasecmp(jtmp->valuestring, "unsubscribePattern")){ + acllist = &role->acls.unsubscribe_pattern; + }else{ + dynsec__command_reply(j_responses, context, "removeRoleACL", "Unknown acltype", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + if(json_get_string(command, "topic", &topic, false)){ + dynsec__command_reply(j_responses, context, "removeRoleACL", "Invalid/missing topic", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + HASH_FIND(hh, *acllist, topic, strlen(topic), acl); + if(acl){ + role__free_acl(acllist, acl); + dynsec__config_save(); + dynsec__command_reply(j_responses, context, "removeRoleACL", NULL, correlation_data); + }else{ + dynsec__command_reply(j_responses, context, "removeRoleACL", "ACL not found", correlation_data); + } + + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_roles__process_get(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *rolename; + struct dynsec__role *role; + cJSON *tree, *j_role, *jtmp, *j_data; + + if(json_get_string(command, "rolename", &rolename, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "getRole", "Invalid/missing rolename", correlation_data); + return MOSQ_ERR_INVAL; + } + + role = dynsec_roles__find(rolename); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "getRole", "Role not found", correlation_data); + return MOSQ_ERR_SUCCESS; + } + + tree = cJSON_CreateObject(); + if(tree == NULL){ + dynsec__command_reply(j_responses, context, "getRole", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + + jtmp = cJSON_CreateString("getRole"); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getRole", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "command", jtmp); + + j_data = cJSON_CreateObject(); + if(j_data == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getRole", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(tree, "data", j_data); + + j_role = add_role_to_json(role, true); + if(j_role == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getRole", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + cJSON_AddItemToObject(j_data, "role", j_role); + + if(correlation_data){ + jtmp = cJSON_CreateString(correlation_data); + if(jtmp == NULL){ + cJSON_Delete(tree); + dynsec__command_reply(j_responses, context, "getRole", "Internal error", correlation_data); + return 1; + } + cJSON_AddItemToObject(tree, "correlationData", jtmp); + } + + cJSON_AddItemToArray(j_responses, tree); + + return MOSQ_ERR_SUCCESS; +} + + +int dynsec_roles__process_modify(cJSON *j_responses, struct mosquitto *context, cJSON *command, char *correlation_data) +{ + char *rolename; + char *text_name, *text_description; + struct dynsec__role *role; + char *str; + cJSON *j_acls; + struct dynsec__acl *tmp_publish_c2b, *tmp_publish_b2c; + struct dynsec__acl *tmp_subscribe_literal, *tmp_subscribe_pattern; + struct dynsec__acl *tmp_unsubscribe_literal, *tmp_unsubscribe_pattern; + + if(json_get_string(command, "rolename", &rolename, false) != MOSQ_ERR_SUCCESS){ + dynsec__command_reply(j_responses, context, "modifyRole", "Invalid/missing rolename", correlation_data); + return MOSQ_ERR_INVAL; + } + + role = dynsec_roles__find(rolename); + if(role == NULL){ + dynsec__command_reply(j_responses, context, "modifyRole", "Role does not exist", correlation_data); + return MOSQ_ERR_INVAL; + } + + if(json_get_string(command, "textname", &text_name, true) == MOSQ_ERR_SUCCESS){ + str = mosquitto_strdup(text_name); + if(str == NULL){ + dynsec__command_reply(j_responses, context, "modifyRole", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + mosquitto_free(role->text_name); + role->text_name = str; + } + + if(json_get_string(command, "textdescription", &text_description, true) == MOSQ_ERR_SUCCESS){ + str = mosquitto_strdup(text_description); + if(str == NULL){ + dynsec__command_reply(j_responses, context, "modifyRole", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + mosquitto_free(role->text_description); + role->text_description = str; + } + + j_acls = cJSON_GetObjectItem(command, "acls"); + if(j_acls && cJSON_IsArray(j_acls)){ + if(dynsec_roles__acl_load(j_acls, "publishClientToBroker", &tmp_publish_c2b) != 0 + || dynsec_roles__acl_load(j_acls, "publishBrokerToClient", &tmp_publish_b2c) != 0 + || dynsec_roles__acl_load(j_acls, "subscribeLiteral", &tmp_subscribe_literal) != 0 + || dynsec_roles__acl_load(j_acls, "subscribePattern", &tmp_subscribe_pattern) != 0 + || dynsec_roles__acl_load(j_acls, "unsubscribeLiteral", &tmp_unsubscribe_literal) != 0 + || dynsec_roles__acl_load(j_acls, "unsubscribePattern", &tmp_unsubscribe_pattern) != 0 + ){ + + /* Free any that were successful */ + role__free_all_acls(&tmp_publish_c2b); + role__free_all_acls(&tmp_publish_b2c); + role__free_all_acls(&tmp_subscribe_literal); + role__free_all_acls(&tmp_subscribe_pattern); + role__free_all_acls(&tmp_unsubscribe_literal); + role__free_all_acls(&tmp_unsubscribe_pattern); + + dynsec__command_reply(j_responses, context, "modifyRole", "Internal error", correlation_data); + return MOSQ_ERR_NOMEM; + } + + role__free_all_acls(&role->acls.publish_c2b); + role__free_all_acls(&role->acls.publish_b2c); + role__free_all_acls(&role->acls.subscribe_literal); + role__free_all_acls(&role->acls.subscribe_pattern); + role__free_all_acls(&role->acls.unsubscribe_literal); + role__free_all_acls(&role->acls.unsubscribe_pattern); + + role->acls.publish_c2b = tmp_publish_c2b; + role->acls.publish_b2c = tmp_publish_b2c; + role->acls.subscribe_literal = tmp_subscribe_literal; + role->acls.subscribe_pattern = tmp_subscribe_pattern; + role->acls.unsubscribe_literal = tmp_unsubscribe_literal; + role->acls.unsubscribe_pattern = tmp_unsubscribe_pattern; + } + + dynsec__config_save(); + + dynsec__command_reply(j_responses, context, "modifyRole", NULL, correlation_data); + return MOSQ_ERR_SUCCESS; +} diff --git a/plugins/dynamic-security/sub_matches_sub.c b/plugins/dynamic-security/sub_matches_sub.c new file mode 100644 index 00000000..118d632f --- /dev/null +++ b/plugins/dynamic-security/sub_matches_sub.c @@ -0,0 +1,218 @@ +#include +#include +#include +#include + +static char *strtok_hier(char *str, char **saveptr) +{ + char *c; + + if(str != NULL){ + *saveptr = str; + } + + if(*saveptr == NULL){ + return NULL; + } + + c = strchr(*saveptr, '/'); + if(c){ + str = *saveptr; + *saveptr = c+1; + c[0] = '\0'; + }else if(*saveptr){ + /* No match, but surplus string */ + str = *saveptr; + *saveptr = NULL; + } + return str; +} + + +static int count_hier_levels(const char *s) +{ + int count = 1; + const char *c = s; + + while((c = strchr(c, '/')) && c[0]){ + c++; + count++; + } + return count; +} + + +static bool hash_check(char *s, size_t *len) +{ + if((*len) == 1 && s[0] == '#'){ + s[0] = '\0'; + (*len)--; + return true; + }else if((*len) > 1 && s[(*len)-2] == '/' && s[(*len)-1] == '#'){ + s[(*len)-2] = '\0'; + s[(*len)-1] = '\0'; + (*len) -= 2; + return true; + } + return false; +} + + +bool sub_acl_check(const char *acl, const char *sub) +{ + char *acl_local; + char *sub_local; + size_t acl_len, sub_len; + bool acl_hash = false, sub_hash = false; + int acl_levels, sub_levels; + int i; + char *acl_token, *sub_token; + char *acl_saveptr, *sub_saveptr; + + acl_len = strlen(acl); + if(acl_len == 1 && acl[0] == '#'){ + return true; + } + + sub_len = strlen(sub); + //mosquitto_validate_utf8(acl, acl_len); + + acl_local = strdup(acl); + sub_local = strdup(sub); + if(acl_local == NULL || sub_local == NULL){ + free(acl_local); + free(sub_local); + return false; + } + + acl_hash = hash_check(acl_local, &acl_len); + sub_hash = hash_check(sub_local, &sub_len); + + if(sub_hash == true && acl_hash == false){ + free(acl_local); + free(sub_local); + return false; + } + + acl_levels = count_hier_levels(acl_local); + sub_levels = count_hier_levels(sub_local); + if(acl_levels > sub_levels){ + free(acl_local); + free(sub_local); + return false; + }else if(sub_levels > acl_levels){ + if(acl_hash == false){ + free(acl_local); + free(sub_local); + return false; + } + } + + acl_saveptr = acl_local; + sub_saveptr = sub_local; + for(i=0; i=acl_levels && acl_hash == true){ + /* The sub has more levels of hierarchy than the acl, but the acl + * ends in a multi level wildcard so the match is fine. */ + }else{ + free(acl_local); + free(sub_local); + return false; + } + } + + free(acl_local); + free(sub_local); + return true; +} + + +#ifdef TEST + +#define BLK "\e[0;30m" +#define RED "\e[0;31m" +#define GRN "\e[0;32m" +#define YEL "\e[0;33m" +#define BLU "\e[0;34m" +#define MAG "\e[0;35m" +#define CYN "\e[0;36m" +#define WHT "\e[0;37m" +#define RST "\e[0m" + +void hier_test(const char *s, int expected) +{ + int levels; + + levels = count_hier_levels(s); + printf("HIER %s %d:%d ", s, expected, levels); + if(levels == expected){ + printf(GRN "passed" RST "\n"); + }else{ + printf(RED "failed" RST "\n"); + } +} + +void test(const char *sub1, const char *sub2, bool expected) +{ + bool result; + + printf("ACL %s : %s ", sub1, sub2); + result = sub_acl_check(sub1, sub2); + if(result == expected){ + printf(GRN "passed\n" RST); + }else{ + printf(RED "failed\n" RST); + } +} + + +int main(int argc, char *argv[]) +{ + hier_test("foo/+/bar", 3); + hier_test("foo/#", 2); + hier_test("foo/+/ba℞/#", 4); + hier_test("foo/baz/ba℞", 3); + hier_test("foo/+/ba℞/#", 4); + hier_test("foo/baz/ba℞/+", 4); + hier_test("foo/+/ba℞/#", 4); + hier_test("foo/baz/ba℞/#", 4); + hier_test("foo/+/ba℞/#", 4); + hier_test("foo/baz/+/#", 4); + hier_test("/+//#", 4); + hier_test("/foo///#", 5); + hier_test("#", 1); + hier_test("+", 1); + hier_test("/", 2); + hier_test("////////////////////////////////////////////////////////////////////////////////////////////////////", 101); + + test("foo/+/bar", "foo/#", false); + test("foo/+/ba℞/#", "foo/baz/ba℞", true); + test("foo/+/ba℞/#", "foo/baz/ba℞/+", true); + test("foo/+/ba℞/#", "foo/baz/ba℞/#", true); + test("foo/+/ba℞/#", "foo/baz/+/#", false); + test("/+//#", "/foo///#", true); + test("#", "#", true); + test("#", "+", true); + test("/#", "+", false); + test("/#", "/+", true); + test("/+", "#", false); + test("/+", "+", false); + test("+/+", "topic/topic", true); + test("+/+", "topic/topic/", false); + test("+", "#", false); + test("+", "+", true); + test("a/b/c/d/e", "a/b/c/d/e", true); + test("a/b/ /d/e", "a/b/c/d/e", false); + + return 0; +} +#endif diff --git a/src/linker-macosx.syms b/src/linker-macosx.syms index 2ff38ba5..5e82a512 100644 --- a/src/linker-macosx.syms +++ b/src/linker-macosx.syms @@ -30,3 +30,4 @@ _mosquitto_property_free_all _mosquitto_realloc _mosquitto_set_username _mosquitto_strdup +_mosquitto_topic_matches_sub diff --git a/src/linker.syms b/src/linker.syms index bd8a3594..9e894b28 100644 --- a/src/linker.syms +++ b/src/linker.syms @@ -31,4 +31,5 @@ mosquitto_realloc; mosquitto_set_username; mosquitto_strdup; + mosquitto_topic_matches_sub; }; diff --git a/src/mosquitto_broker_internal.h b/src/mosquitto_broker_internal.h index 1e9588f6..5f3cd20a 100644 --- a/src/mosquitto_broker_internal.h +++ b/src/mosquitto_broker_internal.h @@ -447,10 +447,12 @@ struct mosquitto_client_msg{ bool dup; }; + struct mosquitto__unpwd{ UT_hash_handle hh; char *username; char *password; + char *clientid; #ifdef WITH_TLS unsigned char *salt; unsigned int password_len; @@ -846,6 +848,8 @@ int mosquitto_psk_key_get_default(struct mosquitto_db *db, struct mosquitto *con int mosquitto_security_auth_start(struct mosquitto_db *db, struct mosquitto *context, bool reauth, const void *data_in, uint16_t data_in_len, void **data_out, uint16_t *data_out_len); int mosquitto_security_auth_continue(struct mosquitto_db *db, struct mosquitto *context, const void *data_in, uint16_t data_len, void **data_out, uint16_t *data_out_len); +void unpwd__free_item(struct mosquitto__unpwd **unpwd, struct mosquitto__unpwd *item); + /* ============================================================ * Session expiry * ============================================================ */ diff --git a/src/security_default.c b/src/security_default.c index 3a1eee97..821b5dd9 100644 --- a/src/security_default.c +++ b/src/security_default.c @@ -816,9 +816,7 @@ static int pwfile__parse(const char *file, struct mosquitto__unpwd **root) } -#ifdef WITH_TLS - -static void unpwd__free_item(struct mosquitto__unpwd **unpwd, struct mosquitto__unpwd *item) +void unpwd__free_item(struct mosquitto__unpwd **unpwd, struct mosquitto__unpwd *item) { mosquitto__free(item->username); mosquitto__free(item->password); @@ -828,6 +826,7 @@ static void unpwd__free_item(struct mosquitto__unpwd **unpwd, struct mosquitto__ } +#ifdef WITH_TLS static int unpwd__decode_passwords(struct mosquitto__unpwd **unpwd) { struct mosquitto__unpwd *u, *tmp; diff --git a/test/broker/14-dynsec-client.py b/test/broker/14-dynsec-client.py new file mode 100755 index 00000000..bbedfef4 --- /dev/null +++ b/test/broker/14-dynsec-client.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 + +from mosq_test_helper import * +import json + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write("allow_anonymous true\n") + f.write("plugin ../../plugins/dynamic-security/mosquitto_dynamic_security.so\n") + f.write("plugin_opt_config_file %d/dynamic-security.json\n" % (port)) + +def command_check(sock, command_payload, expected_response): + command_packet = mosq_test.gen_publish(topic="$CONTROL/dynamic-security/v1", qos=0, payload=json.dumps(command_payload)) + sock.send(command_packet) + response = json.loads(mosq_test.read_publish(sock)) + if response != expected_response: + print(expected_response) + print(response) + raise ValueError(response) + + + +port = mosq_test.get_port() +conf_file = os.path.basename(__file__).replace('.py', '.conf') +write_config(conf_file, port) + +add_client_command = { "commands": [{ + "command": "createClient", "username": "user_one", + "password": "password", "clientid": "cid", + "textName": "Name", "textDescription": "Description", + "roleName": "", "correlationData": "2" }] +} +add_client_response = {'responses': [{'command': 'createClient', 'correlationData': '2'}]} +add_client_repeat_response = {'responses':[{"command":"createClient","error":"Client already exists", "correlationData":"2"}]} + +list_clients_command = { "commands": [{ + "command": "listClients", "verbose": False, "correlationData": "10"}] +} +list_clients_response = {'responses': [{"command": "listClients", "data":{"totalCount":1, "clients":["user_one"]},"correlationData":"10"}]} + +list_clients_verbose_command = { "commands": [{ + "command": "listClients", "verbose": True, "correlationData": "20"}] +} +list_clients_verbose_response = {'responses':[{"command": "listClients", "data":{"totalCount":1, "clients":[ + {"username":"user_one", "clientid":"cid", "textName":"Name", "textDescription":"Description", + "groups":[], "roles":[]}]}, "correlationData":"20"}]} + + +get_client_command = { "commands": [{ + "command": "getClient", "username": "user_one"}]} +get_client_response = {'responses':[{'command': 'getClient', 'data': {'client': {'username': 'user_one', 'clientid': 'cid', + 'textName': 'Name', 'textDescription': 'Description', 'groups': [], 'roles': []}}}]} + +set_client_password_command = {"commands": [{ + "command": "setClientPassword", "username": "user_one", "password": "password"}]} +set_client_password_response = {"responses": [{"command":"setClientPassword"}]} + +delete_client_command = { "commands": [{ + "command": "deleteClient", "username": "user_one"}]} +delete_client_response = {'responses':[{'command': 'deleteClient'}]} + + +rc = 1 +keepalive = 10 +connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive) +connack_packet = mosq_test.gen_connack(rc=0) + +mid = 2 +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +suback_packet = mosq_test.gen_suback(mid, 1) + +try: + os.mkdir(str(port)) + with open("%d/dynamic-security.json" % port, 'w') as f: + f.write('{"defaultACLAction": {"publishClientToBroker":"allow", "publishBrokerToClient":"allow", "subscribe":"allow", "unsubscribe":"allow"}}') +except FileExistsError: + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + +broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + +try: + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Add client + command_check(sock, add_client_command, add_client_response) + + # List clients non-verbose + command_check(sock, list_clients_command, list_clients_response) + + # List clients verbose + command_check(sock, list_clients_verbose_command, list_clients_verbose_response) + + # Kill broker and restart, checking whether our changes were saved. + broker.terminate() + broker.wait() + broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Get client + command_check(sock, get_client_command, get_client_response) + + # List clients non-verbose + command_check(sock, list_clients_command, list_clients_response) + + # List clients verbose + command_check(sock, list_clients_verbose_command, list_clients_verbose_response) + + # Add duplicate client + command_check(sock, add_client_command, add_client_repeat_response) + + # Set client password + command_check(sock, set_client_password_command, set_client_password_response) + + # Delete client + command_check(sock, delete_client_command, delete_client_response) + + rc = 0 + + sock.close() +except mosq_test.TestError: + pass +finally: + os.remove(conf_file) + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + os.rmdir(f"{port}") + broker.terminate() + broker.wait() + (stdo, stde) = broker.communicate() + if rc: + print(stde.decode('utf-8')) + + +exit(rc) diff --git a/test/broker/14-dynsec-group.py b/test/broker/14-dynsec-group.py new file mode 100755 index 00000000..a6dfa641 --- /dev/null +++ b/test/broker/14-dynsec-group.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +from mosq_test_helper import * +import json + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write("allow_anonymous true\n") + f.write("plugin ../../plugins/dynamic-security/mosquitto_dynamic_security.so\n") + f.write("plugin_opt_config_file %d/dynamic-security.json\n" % (port)) + +def command_check(sock, command_payload, expected_response, msg=""): + command_packet = mosq_test.gen_publish(topic="$CONTROL/dynamic-security/v1", qos=0, payload=json.dumps(command_payload)) + sock.send(command_packet) + response = json.loads(mosq_test.read_publish(sock)) + if response != expected_response: + print(msg) + print(expected_response) + print(response) + raise ValueError(response) + + +port = mosq_test.get_port() +conf_file = os.path.basename(__file__).replace('.py', '.conf') +write_config(conf_file, port) + +create_client_command = { "commands": [{ + "command": "createClient", "username": "user_one", + "password": "password", "clientid": "cid", + "textName": "Name", "textDescription": "Description", + "roleName": "", "correlationData": "2" }]} +create_client_response = {'responses':[{"command":"createClient","correlationData":"2"}]} + +create_group_command = { "commands": [{ + "command": "createGroup", "groupName": "group_one", + "textName": "Name", "textDescription": "Description", + "correlationData":"3"}]} +create_group_response = {'responses':[{"command":"createGroup","correlationData":"3"}]} +create_group_repeat_response = {'responses':[{"command":"createGroup","error":"Group already exists","correlationData":"3"}]} + +list_groups_command = { "commands": [{ + "command": "listGroups", "verbose": False, "correlationData": "10"}]} +list_groups_response = {'responses':[{"command": "listGroups", "data":{"totalCount":1, "groups":["group_one"]},"correlationData":"10"}]} + +list_groups_verbose_command = { "commands": [{ + "command": "listGroups", "verbose": True, "correlationData": "15"}]} +list_groups_verbose_response = {'responses':[{'command': 'listGroups', 'data': {"totalCount":1, 'groups': + [{'groupName': 'group_one', 'textName': 'Name', 'textDescription': 'Description', 'clients': [ + {"username":"user_one"}], "roles":[]}]}, + 'correlationData': '15'}]} + +list_clients_verbose_command = { "commands": [{ + "command": "listClients", "verbose": True, "correlationData": "20"}]} +list_clients_verbose_response = {'responses':[{"command": "listClients", "data":{"totalCount":1, "clients":[ + {"username":"user_one", "clientid":"cid", "textName":"Name", "textDescription":"Description", + "groups":[{"groupName":"group_one"}], "roles":[]}]}, "correlationData":"20"}]} + +get_group_command = { "commands": [{"command": "getGroup", "groupName":"group_one"}]} +get_group_response = {'responses':[{'command': 'getGroup', 'data': {'group': {'groupName': 'group_one', + 'textName':'Name', 'textDescription':'Description', 'clients': [{"username":"user_one"}], 'roles': []}}}]} + +add_client_to_group_command = {"commands": [{"command":"addGroupClient", "username":"user_one", + "groupName": "group_one", "correlationData":"1234"}]} +add_client_to_group_response = {'responses':[{'command': 'addGroupClient', 'correlationData': '1234'}]} + +remove_client_from_group_command = {"commands": [{"command":"removeGroupClient", "username":"user_one", + "groupName": "group_one", "correlationData":"4321"}]} +remove_client_from_group_response = {'responses':[{'command': 'removeGroupClient', 'correlationData': '4321'}]} + +delete_group_command = {"commands": [{"command":"deleteGroup", "groupName":"group_one", "correlationData":"5678"}]} +delete_group_response = {'responses':[{"command":"deleteGroup", "correlationData":"5678"}]} + + +rc = 1 +keepalive = 10 +connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive) +connack_packet = mosq_test.gen_connack(rc=0) + +mid = 2 +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +suback_packet = mosq_test.gen_suback(mid, 1) + +try: + os.mkdir(str(port)) + with open("%d/dynamic-security.json" % port, 'w') as f: + f.write('{"defaultACLAction": {"publishClientToBroker":"allow", "publishBrokerToClient":"allow", "subscribe":"allow", "unsubscribe":"allow"}}') +except FileExistsError: + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + +broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + +try: + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Add client + command_check(sock, create_client_command, create_client_response) + + # Add group + command_check(sock, create_group_command, create_group_response) + + # Add client to group + command_check(sock, add_client_to_group_command, add_client_to_group_response) + + # Get group + command_check(sock, get_group_command, get_group_response) + + # List groups non-verbose + command_check(sock, list_groups_command, list_groups_response) + + # List groups verbose + command_check(sock, list_groups_verbose_command, list_groups_verbose_response, "list groups") + + # List clients verbose + command_check(sock, list_clients_verbose_command, list_clients_verbose_response) + + # Kill broker and restart, checking whether our changes were saved. + broker.terminate() + broker.wait() + broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Add duplicate group + command_check(sock, create_group_command, create_group_repeat_response) + + # Remove client from group + command_check(sock, remove_client_from_group_command, remove_client_from_group_response) + + # Add client back to group + command_check(sock, add_client_to_group_command, add_client_to_group_response) + + # Delete group entirely + command_check(sock, delete_group_command, delete_group_response) + + rc = 0 + + sock.close() +except mosq_test.TestError: + pass +finally: + os.remove(conf_file) + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + os.rmdir(f"{port}") + broker.terminate() + broker.wait() + (stdo, stde) = broker.communicate() + if rc: + print(stde.decode('utf-8')) + + +exit(rc) diff --git a/test/broker/14-dynsec-modify-client.py b/test/broker/14-dynsec-modify-client.py new file mode 100755 index 00000000..e0965b8b --- /dev/null +++ b/test/broker/14-dynsec-modify-client.py @@ -0,0 +1,219 @@ +#!/usr/bin/env python3 + +from mosq_test_helper import * +import json + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write("allow_anonymous true\n") + f.write("plugin ../../plugins/dynamic-security/mosquitto_dynamic_security.so\n") + f.write("plugin_opt_config_file %d/dynamic-security.json\n" % (port)) + +def command_check(sock, command_payload, expected_response, msg=""): + command_packet = mosq_test.gen_publish(topic="$CONTROL/dynamic-security/v1", qos=0, payload=json.dumps(command_payload)) + sock.send(command_packet) + response = json.loads(mosq_test.read_publish(sock)) + if response != expected_response: + print(msg) + print(expected_response) + print(response) + raise ValueError(response) + + + +port = mosq_test.get_port() +conf_file = os.path.basename(__file__).replace('.py', '.conf') +write_config(conf_file, port) + +create_client_command = { "commands": [{ + "command": "createClient", "username": "user_one", + "password": "password", "clientid": "cid", + "textName": "Name", "textDescription": "Description", + "correlationData": "2" }] +} +create_client_response = {'responses': [{'command': 'createClient', 'correlationData': '2'}]} + +create_groups_command = { "commands": [ + { + "command": "createGroup", "groupName": "group_one", + "textName": "Name", "textDescription": "Description", + "correlationData": "12" + }, + { + "command": "createGroup", "groupName": "group_two", + "textName": "Name", "textDescription": "Description", + "correlationData": "13" + } + ] +} +create_groups_response = {'responses': [ + {'command': 'createGroup', 'correlationData': '12'}, + {'command': 'createGroup', 'correlationData': '13'} + ]} + +create_roles_command = { "commands": [ + { + "command": "createRole", "roleName": "role_one", + "textName": "Name", "textDescription": "Description", + "acls":[], "correlationData": "21" + }, + { + "command": "createRole", "roleName": "role_two", + "textName": "Name", "textDescription": "Description", + "acls":[], "correlationData": "22" + }, + { + "command": "createRole", "roleName": "role_three", + "textName": "Name", "textDescription": "Description", + "acls":[], "correlationData": "23" + } + ] +} +create_roles_response = {'responses': [ + {'command': 'createRole', 'correlationData': '21'}, + {'command': 'createRole', 'correlationData': '22'}, + {'command': 'createRole', 'correlationData': '23'} + ]} + +modify_client_command1 = { "commands": [{ + "command": "modifyClient", "username": "user_one", + "textName": "Modified name", "textDescription": "Modified description", + "roles":[ + {'roleName':'role_one', 'priority':2}, + {'roleName':'role_two'}, + {'roleName':'role_three', 'priority':10} + ], + "groups":[ + {'groupName':'group_one', 'priority':3}, + {'groupName':'group_two', 'priority':8} + ], + "correlationData": "3" }] +} +modify_client_response1 = {'responses': [{'command': 'modifyClient', 'correlationData': '3'}]} + +modify_client_command2 = { "commands": [{ + "command": "modifyClient", "username": "user_one", + "textName": "Modified name", "textDescription": "Modified description", + "groups":[], + "correlationData": "4" }] +} +modify_client_response2 = {'responses': [{'command': 'modifyClient', 'correlationData': '4'}]} + + +get_client_command1 = { "commands": [{ + "command": "getClient", "username": "user_one"}]} +get_client_response1 = {'responses':[{'command': 'getClient', 'data': {'client': {'username': 'user_one', 'clientid': 'cid', + 'textName': 'Name', 'textDescription': 'Description', + 'groups': [], + 'roles': [], + }}}]} + +get_client_command2 = { "commands": [{ + "command": "getClient", "username": "user_one"}]} +get_client_response2 = {'responses':[{'command': 'getClient', 'data': {'client': {'username': 'user_one', 'clientid': 'cid', + 'textName': 'Modified name', 'textDescription': 'Modified description', + 'groups': [ + {'groupName':'group_two', 'priority':8}, + {'groupName':'group_one', 'priority':3} + ], + 'roles': [ + {'roleName':'role_three', 'priority':10}, + {'roleName':'role_one', 'priority':2}, + {'roleName':'role_two'} + ]}}}]} + +get_client_command3 = { "commands": [{ + "command": "getClient", "username": "user_one"}]} +get_client_response3 = {'responses':[{'command': 'getClient', 'data': {'client': {'username': 'user_one', 'clientid': 'cid', + 'textName': 'Modified name', 'textDescription': 'Modified description', + 'groups': [], + 'roles': [ + {'roleName':'role_three', 'priority':10}, + {'roleName':'role_one', 'priority':2}, + {'roleName':'role_two'} + ]}}}]} + + + +rc = 1 +keepalive = 10 +connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive) +connack_packet = mosq_test.gen_connack(rc=0) + +mid = 2 +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +suback_packet = mosq_test.gen_suback(mid, 1) + +try: + os.mkdir(str(port)) + with open("%d/dynamic-security.json" % port, 'w') as f: + f.write('{"defaultACLAction": {"publishClientToBroker":"allow", "publishBrokerToClient":"allow", "subscribe":"allow", "unsubscribe":"allow"}}') +except FileExistsError: + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + +broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + +try: + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Create client + command_check(sock, create_client_command, create_client_response) + + # Create groups + command_check(sock, create_groups_command, create_groups_response) + + # Create role + command_check(sock, create_roles_command, create_roles_response) + + # Get client + command_check(sock, get_client_command1, get_client_response1, "get client 1") + + # Modify client - with groups + command_check(sock, modify_client_command1, modify_client_response1) + + # Get client + command_check(sock, get_client_command2, get_client_response2, "get client 2a") + + # Kill broker and restart, checking whether our changes were saved. + broker.terminate() + broker.wait() + broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Get client + command_check(sock, get_client_command2, get_client_response2, "get client 2b") + + # Modify client - without groups + command_check(sock, modify_client_command2, modify_client_response2) + + # Get client + command_check(sock, get_client_command3, get_client_response3, "get client 3") + + rc = 0 + + sock.close() +except mosq_test.TestError: + pass +finally: + os.remove(conf_file) + try: + os.remove(f"{port}/dynamic-security.json") + pass + except FileNotFoundError: + pass + os.rmdir(f"{port}") + broker.terminate() + broker.wait() + (stdo, stde) = broker.communicate() + if rc: + print(stde.decode('utf-8')) + + +exit(rc) diff --git a/test/broker/14-dynsec-modify-group.py b/test/broker/14-dynsec-modify-group.py new file mode 100755 index 00000000..baed4fd0 --- /dev/null +++ b/test/broker/14-dynsec-modify-group.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 + +from mosq_test_helper import * +import json + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write("allow_anonymous true\n") + f.write("plugin ../../plugins/dynamic-security/mosquitto_dynamic_security.so\n") + f.write("plugin_opt_config_file %d/dynamic-security.json\n" % (port)) + +def command_check(sock, command_payload, expected_response, msg=""): + command_packet = mosq_test.gen_publish(topic="$CONTROL/dynamic-security/v1", qos=0, payload=json.dumps(command_payload)) + sock.send(command_packet) + response = json.loads(mosq_test.read_publish(sock)) + if response != expected_response: + print(msg) + print(expected_response) + print(response) + raise ValueError(response) + + + +port = mosq_test.get_port() +conf_file = os.path.basename(__file__).replace('.py', '.conf') +write_config(conf_file, port) + +create_client_command = { "commands": [{ + "command": "createClient", "username": "user_one", + "password": "password", "clientid": "cid", + "textName": "Name", "textDescription": "Description", + "correlationData": "2" }] +} +create_client_response = {'responses': [{'command': 'createClient', 'correlationData': '2'}]} + +create_group_command = { "commands": [{ + "command": "createGroup", "groupName": "group_one", + "textName": "Name", "textDescription": "Description", + "roleName": "", "correlationData": "2" }] +} +create_group_response = {'responses': [{'command': 'createGroup', 'correlationData': '2'}]} + +create_role_command = { "commands": [ + { + "command": "createRole", "roleName": "role_one", + "textName": "Name", "textDescription": "Description", + "acls":[], "correlationData": "2" + }, + { + "command": "createRole", "roleName": "role_two", + "textName": "Name", "textDescription": "Description", + "acls":[], "correlationData": "3" + } + ] +} +create_role_response = {'responses': [ + {'command': 'createRole', 'correlationData': '2'}, + {'command': 'createRole', 'correlationData': '3'} + ]} + +modify_group_command1 = { "commands": [{ + "command": "modifyGroup", "groupName": "group_one", + "textName": "Modified name", "textDescription": "Modified description", + "roles":[{'roleName':'role_one'}], + "clients":[{'username':'user_one'}], + "correlationData": "3" }] +} +modify_group_response1 = {'responses': [{'command': 'modifyGroup', 'correlationData': '3'}]} + +modify_group_command2 = { "commands": [{ + "command": "modifyGroup", "groupName": "group_one", + "textName": "Modified name", "textDescription": "Modified description", + "roles":[ + {'roleName':'role_one', 'priority':99}, + {'roleName':'role_two', 'priority':87} + ], + "clients":[], + "correlationData": "3" }] +} +modify_group_response2 = {'responses': [{'command': 'modifyGroup', 'correlationData': '3'}]} + +modify_group_command3 = { "commands": [{ + "command": "modifyGroup", "groupName": "group_one", + "textName": "Modified name", "textDescription": "Modified description", + "roles":[], + "clients":[], + "correlationData": "3" }] +} +modify_group_response3 = {'responses': [{'command': 'modifyGroup', 'correlationData': '3'}]} + + +get_group_command1 = { "commands": [{ + "command": "getGroup", "groupName": "group_one"}]} +get_group_response1 = {'responses':[{'command': 'getGroup', 'data': {'group': {'groupName': 'group_one', + 'textName': 'Name', 'textDescription': 'Description', + 'clients':[], + 'roles': []}}}]} + +get_group_command2 = { "commands": [{ + "command": "getGroup", "groupName": "group_one"}]} +get_group_response2 = {'responses':[{'command': 'getGroup', 'data': {'group': {'groupName': 'group_one', + 'textName': 'Modified name', 'textDescription': 'Modified description', + 'clients':[{'username':'user_one'}], + 'roles': [{'roleName':'role_one'}]}}}]} + +get_group_command3 = { "commands": [{ + "command": "getGroup", "groupName": "group_one"}]} +get_group_response3 = {'responses':[{'command': 'getGroup', 'data': {'group': {'groupName': 'group_one', + 'textName': 'Modified name', 'textDescription': 'Modified description', + 'clients':[], + 'roles': [ + {'roleName':'role_one', 'priority':99}, + {'roleName':'role_two', 'priority':87} + ]}}}]} + +get_group_command4 = { "commands": [{ + "command": "getGroup", "groupName": "group_one"}]} +get_group_response4 = {'responses':[{'command': 'getGroup', 'data': {'group': {'groupName': 'group_one', + 'textName': 'Modified name', 'textDescription': 'Modified description', + 'clients':[], + 'roles': []}}}]} + + + +rc = 1 +keepalive = 10 +connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive) +connack_packet = mosq_test.gen_connack(rc=0) + +mid = 2 +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +suback_packet = mosq_test.gen_suback(mid, 1) + +try: + os.mkdir(str(port)) + with open("%d/dynamic-security.json" % port, 'w') as f: + f.write('{"defaultACLAction": {"publishClientToBroker":"allow", "publishBrokerToClient":"allow", "subscribe":"allow", "unsubscribe":"allow"}}') +except FileExistsError: + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + +broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + +try: + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Create client + command_check(sock, create_client_command, create_client_response) + + # Create group + command_check(sock, create_group_command, create_group_response) + + # Create role + command_check(sock, create_role_command, create_role_response) + + # Get group + command_check(sock, get_group_command1, get_group_response1, "get group 1") + + # Modify group + command_check(sock, modify_group_command1, modify_group_response1) + + # Get group + command_check(sock, get_group_command2, get_group_response2, "get group 2a") + + # Kill broker and restart, checking whether our changes were saved. + broker.terminate() + broker.wait() + broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Get group + command_check(sock, get_group_command2, get_group_response2, "get group 2b") + + # Modify group + command_check(sock, modify_group_command2, modify_group_response2) + + # Get group + command_check(sock, get_group_command3, get_group_response3, "get group 3") + + # Modify group + command_check(sock, modify_group_command3, modify_group_response3) + + # Get group + command_check(sock, get_group_command4, get_group_response4, "get group 4") + + rc = 0 + + sock.close() +except mosq_test.TestError: + pass +finally: + os.remove(conf_file) + try: + os.remove(f"{port}/dynamic-security.json") + pass + except FileNotFoundError: + pass + os.rmdir(f"{port}") + broker.terminate() + broker.wait() + (stdo, stde) = broker.communicate() + if rc: + print(stde.decode('utf-8')) + + +exit(rc) diff --git a/test/broker/14-dynsec-modify-role.py b/test/broker/14-dynsec-modify-role.py new file mode 100755 index 00000000..25d5494c --- /dev/null +++ b/test/broker/14-dynsec-modify-role.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 + +from mosq_test_helper import * +import json + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write("allow_anonymous true\n") + f.write("plugin ../../plugins/dynamic-security/mosquitto_dynamic_security.so\n") + f.write("plugin_opt_config_file %d/dynamic-security.json\n" % (port)) + +def command_check(sock, command_payload, expected_response): + command_packet = mosq_test.gen_publish(topic="$CONTROL/dynamic-security/v1", qos=0, payload=json.dumps(command_payload)) + sock.send(command_packet) + response = json.loads(mosq_test.read_publish(sock)) + if response != expected_response: + print(expected_response) + print(response) + raise ValueError(response) + + + +port = mosq_test.get_port() +conf_file = os.path.basename(__file__).replace('.py', '.conf') +write_config(conf_file, port) + +create_role_command = { "commands": [{ + "command": "createRole", "roleName": "role_one", + "textName": "Name", "textDescription": "Description", + "acls":[ + { + "aclType": "publishClientToBroker", + "allow": True, + "topic": "topic/#", + "priority": 8 + }, + { + "aclType": "publishClientToBroker", + "allow": True, + "topic": "topic/2/#", + "priority": 9 + } + ], "correlationData": "2" }] +} +create_role_response = {'responses': [{'command': 'createRole', 'correlationData': '2'}]} + +modify_role_command = { "commands": [{ + "command": "modifyRole", "roleName": "role_one", + "textName": "Modified name", "textDescription": "Modified description", + "correlationData": "3" }] +} +modify_role_response = {'responses': [{'command': 'modifyRole', 'correlationData': '3'}]} + + +get_role_command1 = { "commands": [{"command": "getRole", "roleName": "role_one"}]} +get_role_response1 = {'responses':[{'command': 'getRole', 'data': {'role': {'roleName': 'role_one', + 'textName': 'Name', 'textDescription': 'Description', + 'acls': [ + { + "aclType": "publishClientToBroker", + "topic": "topic/2/#", + "allow": True, + "priority": 9 + }, + { + "aclType": "publishClientToBroker", + "topic": "topic/#", + "allow": True, + "priority": 8 + } + ]}}}]} + +get_role_command2 = { "commands": [{ + "command": "getRole", "roleName": "role_one"}]} +get_role_response2 = {'responses':[{'command': 'getRole', 'data': {'role': {'roleName': 'role_one', + 'textName': 'Modified name', 'textDescription': 'Modified description', + 'acls': [ + { + "aclType": "publishClientToBroker", + "topic": "topic/2/#", + "allow": True, + "priority": 9 + }, + { + "aclType": "publishClientToBroker", + "topic": "topic/#", + "allow": True, + "priority": 8 + } + ]}}}]} + +rc = 1 +keepalive = 10 +connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive) +connack_packet = mosq_test.gen_connack(rc=0) + +mid = 2 +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +suback_packet = mosq_test.gen_suback(mid, 1) + +try: + os.mkdir(str(port)) + with open("%d/dynamic-security.json" % port, 'w') as f: + f.write('{"defaultACLAction": {"publishClientToBroker":"allow", "publishBrokerToClient":"allow", "subscribe":"allow", "unsubscribe":"allow"}}') +except FileExistsError: + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + +broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + +try: + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Add role + command_check(sock, create_role_command, create_role_response) + + # Get role + command_check(sock, get_role_command1, get_role_response1) + + # Modify role + command_check(sock, modify_role_command, modify_role_response) + + # Get role + command_check(sock, get_role_command2, get_role_response2) + + # Kill broker and restart, checking whether our changes were saved. + broker.terminate() + broker.wait() + broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Get role + command_check(sock, get_role_command2, get_role_response2) + + rc = 0 + + sock.close() +except mosq_test.TestError: + pass +finally: + os.remove(conf_file) + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + os.rmdir(f"{port}") + broker.terminate() + broker.wait() + (stdo, stde) = broker.communicate() + if rc: + print(stde.decode('utf-8')) + + +exit(rc) diff --git a/test/broker/14-dynsec-role.py b/test/broker/14-dynsec-role.py new file mode 100755 index 00000000..bdeb696d --- /dev/null +++ b/test/broker/14-dynsec-role.py @@ -0,0 +1,220 @@ +#!/usr/bin/env python3 + +from mosq_test_helper import * +import json + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write("allow_anonymous true\n") + f.write("plugin ../../plugins/dynamic-security/mosquitto_dynamic_security.so\n") + f.write("plugin_opt_config_file %d/dynamic-security.json\n" % (port)) + +def command_check(sock, command_payload, expected_response, msg=""): + command_packet = mosq_test.gen_publish(topic="$CONTROL/dynamic-security/v1", qos=0, payload=json.dumps(command_payload)) + sock.send(command_packet) + response = json.loads(mosq_test.read_publish(sock)) + if response != expected_response: + print(msg) + print(expected_response) + print(response) + raise ValueError(response) + + + +port = mosq_test.get_port() +conf_file = os.path.basename(__file__).replace('.py', '.conf') +write_config(conf_file, port) + +create_client_command = { "commands": [{ + "command": "createClient", "username": "user_one", + "password": "password", "clientid": "cid", + "textName": "Name", "textDescription": "Description", + "roleName": "", "correlationData": "2" }] +} +create_client_response = {'responses': [{'command': 'createClient', 'correlationData': '2'}]} + +create_group_command = { "commands": [{ + "command": "createGroup", "groupName": "group_one", + "textName": "Name", "textDescription": "Description", + "correlationData":"3"}]} +create_group_response = {'responses':[{"command":"createGroup","correlationData":"3"}]} + +create_role_command = { "commands": [{'command': 'createRole', 'correlationData': '3', + "roleName": "basic", "acls":[ + {"aclType":"publishClientToBroker", "topic": "out/#", "priority":3, "allow": True}], "textName":"name", "textDescription":"desc" + }]} +create_role_response = {'responses': [{'command': 'createRole', 'correlationData': '3'}]} + + +add_role_to_client_command = {"commands": [{'command': 'addClientRole', "username": "user_one", + "roleName": "basic"}]} +add_role_to_client_response = {'responses': [{'command': 'addClientRole'}]} + +add_role_to_group_command = {"commands": [{'command': 'addGroupRole', "groupName": "group_one", + "roleName": "basic"}]} +add_role_to_group_response = {'responses': [{'command': 'addGroupRole'}]} + + +list_roles_verbose_command1 = { "commands": [{ + "command": "listRoles", "verbose": True, "correlationData": "21"}] +} +list_roles_verbose_response1 = {'responses': [{'command': 'listRoles', 'data': + {'totalCount':1, 'roles': [{'roleName': 'basic', "textName": "name", "textDescription": "desc", + 'acls': [{'aclType':'publishClientToBroker', 'topic': 'out/#', 'priority': 3, 'allow': True}] + }]}, 'correlationData': '21'}]} + +add_acl_command = {"commands": [{'command': "addRoleACL", "roleName":"basic", "aclType":"subscribeLiteral", + "topic":"basic/out", "priority":1, "allow":True}]} +add_acl_response = {'responses': [{'command': 'addRoleACL'}]} + +list_roles_verbose_command2 = { "commands": [{ + "command": "listRoles", "verbose": True, "correlationData": "22"}] +} +list_roles_verbose_response2 = {'responses': [{'command': 'listRoles', 'data': {'totalCount':1, 'roles': + [{'roleName': 'basic', 'textName': 'name', 'textDescription': 'desc', 'acls': + [{'aclType':'publishClientToBroker', 'topic': 'out/#', 'priority': 3, 'allow': True}, + {'aclType':'subscribeLiteral', 'topic': 'basic/out', 'priority': 1, 'allow': True}], + }]}, 'correlationData': '22'}]} + +get_role_command = {"commands": [{'command': "getRole", "roleName":"basic"}]} +get_role_response = {'responses': [{'command': 'getRole', 'data': {'role': + {'roleName': 'basic', 'textName': 'name', 'textDescription': 'desc', 'acls': + [{'aclType':'publishClientToBroker', 'topic': 'out/#', 'priority': 3, 'allow': True}, + {'aclType':'subscribeLiteral', 'topic': 'basic/out', 'priority': 1, 'allow': True}], + }}}]} + +remove_acl_command = {"commands": [{'command': "removeRoleACL", "roleName":"basic", "aclType":"subscribeLiteral", + "topic":"basic/out"}]} +remove_acl_response = {'responses': [{'command': 'removeRoleACL'}]} + +delete_role_command = {"commands": [{'command': "deleteRole", "roleName":"basic"}]} +delete_role_response = {"responses": [{"command": "deleteRole"}]} + +list_clients_verbose_command = { "commands": [{ + "command": "listClients", "verbose": True, "correlationData": "20"}] +} +list_clients_verbose_response = {'responses':[{"command": "listClients", "data":{'totalCount':1, "clients":[ + {"username":"user_one", "clientid":"cid", "textName":"Name", "textDescription":"Description", + "groups":[], "roles":[{'roleName':'basic'}]}]}, "correlationData":"20"}]} + +list_groups_verbose_command = { "commands": [{ + "command": "listGroups", "verbose": True, "correlationData": "20"}] +} +list_groups_verbose_response = {'responses':[{"command": "listGroups", "data":{'totalCount':1, "groups":[ + {"groupName":"group_one", "textName":"Name", "textDescription":"Description", + "clients":[], "roles":[{'roleName':'basic'}]}]}, "correlationData":"20"}]} + +remove_role_from_client_command = {"commands": [{'command': 'removeClientRole', "username": "user_one", + "roleName": "basic"}]} +remove_role_from_client_response = {'responses': [{'command': 'removeClientRole'}]} + +remove_role_from_group_command = {"commands": [{'command': 'removeGroupRole', "groupName": "group_one", + "roleName": "basic"}]} +remove_role_from_group_response = {'responses': [{'command': 'removeGroupRole'}]} + + +rc = 1 +keepalive = 10 +connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive) +connack_packet = mosq_test.gen_connack(rc=0) + +mid = 2 +subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/#", 1) +suback_packet = mosq_test.gen_suback(mid, 1) + +try: + os.mkdir(str(port)) + with open("%d/dynamic-security.json" % port, 'w') as f: + f.write('{"defaultACLAction": {"publishClientToBroker":"allow", "publishBrokerToClient":"allow", "subscribe":"allow", "unsubscribe":"allow"}}') +except FileExistsError: + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + +broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + +try: + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # Create client + command_check(sock, create_client_command, create_client_response) + + # Create group + command_check(sock, create_group_command, create_group_response) + + # Create role + command_check(sock, create_role_command, create_role_response) + + # Add role to client + command_check(sock, add_role_to_client_command, add_role_to_client_response) + + # Add role to group + command_check(sock, add_role_to_group_command, add_role_to_group_response) + + # List clients verbose + command_check(sock, list_clients_verbose_command, list_clients_verbose_response) + + # List groups verbose + command_check(sock, list_groups_verbose_command, list_groups_verbose_response) + + # List roles verbose 1 + command_check(sock, list_roles_verbose_command1, list_roles_verbose_response1, "list roles verbose 1a") + + # Add ACL + command_check(sock, add_acl_command, add_acl_response) + + # List roles verbose 2 + command_check(sock, list_roles_verbose_command2, list_roles_verbose_response2, "list roles verbose 2a") + + # Kill broker and restart, checking whether our changes were saved. + broker.terminate() + broker.wait() + broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) + + sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) + mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") + + # List roles verbose 2 + command_check(sock, list_roles_verbose_command2, list_roles_verbose_response2, "list roles verbose 2b") + + # Get role + command_check(sock, get_role_command, get_role_response) + + # Remove ACL + command_check(sock, remove_acl_command, remove_acl_response) + + # List roles verbose 1 + command_check(sock, list_roles_verbose_command1, list_roles_verbose_response1, "list roles verbose 1b") + + # Remove role from client + command_check(sock, remove_role_from_client_command, remove_role_from_client_response) + + # Remove role from group + command_check(sock, remove_role_from_group_command, remove_role_from_group_response) + + # Delete role + command_check(sock, delete_role_command, delete_role_response) + + rc = 0 + + sock.close() +except mosq_test.TestError: + pass +finally: + os.remove(conf_file) + try: + os.remove(f"{port}/dynamic-security.json") + except FileNotFoundError: + pass + os.rmdir(f"{port}") + broker.terminate() + broker.wait() + (stdo, stde) = broker.communicate() + if rc: + print(stde.decode('utf-8')) + + +exit(rc) diff --git a/test/broker/14-plugin-register-control.py b/test/broker/14-plugin-register-control.py deleted file mode 100755 index 4d29ca53..00000000 --- a/test/broker/14-plugin-register-control.py +++ /dev/null @@ -1,55 +0,0 @@ -#!/usr/bin/env python3 - -from mosq_test_helper import * - -def write_config(filename, port): - with open(filename, 'w') as f: - f.write("port %d\n" % (port)) - f.write("auth_plugin c/plugin_control.so\n") - f.write("allow_anonymous true\n") - -port = mosq_test.get_port() -conf_file = os.path.basename(__file__).replace('.py', '.conf') -write_config(conf_file, port) - -rc = 1 -keepalive = 10 -connect_packet = mosq_test.gen_connect("ctrl-test", keepalive=keepalive) -connack_packet = mosq_test.gen_connack(rc=0) - -mid = 2 -subscribe_packet = mosq_test.gen_subscribe(mid, "$CONTROL/user-management/v1", 1) -suback_packet = mosq_test.gen_suback(mid, 1) - -mid = 3 -publish_packet = mosq_test.gen_publish(topic="$CONTROL/user-management/v1", qos=1, payload="payload contents", retain=1, mid=mid) -puback_packet = mosq_test.gen_puback(mid) - -mid = 1 -publish_packet_recv = mosq_test.gen_publish(topic="$CONTROL/user-management/v1", qos=0, payload="payload contents", retain=0) - - -broker = mosq_test.start_broker(filename=os.path.basename(__file__), use_conf=True, port=port) - -try: - sock = mosq_test.do_client_connect(connect_packet, connack_packet, timeout=5, port=port) - mosq_test.do_send_receive(sock, subscribe_packet, suback_packet, "suback") - sock.send(publish_packet) - mosq_test.receive_unordered(sock, puback_packet, publish_packet_recv, "puback/publish_receive") - - rc = 0 - - sock.close() -except mosq_test.TestError: - pass -finally: - os.remove(conf_file) - broker.terminate() - broker.wait() - (stdo, stde) = broker.communicate() - if rc: - print(stde.decode('utf-8')) - - -exit(rc) - diff --git a/test/broker/Makefile b/test/broker/Makefile index da905ed0..4baa492f 100644 --- a/test/broker/Makefile +++ b/test/broker/Makefile @@ -220,4 +220,9 @@ endif ./13-malformed-unsubscribe-v5.py 14 : - #./14-plugin-register-control.py + #./14-dynsec-client.py + #./14-dynsec-group.py + #./14-dynsec-role.py + #./14-dynsec-modify-client.py + #./14-dynsec-modify-group.py + #./14-dynsec-modify-role.py diff --git a/test/broker/test.py b/test/broker/test.py index 95e37ad8..d23c2d61 100755 --- a/test/broker/test.py +++ b/test/broker/test.py @@ -188,7 +188,12 @@ tests = [ (1, './13-malformed-subscribe-v5.py'), (1, './13-malformed-unsubscribe-v5.py'), - #(1, './14-plugin-register-control.py'), + #(1, './14-dynsec-client.py'), + #(1, './14-dynsec-group.py'), + #(1, './14-dynsec-role.py'), + #(1, './14-dynsec-modify-client.py'), + #(1, './14-dynsec-modify-group.py'), + #(1, './14-dynsec-modify-role.py'), ] ptest.run_tests(tests) diff --git a/test/mosq_test.py b/test/mosq_test.py index 1805c065..f967c7ac 100644 --- a/test/mosq_test.py +++ b/test/mosq_test.py @@ -364,6 +364,51 @@ def to_string(packet): (cmd, rl) = struct.unpack('!BB', packet) return "AUTH, rl="+str(rl) + +def read_varint(sock, rl): + varint = 0 + multiplier = 1 + while True: + byte = sock.recv(1) + byte, = struct.unpack("!B", byte) + varint += (byte & 127)*multiplier + multiplier *= 128 + rl -= 1 + if byte & 128 == 0x00: + return (varint, rl) + + +def mqtt_read_string(sock, rl): + slen = sock.recv(2) + slen, = struct.unpack("!H", slen) + payload = sock.recv(slen) + payload, = struct.unpack("!%ds" % (slen), payload) + rl -= (2 + slen) + return (payload, rl) + + +def read_publish(sock, proto_ver=4): + cmd, = struct.unpack("!B", sock.recv(1)) + if cmd & 0xF0 != 0x30: + raise ValueError + + qos = (cmd & 0x06) >> 1 + rl, t = read_varint(sock, 0) + topic, rl = mqtt_read_string(sock, rl) + + if qos > 0: + sock.recv(2) + rl -= 1 + + if proto_ver == 5: + proplen, rl = read_varint(sock, rl) + sock.recv(proplen) + rl -= proplen + + payload = sock.recv(rl).decode('utf-8') + return payload + + def gen_connect(client_id, clean_session=True, keepalive=60, username=None, password=None, will_topic=None, will_qos=0, will_retain=False, will_payload=b"", proto_ver=4, connect_reserved=False, properties=b"", will_properties=b"", session_expiry=-1): if (proto_ver&0x7F) == 3 or proto_ver == 0: remaining_length = 12