1
0
mirror of https://github.com/eclipse/mosquitto.git synced 2025-05-09 01:01:11 +08:00

New dynamic security plugin.

This commit is contained in:
Roger A. Light 2020-09-23 22:59:31 +01:00
parent e82ee879d5
commit fdff255916
53 changed files with 9165 additions and 80 deletions

2
.gitignore vendored
View File

@ -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

View File

@ -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
# ========================================

View File

@ -1 +1,2 @@
add_subdirectory(mosquitto_ctrl)
add_subdirectory(mosquitto_passwd)

View File

@ -1,5 +1,6 @@
DIRS= \
db_dump \
mosquitto_ctrl \
mosquitto_passwd
.PHONY : all binary check clean reallyclean test install uninstall

View File

@ -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)

View File

@ -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

View File

@ -0,0 +1,147 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <mosquitto.h>
#include <mqtt_protocol.h>
#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;
}

View File

@ -0,0 +1,505 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <new-config-file> <admin-username> <admin-password> [admin-role]\n");
printf("\nGeneral\n-------\n");
printf("Set ACL default access: setDefaultACLAccess <acltype> allow|deny\n");
printf("Set group for anonymous clients: setAnonymousGroup <groupname>\n");
printf("\nClients\n-------\n");
printf("Create a new client: createClient <username> <password>\n");
printf("Delete a client: deleteClient <username>\n");
printf("Set a client password: setClientPassword <username> <password>\n");
printf("Add a role to a client: addClientRole <username> <rolename> [priority]\n");
printf(" Higher priority (larger numerical value) roles are evaluated first.\n");
printf("Remove role from a client: removeClientRole <username> <rolename>\n");
printf("Get client information: getClient <username>\n");
printf("List all clients: listClients [count [offset]]\n");
printf("\nGroups\n------\n");
printf("Create a new group: createGroup <groupname>\n");
printf("Delete a group: deleteGroup <groupname>\n");
printf("Add a role to a group: addGroupRole <groupname> <rolename> [priority]\n");
printf(" Higher priority (larger numerical value) roles are evaluated first.\n");
printf("Remove role from a group: removeGroupRole <groupname> <rolename>\n");
printf("Add client to a group: addGroupClient <groupname> <username> [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 <groupname> <username>\n");
printf("Get group information: getGroup <groupname>\n");
printf("List all groups: listGroups [count [offset]]\n");
printf("\nRoles\n------\n");
printf("Create a new role: createRole <rolename>\n");
printf("Delete a role: deleteRole <rolename>\n");
printf("Add an ACL to a role: addRoleACL <rolename> <aclspec> [priority]\n");
printf(" Higher priority (larger numerical value) ACLs are evaluated first.\n");
printf("Remove ACL from a role: removeRoleACL <rolename> <aclspec>\n");
printf("Get role information: getRole <rolename>\n");
printf("List all roles: listRoles [count [offset]]\n");
printf("\naclspec: <acltype> <topicFilter> 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 <filename> <admin-user> <admin-password> [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;
}

View File

@ -0,0 +1,163 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
}

View File

@ -0,0 +1,185 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
}

View File

@ -0,0 +1,191 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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;
}
}

View File

@ -0,0 +1,90 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <errno.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 <module> <module-command> <command-options>\n");
printf("For module specific help use: mosquitto_ctrl <module> 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;
}

View File

@ -0,0 +1,109 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <stdbool.h>
#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

View File

@ -0,0 +1,898 @@
/*
Copyright (c) 2014-2020 Roger Light <roger@atchoo.org>
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 <errno.h>
#include <fcntl.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifndef WIN32
#include <unistd.h>
#include <strings.h>
#else
#include <process.h>
#include <winsock2.h>
#define snprintf sprintf_s
#define strncasecmp _strnicmp
#endif
#include <mosquitto.h>
#include <mqtt_protocol.h>
#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; i++){
if(str[i] == '%'){
if(i+2 >= len){
return 1;
}
if(str[i+1] == '2' && str[i+2] == '5'){
str[i] = '%';
len -= 2;
for(j=i+1; j<len; j++){
str[j] = str[j+2];
}
str[j] = '\0';
}else if(str[i+1] == '3' && (str[i+2] == 'A' || str[i+2] == 'a')){
str[i] = ':';
len -= 2;
for(j=i+1; j<len; j++){
str[j] = str[j+2];
}
str[j] = '\0';
}else if(str[i+1] == '4' && str[i+2] == '0'){
str[i] = ':';
len -= 2;
for(j=i+1; j<len; j++){
str[j] = str[j+2];
}
str[j] = '\0';
}else{
return 1;
}
}
}
return 0;
}
static int mosquitto__parse_socks_url(struct mosq_config *cfg, char *url)
{
char *str;
size_t i;
char *username = NULL, *password = NULL, *host = NULL, *port = NULL;
char *username_or_host = NULL;
size_t start;
size_t len;
bool have_auth = false;
int port_int;
if(!strncmp(url, "socks5h://", strlen("socks5h://"))){
str = url + strlen("socks5h://");
}else{
fprintf(stderr, "Error: Unsupported proxy protocol: %s\n", url);
return 1;
}
// socks5h://username:password@host:1883
// socks5h://username:password@host
// socks5h://username@host:1883
// socks5h://username@host
// socks5h://host:1883
// socks5h://host
start = 0;
for(i=0; i<strlen(str); i++){
if(str[i] == ':'){
if(i == start){
goto cleanup;
}
if(have_auth){
/* Have already seen a @ , so this must be of form
* socks5h://username[:password]@host:port */
if(host){
/* Already seen a host, must be malformed. */
goto cleanup;
}
len = i-start;
host = malloc(len + 1);
if(!host){
fprintf(stderr, "Error: Out of memory.\n");
goto cleanup;
}
memcpy(host, &(str[start]), len);
host[len] = '\0';
start = i+1;
}else if(!username_or_host){
/* Haven't seen a @ before, so must be of form
* socks5h://host:port or
* socks5h://username:password@host[:port] */
len = i-start;
username_or_host = malloc(len + 1);
if(!username_or_host){
fprintf(stderr, "Error: Out of memory.\n");
goto cleanup;
}
memcpy(username_or_host, &(str[start]), len);
username_or_host[len] = '\0';
start = i+1;
}
}else if(str[i] == '@'){
if(i == start){
goto cleanup;
}
have_auth = true;
if(username_or_host){
/* Must be of form socks5h://username:password@... */
username = username_or_host;
username_or_host = NULL;
len = i-start;
password = malloc(len + 1);
if(!password){
fprintf(stderr, "Error: Out of memory.\n");
goto cleanup;
}
memcpy(password, &(str[start]), len);
password[len] = '\0';
start = i+1;
}else{
/* Haven't seen a : yet, so must be of form
* socks5h://username@... */
if(username){
/* Already got a username, must be malformed. */
goto cleanup;
}
len = i-start;
username = malloc(len + 1);
if(!username){
fprintf(stderr, "Error: Out of memory.\n");
goto cleanup;
}
memcpy(username, &(str[start]), len);
username[len] = '\0';
start = i+1;
}
}
}
/* Deal with remainder */
if(i > 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

View File

@ -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})

View File

@ -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

View File

@ -12,6 +12,7 @@ FIND_PATH(
HINTS
CJSON_DIR
/usr/include/cjson
/usr/local/include/cjson
)
FIND_LIBRARY( CJSON_LIBRARY

View File

@ -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)

View File

@ -3,7 +3,8 @@ FROM alpine:3.12
LABEL maintainer="Roger Light <roger@atchoo.org>" \
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 \

View File

@ -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

View File

@ -511,6 +511,12 @@
<manvolnum>5</manvolnum>
</citerefentry>
</member>
<member>
<citerefentry>
<refentrytitle><link xlink:href="mosquitto_ctrl-1.html">mosquitto_ctrl</link></refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>
</member>
<member>
<citerefentry>
<refentrytitle><link xlink:href="mosquitto_passwd-1.html">mosquitto_passwd</link></refentrytitle>

View File

@ -0,0 +1,5 @@
.. title: mosquitto_ctrl man page
.. slug: mosquitto_ctrl-1
.. category: man
.. type: man
.. pretty_url: False

638
man/mosquitto_ctrl.1.xml Normal file
View File

@ -0,0 +1,638 @@
<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet type="text/xsl" href="manpage.xsl"?>
<refentry xml:id="mosquitto_ctrl" xmlns:xlink="http://www.w3.org/1999/xlink">
<refmeta>
<refentrytitle>mosquitto_ctrl</refentrytitle>
<manvolnum>1</manvolnum>
<refmiscinfo class="source">Mosquitto Project</refmiscinfo>
<refmiscinfo class="manual">Commands</refmiscinfo>
</refmeta>
<refnamediv>
<refname>mosquitto_ctrl</refname>
<refpurpose>a tool for initialising/configuring a Mosquitto broker instance</refpurpose>
</refnamediv>
<refsynopsisdiv>
<cmdsynopsis>
<command>mosquitto_ctrl</command>
<arg choice='opt'>connection-options</arg>
<arg choice='plain'>module</arg>
<arg choice='plain'>module-command</arg>
<arg choice='opt'>command-options</arg>
</cmdsynopsis>
<cmdsynopsis>
<command>mosquitto_ctrl</command>
<group choice='req'>
<arg choice='plain'>
<arg><option>-h</option> <replaceable>hostname</replaceable></arg>
<arg><option>--unix</option> <replaceable>socket path</replaceable></arg>
<arg><option>-p</option> <replaceable>port-number</replaceable></arg>
<arg><option>-u</option> <replaceable>username</replaceable></arg>
<arg><option>-P</option> <replaceable>password</replaceable></arg>
</arg>
<arg choice='plain'><option>-L</option> <replaceable>URL</replaceable></arg>
</group>
<arg><option>-A</option> <replaceable>bind-address</replaceable></arg>
<arg><option>-c</option></arg>
<arg><option>-d</option></arg>
<arg><option>-i</option> <replaceable>client-id</replaceable></arg>
<arg><option>-q</option> <replaceable>message-QoS</replaceable></arg>
<arg><option>--quiet</option></arg>
<arg><option>-V</option> <replaceable>protocol-version</replaceable></arg>
<group>
<arg>
<group choice='req'>
<arg choice='plain'><option>--cafile</option> <replaceable>file</replaceable></arg>
<arg choice='plain'><option>--capath</option> <replaceable>dir</replaceable></arg>
</group>
<arg><option>--cert</option> <replaceable>file</replaceable></arg>
<arg><option>--key</option> <replaceable>file</replaceable></arg>
<arg><option>--ciphers</option> <replaceable>ciphers</replaceable></arg>
<arg><option>--tls-version</option> <replaceable>version</replaceable></arg>
<arg><option>--tls-alpn</option> <replaceable>protocol</replaceable></arg>
<arg><option>--tls-engine</option> <replaceable>engine</replaceable></arg>
<arg><option>--keyform</option>
<group choice='req'>
<arg choice='plain'><replaceable>pem</replaceable></arg>
<arg choice='plain'><replaceable>engine</replaceable></arg>
</group></arg>
<arg><option>--tls-engine-kpass-sha1</option> <replaceable>kpass-sha1</replaceable></arg>
<arg><option>--insecure</option></arg>
</arg>
<arg>
<arg choice='plain'><option>--psk</option> <replaceable>hex-key</replaceable></arg>
<arg choice='plain'><option>--psk-identity</option> <replaceable>identity</replaceable></arg>
<arg><option>--ciphers</option> <replaceable>ciphers</replaceable></arg>
<arg><option>--tls-version</option> <replaceable>version</replaceable></arg>
</arg>
</group>
<arg><option>--proxy</option> <replaceable>socks-url</replaceable></arg>
</cmdsynopsis>
<cmdsynopsis>
<command>mosquitto_ctrl</command>
<group choice='plain'>
<arg><option>--help</option></arg>
</group>
</cmdsynopsis>
</refsynopsisdiv>
<refsect1>
<title>Description</title>
<para><command>mosquitto_ctrl</command> is a simple MQTT version 5/3.1.1
client that will publish a single message on a topic and
exit.</para>
</refsect1>
<refsect1>
<title>Encrypted Connections</title>
<para><command>mosquitto_ctrl</command> supports TLS encrypted
connections. It is strongly recommended that you use an encrypted
connection for all remote use of mosquitto_ctrl.</para>
<para>To enable TLS connections when using x509 certificates, one of
either <option>--cafile</option> or <option>--capath</option> must
be provided as an option.</para>
<para>To enable TLS connections when using TLS-PSK, you must use the
<option>--psk</option> and the <option>--psk-identity</option>
options.</para>
</refsect1>
<refsect1>
<title>Dynamic Security Module</title>
<para>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
<command>dynsec</command> module allows you to interact with
a broker using the dynamic security plugin.</para>
<refsect2>
<title>Configuration file initialisation</title>
<para></para>
<itemizedlist mark="circle">
<listitem><para>mosquitto_ctrl dynsec init <literal>filename</literal> <literal>admin-user</literal> admin-password [admin-role]</para></listitem>
</itemizedlist>
</refsect2>
</refsect1>
<refsect1>
<title>Connection Options</title>
<para>The options below may be given on the command line, but may also
be placed in a config file located at
<option>$XDG_CONFIG_HOME/mosquitto_ctrl</option> or
<option>$HOME/.config/mosquitto_ctrl</option> with one pair of
<option>-option <replaceable>value</replaceable></option>
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. <option>-S</option>. Config file lines that have a
<option>#</option> as the first character are treated as comments
and not processed any further.</para>
<variablelist>
<varlistentry>
<term><option>-A</option></term>
<listitem>
<para>Bind the outgoing connection to a local ip
address/hostname. Use this argument if you need to
restrict network communication to a particular
interface.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--cafile</option></term>
<listitem>
<para>Define the path to a file containing PEM encoded CA
certificates that are trusted. Used to enable SSL
communication.</para>
<para>See also <option>--capath</option></para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--capath</option></term>
<listitem>
<para>Define the path to a directory containing PEM encoded CA
certificates that are trusted. Used to enable SSL
communication.</para>
<para>For <option>--capath</option> to work correctly, the
certificate files must have ".crt" as the file ending
and you must run "openssl rehash &lt;path to capath&gt;" each
time you add/remove a certificate.</para>
<para>See also <option>--cafile</option></para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--cert</option></term>
<listitem>
<para>Define the path to a file containing a PEM encoded
certificate for this client, if required by the
server.</para>
<para>See also <option>--key</option>.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--ciphers</option></term>
<listitem>
<para>An openssl compatible list of TLS ciphers to support
in the client. See
<citerefentry><refentrytitle>ciphers</refentrytitle><manvolnum>1</manvolnum></citerefentry>
for more information.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-d</option></term>
<term><option>--debug</option></term>
<listitem>
<para>Enable debug messages.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-D</option></term>
<term><option>--property</option></term>
<listitem>
<para>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:</para>
<para><option>-D command identifier value</option></para>
<para><option>-D command identifier name value</option></para>
<para><option>command</option> 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
<link linkend='properties'>Properties</link>
section.</para>
<para><option>identifier</option> is the name of the
property to add. This is as described in the
specification, but with '-' as a word separator. For
example:
<option>payload-format-indicator</option>. More details
are in the <link linkend='properties'>Properties</link>
section.</para>
<para><option>value</option> is the value of the property
to add, with a data type that is property
specific.</para>
<para><option>name</option> is only used for the
<option>user-property</option> property as the first of
the two strings in the string pair. In that case,
<option>value</option> is the second of the strings in
the pair.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--help</option></term>
<listitem>
<para>Display usage information.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-h</option></term>
<term><option>--host</option></term>
<listitem>
<para>Specify the host to connect to. Defaults to localhost.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-i</option></term>
<term><option>--id</option></term>
<listitem>
<para>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
<option>mosq-XXXXXXXXXXXXXXXXXX</option>, where the
<option>X</option> 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.</para>
<para>This option cannot be used at the same time as the
<option>--id-prefix</option> argument.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--insecure</option></term>
<listitem>
<para>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 <emphasis>only</emphasis>. 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.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--key</option></term>
<listitem>
<para>Define the path to a file containing a PEM encoded
private key for this client, if required by the
server.</para>
<para>See also <option>--cert</option>.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--keyform</option></term>
<listitem>
<para>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.</para>
<para>See also <option>--tls-engine</option>.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-L</option></term>
<term><option>--url</option></term>
<listitem>
<para>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</para>
<para>If the scheme is mqtt:// then the port defaults to
1883. If the scheme is mqtts:// then the port defaults
to 8883.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--nodelay</option></term>
<listitem>
<para>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.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-p</option></term>
<term><option>--port</option></term>
<listitem>
<para>Connect to the port specified. If not given, the
default of 1883 for plain MQTT or 8883 for MQTT over
TLS will be used.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-P</option></term>
<term><option>--pw</option></term>
<listitem>
<para>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>--username</option> option.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--proxy</option></term>
<listitem>
<para>Specify a SOCKS5 proxy to connect through. "None" and
"username" authentication types are supported. The
<option>socks-url</option> must be of the form
<option>socks5h://[username[:password]@]host[:port]</option>.
The protocol prefix <option>socks5h</option> 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.</para>
<para>If username is not given, then no authentication is
attempted. If the port is not given, then the default
of 1080 is used.</para>
<para>More SOCKS versions may be available in the future,
depending on demand, and will use different protocol
prefixes as described in <citerefentry>
<refentrytitle>curl</refentrytitle>
<manvolnum>1</manvolnum> </citerefentry>.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--psk</option></term>
<listitem>
<para>Provide the hexadecimal (no leading 0x)
pre-shared-key matching the one used on the broker to
use TLS-PSK encryption support.
<option>--psk-identity</option> must also be provided
to enable TLS-PSK.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--psk-identity</option></term>
<listitem>
<para>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.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-q</option></term>
<term><option>--qos</option></term>
<listitem>
<para>Specify the quality of service to use for messages, from 0, 1 and 2. Defaults to 1.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--quiet</option></term>
<listitem>
<para>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 <option>--port</option> without a
port).</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--tls-alpn</option></term>
<listitem>
<para>Provide a protocol to use when connecting to a broker
that has multiple protocols available on a single port,
e.g. MQTT and WebSockets.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--tls-engine</option></term>
<listitem>
<para>A valid openssl engine id. These can be listed with
openssl engine command.</para>
<para>See also <option>--keyform</option>.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--tls-engine-kpass-sha1</option></term>
<listitem>
<para>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.</para>
<para>See also <option>--tls-engine</option>.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--tls-version</option></term>
<listitem>
<para>Choose which TLS protocol version to use when
communicating with the broker. Valid options are
<option>tlsv1.3</option>, <option>tlsv1.2</option> and
<option>tlsv1.1</option>. The default value is
<option>tlsv1.2</option>. Must match the protocol
version used by the broker.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-u</option></term>
<term><option>--username</option></term>
<listitem>
<para>Provide a username to be used for authenticating with
the broker. See also the <option>--pw</option>
argument.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>--unix</option></term>
<listitem>
<para>Connect to a broker through a local unix domain socket
instead of a TCP socket. This is a replacement for
<option>-h</option> and <option>-L</option>. For example:
<option>mosquitto_ctrl --unix /tmp/mosquitto.sock ...</option>
</para>
<para>See the <option>socket_domain</option> option in
<refentrytitle>
<link xlink:href="mosquitto-conf-5.html">mosquitto.conf</link>
</refentrytitle><manvolnum>5</manvolnum>
to configure Mosquitto to listen on a unix socket.</para>
</listitem>
</varlistentry>
<varlistentry>
<term><option>-V</option></term>
<term><option>--protocol-version</option></term>
<listitem>
<para>Specify which version of the MQTT protocol should be
used when connecting to the rmeote broker. Can be
<option>5</option>, <option>311</option>,
<option>31</option>, or the more verbose
<option>mqttv5</option>, <option>mqttv311</option>, or
<option>mqttv31</option>.
Defaults to <option>311</option>.</para>
</listitem>
</varlistentry>
</variablelist>
</refsect1>
<refsect1 id='properties'>
<title>Properties</title>
<para>The <option>-D</option> / <option>--property</option> option
allows adding properties to different stages of the mosquitto_ctrl
run. The properties supported for each command are as
follows:</para>
<refsect2>
<title>Connect</title>
<itemizedlist>
<listitem><para><option>authentication-data</option> (binary data - note treated as a string in mosquitto_ctrl)</para></listitem>
<listitem><para><option>authentication-method</option> (UTF-8 string pair)</para></listitem>
<listitem><para><option>maximum-packet-size</option> (32-bit unsigned integer)</para></listitem>
<listitem><para><option>receive-maximum</option> (16-bit unsigned integer)</para></listitem>
<listitem><para><option>request-problem-information</option> (8-bit unsigned integer)</para></listitem>
<listitem><para><option>request-response-information</option> (8-bit unsigned integer)</para></listitem>
<listitem><para><option>session-expiry-interval</option> (32-bit unsigned integer, note use <option>-x</option> instead)</para></listitem>
<listitem><para><option>topic-alias-maximum</option> (16-bit unsigned integer)</para></listitem>
<listitem><para><option>user-property</option> (UTF-8 string pair)</para></listitem>
</itemizedlist>
</refsect2>
<refsect2>
<title>Publish</title>
<itemizedlist>
<listitem><para><option>content-type</option> (UTF-8 string)</para></listitem>
<listitem><para><option>correlation-data</option> (binary data - note treated as a string in mosquitto_ctrl)</para></listitem>
<listitem><para><option>message-expiry-interval</option> (32-bit unsigned integer)</para></listitem>
<listitem><para><option>payload-format-indicator</option> (8-bit unsigned integer)</para></listitem>
<listitem><para><option>response-topic</option> (UTF-8 string)</para></listitem>
<listitem><para><option>topic-alias</option> (16-bit unsigned integer)</para></listitem>
<listitem><para><option>user-property</option> (UTF-8 string pair)</para></listitem>
</itemizedlist>
</refsect2>
<refsect2>
<title>Disconnect</title>
<itemizedlist>
<listitem><para><option>session-expiry-interval</option> (32-bit unsigned integer)</para></listitem>
<listitem><para><option>user-property</option> (UTF-8 string pair)</para></listitem>
</itemizedlist>
</refsect2>
<refsect2>
<title>Will properties</title>
<itemizedlist>
<listitem><para><option>content-type</option> (UTF-8 string)</para></listitem>
<listitem><para><option>correlation-data</option> (binary data - note treated as a string in mosquitto_ctrl)</para></listitem>
<listitem><para><option>message-expiry-interval</option> (32-bit unsigned integer)</para></listitem>
<listitem><para><option>payload-format-indicator</option> (8-bit unsigned integer)</para></listitem>
<listitem><para><option>response-topic</option> (UTF-8 string)</para></listitem>
<listitem><para><option>user-property</option> (UTF-8 string pair)</para></listitem>
<listitem><para><option>will-delay-interval</option> (32-bit unsigned integer)</para></listitem>
</itemizedlist>
</refsect2>
</refsect1>
<refsect1>
<title>Exit Status</title>
<para>
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.
</para>
<para>MQTT v3.1.1 CONNACK codes:</para>
<itemizedlist mark="circle">
<listitem><para><option>0</option> Success</para></listitem>
<listitem><para><option>1</option> Connection refused: Bad protocol version</para></listitem>
<listitem><para><option>2</option> Connection refused: Identifier rejected</para></listitem>
<listitem><para><option>3</option> Connection refused: Server unavailable</para></listitem>
<listitem><para><option>4</option> Connection refused: Bad username/password</para></listitem>
<listitem><para><option>5</option> Connection refused: Not authorized</para></listitem>
</itemizedlist>
<para>MQTT v5 CONNACK codes:</para>
<itemizedlist>
<listitem><para><option>0</option> Success</para></listitem>
<listitem><para><option>128</option> Unspecified error</para></listitem>
<listitem><para><option>129</option> Malformed packet</para></listitem>
<listitem><para><option>130</option> Protocol error</para></listitem>
<listitem><para><option>131</option> Implementation specific error</para></listitem>
<listitem><para><option>132</option> Unsupported protocol version</para></listitem>
<listitem><para><option>133</option> Client ID not valid</para></listitem>
<listitem><para><option>134</option> Bad username or password</para></listitem>
<listitem><para><option>135</option> Not authorized</para></listitem>
<listitem><para><option>136</option> Server unavailable</para></listitem>
<listitem><para><option>137</option> Server busy</para></listitem>
<listitem><para><option>138</option> Banned</para></listitem>
<listitem><para><option>139</option> Server shutting down</para></listitem>
<listitem><para><option>140</option> Bad authentication method</para></listitem>
<listitem><para><option>141</option> Keep alive timeout</para></listitem>
<listitem><para><option>142</option> Session taken over</para></listitem>
<listitem><para><option>143</option> Topic filter invalid</para></listitem>
<listitem><para><option>144</option> Topic name invalid</para></listitem>
<listitem><para><option>147</option> Receive maximum exceeded</para></listitem>
<listitem><para><option>148</option> Topic alias invalid</para></listitem>
<listitem><para><option>149</option> Packet too large</para></listitem>
<listitem><para><option>148</option> Message rate too high</para></listitem>
<listitem><para><option>151</option> Quota exceeded</para></listitem>
<listitem><para><option>152</option> Administrative action</para></listitem>
<listitem><para><option>153</option> Payload format invalid</para></listitem>
<listitem><para><option>154</option> Retain not supported</para></listitem>
<listitem><para><option>155</option> QoS not supported</para></listitem>
<listitem><para><option>156</option> Use another server</para></listitem>
<listitem><para><option>157</option> Server moved</para></listitem>
<listitem><para><option>158</option> Shared subscriptions not supported</para></listitem>
<listitem><para><option>159</option> Connection rate exceeded</para></listitem>
<listitem><para><option>160</option> Maximum connect time</para></listitem>
<listitem><para><option>161</option> Subscription IDs not supported</para></listitem>
<listitem><para><option>162</option> Wildcard subscriptions not supported</para></listitem>
</itemizedlist>
</refsect1>
<refsect1>
<title>Bugs</title>
<para><command>mosquitto</command> bug information can be found at
<ulink url="https://github.com/eclipse/mosquitto/issues"/></para>
</refsect1>
<refsect1>
<title>See Also</title>
<simplelist type="inline">
<member>
<citerefentry>
<refentrytitle><link xlink:href="mqtt-7.html">mqtt</link></refentrytitle>
<manvolnum>7</manvolnum>
</citerefentry>
</member>
<member>
<citerefentry>
<refentrytitle><link xlink:href="mosquitto_rr-1.html">mosquitto_rr</link></refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>
</member>
<member>
<citerefentry>
<refentrytitle><link xlink:href="mosquitto_pub-1.html">mosquitto_pub</link></refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>
</member>
<member>
<citerefentry>
<refentrytitle><link xlink:href="mosquitto_sub-1.html">mosquitto_sub</link></refentrytitle>
<manvolnum>1</manvolnum>
</citerefentry>
</member>
<member>
<citerefentry>
<refentrytitle><link xlink:href="mosquitto-8.html">mosquitto</link></refentrytitle>
<manvolnum>8</manvolnum>
</citerefentry>
</member>
<member>
<citerefentry>
<refentrytitle><link xlink:href="libmosquitto-3.html">libmosquitto</link></refentrytitle>
<manvolnum>3</manvolnum>
</citerefentry>
</member>
<member>
<citerefentry>
<refentrytitle><link xlink:href="mosquitto-tls-7.html">mosquitto-tls</link></refentrytitle>
<manvolnum>7</manvolnum>
</citerefentry>
</member>
</simplelist>
</refsect1>
<refsect1>
<title>Author</title>
<para>Roger Light <email>roger@atchoo.org</email></para>
</refsect1>
</refentry>

View File

@ -1,2 +1,3 @@
add_subdirectory(dynamic-security)
add_subdirectory(message-timestamp)
add_subdirectory(payload-modification)

View File

@ -1,4 +1,5 @@
DIRS= \
dynamic-security \
message-timestamp \
payload-modification

View File

@ -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.

View File

@ -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}")

View File

@ -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"

View File

@ -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/<feature>/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/#
```

View File

@ -0,0 +1,248 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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;
}

View File

@ -0,0 +1,193 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <openssl/bio.h>
#include <openssl/buffer.h>
#include <openssl/evp.h>
#include <openssl/rand.h>
#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; i<len; i++){
if( ((char *)a)[i] != ((char *)b)[i] ){
rc = 1;
}
}
return rc;
}
int dynsec_auth__basic_auth_callback(int event, void *event_data, void *userdata)
{
struct mosquitto_evt_basic_auth *ed = event_data;
struct dynsec__client *client;
unsigned char password_hash[64]; /* For SHA512 */
const char *clientid;
if(ed->username == 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;
}
}

View File

@ -0,0 +1,868 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <stdio.h>
#include <uthash.h>
#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;
}

View File

@ -0,0 +1,223 @@
#ifndef DYNAMIC_SECURITY_H
#define DYNAMIC_SECURITY_H
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <uthash.h>
#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

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,9 @@ Contributors:
#include <cJSON.h>
#include <stdbool.h>
#include <stdlib.h>
#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;

View File

@ -0,0 +1,485 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#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<supported_version_count; i++){
if(supported_versions[i] == 5){
return 5;
}
}
return -1;
}
static int dynsec__general_config_load(cJSON *tree)
{
cJSON *j_default_access, *jtmp;
j_default_access = cJSON_GetObjectItem(tree, "defaultACLAccess");
if(j_default_access && cJSON_IsObject(j_default_access)){
jtmp = cJSON_GetObjectItem(j_default_access, "publishClientToBroker");
if(jtmp && cJSON_IsBool(jtmp)){
default_access.publish_c2b = cJSON_IsTrue(jtmp);
}else{
default_access.publish_c2b = false;
}
jtmp = cJSON_GetObjectItem(j_default_access, "publishBrokerToClient");
if(jtmp && cJSON_IsBool(jtmp)){
default_access.publish_b2c = cJSON_IsTrue(jtmp);
}else{
default_access.publish_b2c = false;
}
jtmp = cJSON_GetObjectItem(j_default_access, "subscribe");
if(jtmp && cJSON_IsBool(jtmp)){
default_access.subscribe = cJSON_IsTrue(jtmp);
}else{
default_access.subscribe = false;
}
jtmp = cJSON_GetObjectItem(j_default_access, "unsubscribe");
if(jtmp && cJSON_IsBool(jtmp)){
default_access.unsubscribe = cJSON_IsTrue(jtmp);
}else{
default_access.unsubscribe = false;
}
}
return MOSQ_ERR_SUCCESS;
}
static int dynsec__general_config_save(cJSON *tree)
{
cJSON *j_default_access;
j_default_access = cJSON_CreateObject();
if(j_default_access == NULL){
return 1;
}
cJSON_AddItemToObject(tree, "defaultACLAccess", j_default_access);
if(cJSON_AddBoolToObject(j_default_access, "publishClientToBroker", default_access.publish_c2b) == NULL
|| cJSON_AddBoolToObject(j_default_access, "publishBrokerToClient", default_access.publish_b2c) == NULL
|| cJSON_AddBoolToObject(j_default_access, "subscribe", default_access.subscribe) == NULL
|| cJSON_AddBoolToObject(j_default_access, "unsubscribe", default_access.unsubscribe) == NULL
){
return 1;
}
return MOSQ_ERR_SUCCESS;
}
static int dynsec__config_load(void)
{
FILE *fptr;
long flen;
char *json_str;
cJSON *tree;
/* Save to file */
fptr = fopen(config_file, "rt");
if(fptr == NULL){
return 1;
}
fseek(fptr, 0, SEEK_END);
flen = ftell(fptr);
fseek(fptr, 0, SEEK_SET);
json_str = mosquitto_calloc(flen+1, sizeof(char));
if(json_str == NULL){
fclose(fptr);
return 1;
}
if(fread(json_str, 1, flen, fptr) != flen){
fclose(fptr);
return 1;
}
fclose(fptr);
tree = cJSON_Parse(json_str);
mosquitto_free(json_str);
if(tree == NULL){
return 1;
}
if(dynsec__general_config_load(tree)){
cJSON_Delete(tree);
return 1;
}
if(dynsec_roles__config_load(tree)){
cJSON_Delete(tree);
return 1;
}
if(dynsec_clients__config_load(tree)){
cJSON_Delete(tree);
return 1;
}
if(dynsec_groups__config_load(tree)){
cJSON_Delete(tree);
return 1;
}
cJSON_Delete(tree);
return 0;
}
void dynsec__config_save(void)
{
cJSON *tree;
int file_path_len;
char *file_path;
FILE *fptr;
int json_str_len;
char *json_str;
tree = cJSON_CreateObject();
if(tree == NULL) return;
if(dynsec__general_config_save(tree)){
cJSON_Delete(tree);
return;
}
if(dynsec_clients__config_save(tree)){
cJSON_Delete(tree);
return;
}
if(dynsec_groups__config_save(tree)){
cJSON_Delete(tree);
return;
}
if(dynsec_roles__config_save(tree)){
cJSON_Delete(tree);
return;
}
/* Print json to string */
json_str = cJSON_Print(tree);
if(json_str == NULL){
cJSON_Delete(tree);
return;
}
cJSON_Delete(tree);
json_str_len = strlen(json_str);
/* Save to file */
file_path_len = strlen(config_file) + 1;
file_path = mosquitto_malloc(file_path_len);
if(file_path == NULL){
mosquitto_free(json_str);
return;
}
snprintf(file_path, file_path_len, "%s.new", config_file);
fptr = fopen(file_path, "wt");
if(fptr == NULL){
mosquitto_free(json_str);
mosquitto_free(file_path);
return;
}
fwrite(json_str, 1, json_str_len, fptr);
mosquitto_free(json_str);
fclose(fptr);
/* Everything is ok, so move new file over proper file */
rename(file_path, config_file);
mosquitto_free(file_path);
}
int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **user_data, struct mosquitto_opt *options, int option_count)
{
int i;
for(i=0; i<option_count; i++){
if(!strcasecmp(options[i].key, "config_file")){
config_file = mosquitto_strdup(options[i].value);
if(config_file == NULL){
return MOSQ_ERR_NOMEM;
}
break;
}
}
if(config_file == NULL){
mosquitto_log_printf(MOSQ_LOG_WARNING, "Warning: Dynamic security plugin has no plugin_opt_config_file defined. The plugin will not be activated.");
return MOSQ_ERR_SUCCESS;
}
plg_id = identifier;
dynsec__config_load();
mosquitto_callback_register(plg_id, MOSQ_EVT_CONTROL, dynsec_control_callback, "$CONTROL/dynamic-security/v1", NULL);
mosquitto_callback_register(plg_id, MOSQ_EVT_BASIC_AUTH, dynsec_auth__basic_auth_callback, NULL, NULL);
mosquitto_callback_register(plg_id, MOSQ_EVT_ACL_CHECK, dynsec__acl_check_callback, NULL, NULL);
return MOSQ_ERR_SUCCESS;
}
int mosquitto_plugin_cleanup(void *user_data, struct mosquitto_opt *options, int option_count)
{
if(plg_id){
mosquitto_callback_unregister(plg_id, MOSQ_EVT_CONTROL, dynsec_control_callback, "$CONTROL/dynamic-security/v1");
mosquitto_callback_unregister(plg_id, MOSQ_EVT_BASIC_AUTH, dynsec_auth__basic_auth_callback, NULL);
mosquitto_callback_unregister(plg_id, MOSQ_EVT_ACL_CHECK, dynsec__acl_check_callback, NULL);
}
dynsec_groups__cleanup();
dynsec_clients__cleanup();
dynsec_roles__cleanup();
mosquitto_free(config_file);
config_file = NULL;
return MOSQ_ERR_SUCCESS;
}
/* ################################################################
* #
* # $CONTROL/dynamic-security/v1 handler
* #
* ################################################################ */
int dynsec__handle_control(cJSON *j_responses, struct mosquitto *context, cJSON *commands)
{
int rc = MOSQ_ERR_SUCCESS;
cJSON *aiter;
char *command;
char *correlation_data = NULL;
cJSON_ArrayForEach(aiter, commands){
if(cJSON_IsObject(aiter)){
if(json_get_string(aiter, "command", &command, false) == MOSQ_ERR_SUCCESS){
if(json_get_string(aiter, "correlationData", &correlation_data, true) != MOSQ_ERR_SUCCESS){
dynsec__command_reply(j_responses, context, command, "Invalid correlationData data type.", NULL);
return MOSQ_ERR_INVAL;
}
/* Plugin */
if(!strcasecmp(command, "setDefaultACLAccess")){
rc = dynsec__process_default_acl_access(j_responses, context, aiter, correlation_data);
/* Clients */
}else if(!strcasecmp(command, "createClient")){
rc = dynsec_clients__process_create(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "deleteClient")){
rc = dynsec_clients__process_delete(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "getClient")){
rc = dynsec_clients__process_get(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "listClients")){
rc = dynsec_clients__process_list(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "modifyClient")){
rc = dynsec_clients__process_modify(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "setClientPassword")){
rc = dynsec_clients__process_set_password(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "addClientRole")){
rc = dynsec_clients__process_add_role(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "removeClientRole")){
rc = dynsec_clients__process_remove_role(j_responses, context, aiter, correlation_data);
/* Groups */
}else if(!strcasecmp(command, "addGroupClient")){
rc = dynsec_groups__process_add_client(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "createGroup")){
rc = dynsec_groups__process_create(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "deleteGroup")){
rc = dynsec_groups__process_delete(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "getGroup")){
rc = dynsec_groups__process_get(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "listGroups")){
rc = dynsec_groups__process_list(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "modifyGroup")){
rc = dynsec_groups__process_modify(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "removeGroupClient")){
rc = dynsec_groups__process_remove_client(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "addGroupRole")){
rc = dynsec_groups__process_add_role(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "removeGroupRole")){
rc = dynsec_groups__process_remove_role(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "setAnonymousGroup")){
rc = dynsec_groups__process_set_anonymous_group(j_responses, context, aiter, correlation_data);
/* Roles */
}else if(!strcasecmp(command, "createRole")){
rc = dynsec_roles__process_create(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "getRole")){
rc = dynsec_roles__process_get(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "listRoles")){
rc = dynsec_roles__process_list(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "modifyRole")){
rc = dynsec_roles__process_modify(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "deleteRole")){
rc = dynsec_roles__process_delete(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "addRoleACL")){
rc = dynsec_roles__process_add_acl(j_responses, context, aiter, correlation_data);
}else if(!strcasecmp(command, "removeRoleACL")){
rc = dynsec_roles__process_remove_acl(j_responses, context, aiter, correlation_data);
/* Unknown */
}else{
dynsec__command_reply(j_responses, context, command, "Unknown command", correlation_data);
rc = MOSQ_ERR_INVAL;
}
}else{
dynsec__command_reply(j_responses, context, "Unknown command", "Missing command", correlation_data);
rc = MOSQ_ERR_INVAL;
}
}else{
dynsec__command_reply(j_responses, context, "Unknown command", "Command not an object", correlation_data);
rc = MOSQ_ERR_INVAL;
}
}
return rc;
}

View File

@ -0,0 +1,953 @@
/*
Copyright (c) 2020 Roger Light <roger@atchoo.org>
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 <cJSON.h>
#include <stdio.h>
#include <string.h>
#include <uthash.h>
#include <utlist.h>
#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;
}

View File

@ -0,0 +1,218 @@
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
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<sub_levels; i++){
acl_token = strtok_hier(acl_saveptr, &acl_saveptr);
sub_token = strtok_hier(sub_saveptr, &sub_saveptr);
if(i<acl_levels &&
(!strcmp(acl_token, "+")
|| !strcmp(acl_token, sub_token))){
/* This level matches a single level wildcard, or is an exact
* match, so carry on checking. */
}else if(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

View File

@ -30,3 +30,4 @@ _mosquitto_property_free_all
_mosquitto_realloc
_mosquitto_set_username
_mosquitto_strdup
_mosquitto_topic_matches_sub

View File

@ -31,4 +31,5 @@
mosquitto_realloc;
mosquitto_set_username;
mosquitto_strdup;
mosquitto_topic_matches_sub;
};

View File

@ -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
* ============================================================ */

View File

@ -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;

143
test/broker/14-dynsec-client.py Executable file
View File

@ -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)

160
test/broker/14-dynsec-group.py Executable file
View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

220
test/broker/14-dynsec-role.py Executable file
View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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