From 0b4ab2f285c921925affd8bb9bb4780bca359c58 Mon Sep 17 00:00:00 2001 From: "Roger A. Light" Date: Thu, 10 Jul 2025 10:59:38 +0100 Subject: [PATCH] Move password file code to own plugin --- plugins/CMakeLists.txt | 5 + plugins/Makefile | 1 + plugins/password-file/CMakeLists.txt | 12 + plugins/password-file/Makefile | 21 ++ plugins/password-file/password_check.c | 54 ++++ plugins/password-file/password_parse.c | 137 ++++++++++ plugins/password-file/plugin.c | 79 ++++++ plugins/password-file/test.conf | 4 + plugins/password-file/test.pwfile | 1 + plugins/password-file/test.sh | 2 + src/CMakeLists.txt | 3 + src/Makefile | 9 + src/acl_file.c | 8 +- src/conf.c | 23 +- src/listeners.c | 4 +- src/mosquitto_broker_internal.h | 17 +- src/password_file.c | 86 +++++++ src/password_file.h | 39 +++ src/plugin_cleanup.c | 2 +- src/plugin_init.c | 2 +- src/security_default.c | 236 +----------------- ...lugin-connect-uname-password-denied.pwfile | 1 + ...01-plugin-connect-uname-password-denied.py | 53 ++++ test/broker/Makefile | 1 + test/broker/test.py | 1 + 25 files changed, 543 insertions(+), 258 deletions(-) create mode 100644 plugins/password-file/CMakeLists.txt create mode 100644 plugins/password-file/Makefile create mode 100644 plugins/password-file/password_check.c create mode 100644 plugins/password-file/password_parse.c create mode 100644 plugins/password-file/plugin.c create mode 100644 plugins/password-file/test.conf create mode 100644 plugins/password-file/test.pwfile create mode 100755 plugins/password-file/test.sh create mode 100644 src/password_file.c create mode 100644 src/password_file.h create mode 100644 test/broker/01-plugin-connect-uname-password-denied.pwfile create mode 100755 test/broker/01-plugin-connect-uname-password-denied.py diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index f91f1fdf..1aeca77e 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -41,6 +41,7 @@ option(WITH_PLUGIN_ACL_FILE "Build acl-file plugin?" ON) option(WITH_PLUGIN_DYNAMIC_SECURITY "Build dynamic-security plugin?" ON) option(WITH_PLUGIN_EXAMPLES "Build example plugins?" ON) option(WITH_PLUGIN_PERSIST_SQLITE "Build persist-sqlite plugin?" ON) +option(WITH_PLUGIN_PASSWORD_FILE "Build password-file plugin?" ON) option(WITH_PLUGIN_SPARKPLUG_AWARE "Build sparkplug-aware plugin?" ON) if(WITH_PLUGIN_ACL_FILE) @@ -55,6 +56,10 @@ if (WITH_PLUGIN_EXAMPLES) add_subdirectory(examples) endif() +if(WITH_PLUGIN_PASSWORD_FILE) + add_subdirectory(password-file) +endif() + if (WITH_PLUGIN_PERSIST_SQLITE) find_package(SQLite3 REQUIRED) add_subdirectory(persist-sqlite) diff --git a/plugins/Makefile b/plugins/Makefile index d06e1b71..aab6c973 100644 --- a/plugins/Makefile +++ b/plugins/Makefile @@ -2,6 +2,7 @@ DIRS= \ acl-file \ dynamic-security \ examples \ + password-file \ persist-sqlite \ sparkplug-aware diff --git a/plugins/password-file/CMakeLists.txt b/plugins/password-file/CMakeLists.txt new file mode 100644 index 00000000..6da58ff0 --- /dev/null +++ b/plugins/password-file/CMakeLists.txt @@ -0,0 +1,12 @@ +set(PLUGIN_NAME mosquitto_password_file) + +set(SRCLIST + password_check.c + password_parse.c + plugin.c +) + +set(INCLIST ${mosquitto_SOURCE_DIR}/src) +set(LINKLIST libmosquitto_common) + +add_mosquitto_plugin("${PLUGIN_NAME}" "${SRCLIST}" "${INCLIST}" "${LINKLIST}") diff --git a/plugins/password-file/Makefile b/plugins/password-file/Makefile new file mode 100644 index 00000000..b8daabcc --- /dev/null +++ b/plugins/password-file/Makefile @@ -0,0 +1,21 @@ +R=../.. +include ${R}/config.mk + +PLUGIN_NAME=mosquitto_password_file +LOCAL_CFLAGS+= +LOCAL_CPPFLAGS+=-I${R}/src +LOCAL_LDFLAGS+= +LOCAL_LIBADD+=${R}/libcommon/libmosquitto_common.so.${SOVERSION} + +# Objects for this plugin only, built from source in this directory +OBJS = \ + password_check.o \ + password_parse.o \ + plugin.o + +# Objects from e.g. the common directory that are not in this directory +OBJS_EXTERNAL = + +all : binary + +include ${R}/plugins/plugin.mk diff --git a/plugins/password-file/password_check.c b/plugins/password-file/password_check.c new file mode 100644 index 00000000..cc8ed9f0 --- /dev/null +++ b/plugins/password-file/password_check.c @@ -0,0 +1,54 @@ +/* +Copyright (c) 2011-2021 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include + +#include "mosquitto.h" +#include "password_file.h" + +int password_file__check(int event, void *event_data, void *userdata) +{ + struct mosquitto_evt_basic_auth *ed = event_data; + struct password_file_data *data = userdata; + struct mosquitto__unpwd *u; + + UNUSED(event); + + if(ed->username == NULL){ + return MOSQ_ERR_PLUGIN_IGNORE; + } + + // FIXME if(ed->client->bridge) return MOSQ_ERR_SUCCESS; + + HASH_FIND(hh, data->unpwd, ed->username, strlen(ed->username), u); + if(u){ + if(u->pw){ + if(ed->password){ + return mosquitto_pw_verify(u->pw, ed->password); + }else{ + return MOSQ_ERR_AUTH; + } + }else{ + return MOSQ_ERR_SUCCESS; + } + } + + return MOSQ_ERR_AUTH; +} diff --git a/plugins/password-file/password_parse.c b/plugins/password-file/password_parse.c new file mode 100644 index 00000000..cf1b0f5b --- /dev/null +++ b/plugins/password-file/password_parse.c @@ -0,0 +1,137 @@ +/* +Copyright (c) 2011-2021 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include + +#include "mosquitto.h" +#include "password_file.h" + +int password_file__parse(struct password_file_data *data) +{ + FILE *pwfile; + struct mosquitto__unpwd *unpwd; + char *username, *password; + char *saveptr = NULL; + char *buf; + int buflen = 256; + + buf = mosquitto_malloc((size_t)buflen); + if(buf == NULL){ + mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Out of memory."); + return MOSQ_ERR_NOMEM; + } + + pwfile = mosquitto_fopen(data->password_file, "rt", true); + if(!pwfile){ + mosquitto_log_printf(MOSQ_LOG_ERR, "password-file: Error: Unable to open pwfile \"%s\".", data->password_file); + mosquitto_FREE(buf); + return MOSQ_ERR_UNKNOWN; + } + + while(!feof(pwfile)){ + if(mosquitto_fgets(&buf, &buflen, pwfile)){ + if(buf[0] == '#') continue; + if(!strchr(buf, ':')) continue; + + username = strtok_r(buf, ":", &saveptr); + if(username){ + username = mosquitto_trimblanks(username); + if(strlen(username) > 65535){ + mosquitto_log_printf(MOSQ_LOG_NOTICE, "password-file: Warning: Invalid line in password file '%s', username too long.", data->password_file); + continue; + } + if(strlen(username) <= 0){ + mosquitto_log_printf(MOSQ_LOG_NOTICE, "password-file: Warning: Empty username in password file '%s', ignoring.", data->password_file); + continue; + } + + HASH_FIND(hh, data->unpwd, username, strlen(username), unpwd); + if(unpwd){ + mosquitto_log_printf(MOSQ_LOG_NOTICE, "password-file: Error: Duplicate user '%s' in password file '%s', ignoring.", username, data->password_file); + continue; + } + + unpwd = mosquitto_calloc(1, sizeof(struct mosquitto__unpwd)); + if(!unpwd){ + fclose(pwfile); + mosquitto_FREE(buf); + return MOSQ_ERR_NOMEM; + } + + unpwd->username = mosquitto_strdup(username); + if(!unpwd->username){ + mosquitto_FREE(unpwd); + mosquitto_FREE(buf); + fclose(pwfile); + return MOSQ_ERR_NOMEM; + } + password = strtok_r(NULL, ":", &saveptr); + if(password){ + password = mosquitto_trimblanks(password); + + if(strlen(password) > 65535){ + mosquitto_log_printf(MOSQ_LOG_NOTICE, "password-file: Warning: Invalid line in password file '%s', password too long.", data->password_file); + mosquitto_FREE(unpwd->username); + mosquitto_FREE(unpwd); + continue; + } + + if(mosquitto_pw_new(&unpwd->pw, MOSQ_PW_DEFAULT) + || mosquitto_pw_decode(unpwd->pw, password)){ + + mosquitto_log_printf(MOSQ_LOG_NOTICE, "password-file: Warning: Unable to decode line in password file '%s'.", data->password_file); + mosquitto_pw_cleanup(unpwd->pw); + mosquitto_FREE(unpwd->username); + mosquitto_FREE(unpwd); + continue; + } + + HASH_ADD_KEYPTR(hh, data->unpwd, unpwd->username, strlen(unpwd->username), unpwd); + }else{ + mosquitto_log_printf(MOSQ_LOG_NOTICE, "password-file: Warning: Invalid line in password file '%s': %s", data->password_file, buf); + mosquitto_pw_cleanup(unpwd->pw); + mosquitto_FREE(unpwd->username); + mosquitto_FREE(unpwd); + } + } + } + } + fclose(pwfile); + mosquitto_FREE(buf); + + return MOSQ_ERR_SUCCESS; +} + + +void password_file__cleanup(struct password_file_data *data) +{ + struct mosquitto__unpwd *u, *tmp = NULL; + + if(!data) return; + + HASH_ITER(hh, data->unpwd, u, tmp){ + HASH_DEL(data->unpwd, u); + mosquitto_pw_cleanup(u->pw); + mosquitto_FREE(u->username); + mosquitto_FREE(u); + } +} diff --git a/plugins/password-file/plugin.c b/plugins/password-file/plugin.c new file mode 100644 index 00000000..d97292dc --- /dev/null +++ b/plugins/password-file/plugin.c @@ -0,0 +1,79 @@ +/* +Copyright (c) 2025 Cedalo Gmbh +*/ + +#include "config.h" + +#include +#include + +#include "mosquitto.h" +#include "password_file.h" + +#define PLUGIN_NAME "password-file" + +MOSQUITTO_PLUGIN_DECLARE_VERSION(5); + +static mosquitto_plugin_id_t *mosq_pid = NULL; + +static int handle_options(struct password_file_data *data, struct mosquitto_opt *options, int option_count) +{ + for(int i=0; ipassword_file); + data->password_file = mosquitto_strdup(options[i].value); + if(!data->password_file){ + return MOSQ_ERR_NOMEM; + } + }else{ + mosquitto_log_printf(MOSQ_LOG_ERR, PLUGIN_NAME ": Error: Unknown option '%s'.", options[i].key); + return MOSQ_ERR_INVAL; + } + } + + return MOSQ_ERR_SUCCESS; +} + + +int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **user_data, struct mosquitto_opt *options, int option_count) +{ + struct password_file_data *data; + int rc; + + UNUSED(options); + UNUSED(option_count); + + data = mosquitto_calloc(1, sizeof(struct password_file_data)); + if(!data){ + return MOSQ_ERR_NOMEM; + } + *user_data = data; + + mosq_pid = identifier; + mosquitto_plugin_set_info(identifier, PLUGIN_NAME, NULL); + + rc = handle_options(data, options, option_count); + if(rc) return rc; + + rc = password_file__parse(data); + if(rc) return rc; + + rc = mosquitto_callback_register(mosq_pid, MOSQ_EVT_BASIC_AUTH, password_file__check, NULL, data); + if(rc) return rc; + + return MOSQ_ERR_SUCCESS; +} + + +int mosquitto_plugin_cleanup(void *user_data, struct mosquitto_opt *options, int option_count) +{ + struct password_file_data *data = user_data; + + UNUSED(options); + UNUSED(option_count); + + mosquitto_callback_unregister(mosq_pid, MOSQ_EVT_BASIC_AUTH, password_file__check, NULL); + password_file__cleanup(data); + + return MOSQ_ERR_SUCCESS; +} diff --git a/plugins/password-file/test.conf b/plugins/password-file/test.conf new file mode 100644 index 00000000..6d62725f --- /dev/null +++ b/plugins/password-file/test.conf @@ -0,0 +1,4 @@ +listener 1883 +allow_anonymous true +plugin ./mosquitto_password_file.so +plugin_opt_password_file ./test.pwfile diff --git a/plugins/password-file/test.pwfile b/plugins/password-file/test.pwfile new file mode 100644 index 00000000..56bd82dd --- /dev/null +++ b/plugins/password-file/test.pwfile @@ -0,0 +1 @@ +user:$7$1000$h0tqVxBwkB9rKAXukTtffzdbBQtNy1q5FBTDwSW4hucfjpqunBbxW10NVnRk7Cfh0lQndnOv2+k4wJavgz1JNw==$02ujkUXlKkJGFzlQHNjUgXwG3XRB1mr3vs8NX5teGCJGbN4hdgSpHNHuj47j8r5SHXsO7GeHpmkpPNhLraVVcQ== diff --git a/plugins/password-file/test.sh b/plugins/password-file/test.sh new file mode 100755 index 00000000..f18a3524 --- /dev/null +++ b/plugins/password-file/test.sh @@ -0,0 +1,2 @@ +VG="valgrind --log-file=vglog" +${VG} ../../src/mosquitto -c test.conf -v diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 933d30e5..d1d28525 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,9 @@ add_executable(mosquitto ../lib/net_ws.c ../lib/packet_datatypes.c ../lib/packet_mosq.c ../lib/packet_mosq.h + password_file.c password_file.h + ../plugins/password-file/password_check.c + ../plugins/password-file/password_parse.c persist_read_v234.c persist_read_v5.c persist_read.c persist_write_v5.c persist_write.c persist.h diff --git a/src/Makefile b/src/Makefile index 8e913ab1..0861c707 100644 --- a/src/Makefile +++ b/src/Makefile @@ -73,6 +73,7 @@ OBJS= mosquitto.o \ mux_kqueue.o \ mux_poll.o \ net.o \ + password_file.o \ property_broker.o \ persist_read.o \ persist_read_v234.o \ @@ -137,6 +138,8 @@ OBJS_EXTERNAL= \ net_ws.o \ packet_datatypes.o \ packet_mosq.o \ + password_check.o \ + password_parse.o \ property_mosq.o \ send_connect.o \ send_disconnect.o \ @@ -206,6 +209,12 @@ packet_datatypes.o : ${R}/lib/packet_datatypes.c ${R}/lib/packet_mosq.h packet_mosq.o : ${R}/lib/packet_mosq.c ${R}/lib/packet_mosq.h ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(LOCAL_CFLAGS) -c $< -o $@ +password_check.o : ${R}/plugins/password-file/password_check.c password_file.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(LOCAL_CFLAGS) -c $< -o $@ + +password_parse.o : ${R}/plugins/password-file/password_parse.c password_file.h + ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(LOCAL_CFLAGS) -c $< -o $@ + ${R}/deps/picohttpparser/picohttpparser.o : ${R}/deps/picohttpparser/picohttpparser.c ${R}/deps/picohttpparser/picohttpparser.h ${CROSS_COMPILE}${CC} $(LOCAL_CPPFLAGS) $(LOCAL_CFLAGS) -c $< -o $@ diff --git a/src/acl_file.c b/src/acl_file.c index 1d386419..80a351f9 100644 --- a/src/acl_file.c +++ b/src/acl_file.c @@ -39,7 +39,7 @@ int broker_acl_file__init(void) if(db.config->listeners[i].security_options->acl_data.acl_file){ rc = acl_file__parse(&db.config->listeners[i].security_options->acl_data); if(rc){ - log__printf(NULL, MOSQ_LOG_ERR, "Error opening acl file \"%s\".", db.config->listeners[i].security_options->acl_file); + log__printf(NULL, MOSQ_LOG_ERR, "Error opening acl file \"%s\".", db.config->listeners[i].security_options->acl_data.acl_file); return rc; } if(db.config->listeners[i].security_options->plugin_count == 0){ @@ -78,9 +78,6 @@ void broker_acl_file__cleanup(void) MOSQ_EVT_ACL_CHECK, acl_file__check, NULL); acl_file__cleanup(&db.config->listeners[i].security_options->acl_data); - mosquitto_FREE(db.config->listeners[i].security_options->pid->plugin_name); - mosquitto_FREE(db.config->listeners[i].security_options->pid->config.security_options); - mosquitto_FREE(db.config->listeners[i].security_options->pid); } } }else{ @@ -89,9 +86,6 @@ void broker_acl_file__cleanup(void) MOSQ_EVT_ACL_CHECK, acl_file__check, NULL); acl_file__cleanup(&db.config->security_options.acl_data); - mosquitto_FREE(db.config->security_options.pid->plugin_name); - mosquitto_FREE(db.config->security_options.pid->config.security_options); - mosquitto_FREE(db.config->security_options.pid); } } } diff --git a/src/conf.c b/src/conf.c index 383c679a..91f9c61e 100644 --- a/src/conf.c +++ b/src/conf.c @@ -256,7 +256,7 @@ static void config__init_reload(struct mosquitto__config *config) config->allow_duplicate_messages = true; mosquitto_FREE(config->security_options.acl_data.acl_file); - mosquitto_FREE(config->security_options.password_file); + mosquitto_FREE(config->security_options.password_data.password_file); mosquitto_FREE(config->security_options.psk_file); config->security_options.allow_anonymous = -1; @@ -364,7 +364,7 @@ void config__cleanup(struct mosquitto__config *config) mosquitto_FREE(config->persistence_filepath); mosquitto_FREE(config->security_options.auto_id_prefix); mosquitto_FREE(config->security_options.acl_data.acl_file); - mosquitto_FREE(config->security_options.password_file); + mosquitto_FREE(config->security_options.password_data.password_file); mosquitto_FREE(config->security_options.psk_file); mosquitto_FREE(config->security_options.plugins); mosquitto_FREE(config->pid_file); @@ -379,7 +379,7 @@ void config__cleanup(struct mosquitto__config *config) if(config->listeners[i].security_options){ mosquitto_FREE(config->listeners[i].security_options->auto_id_prefix); mosquitto_FREE(config->listeners[i].security_options->acl_data.acl_file); - mosquitto_FREE(config->listeners[i].security_options->password_file); + mosquitto_FREE(config->listeners[i].security_options->password_data.password_file); mosquitto_FREE(config->listeners[i].security_options->psk_file); mosquitto_FREE(config->listeners[i].security_options->plugins); mosquitto_FREE(config->listeners[i].security_options); @@ -611,6 +611,12 @@ static void config__copy(struct mosquitto__config *src, struct mosquitto__config mosquitto_FREE(dest->security_options.acl_data.acl_file); dest->security_options.acl_data.acl_file = src->security_options.acl_data.acl_file; + acl_file__cleanup(&dest->security_options.acl_data); + dest->security_options.acl_data.acl_users = src->security_options.acl_data.acl_users; + dest->security_options.acl_data.acl_patterns = src->security_options.acl_data.acl_patterns; + dest->security_options.acl_data.acl_anon.username = src->security_options.acl_data.acl_anon.username; + dest->security_options.acl_data.acl_anon.acl = src->security_options.acl_data.acl_anon.acl; + dest->security_options.allow_anonymous = src->security_options.allow_anonymous; dest->security_options.allow_zero_length_clientid = src->security_options.allow_zero_length_clientid; @@ -618,8 +624,11 @@ static void config__copy(struct mosquitto__config *src, struct mosquitto__config dest->security_options.auto_id_prefix = src->security_options.auto_id_prefix; dest->security_options.auto_id_prefix_len = src->security_options.auto_id_prefix_len; - mosquitto_FREE(dest->security_options.password_file); - dest->security_options.password_file = src->security_options.password_file; + mosquitto_FREE(dest->security_options.password_data.password_file); + dest->security_options.password_data.password_file = src->security_options.password_data.password_file; + + password_file__cleanup(&dest->security_options.password_data); + dest->security_options.password_data.unpwd = src->security_options.password_data.unpwd; mosquitto_FREE(dest->security_options.psk_file); dest->security_options.psk_file = src->security_options.psk_file; @@ -2064,8 +2073,8 @@ static int config__read_file_core(struct mosquitto__config *config, bool reload, }else if(!strcmp(token, "password_file")){ REQUIRE_LISTENER_IF_PER_LISTENER(token); conf__set_cur_security_options(config, &cur_listener, &cur_security_options, token); - mosquitto_FREE(cur_security_options->password_file); - if(conf__parse_string(&token, "password_file", &cur_security_options->password_file, &saveptr)) return MOSQ_ERR_INVAL; + mosquitto_FREE(cur_security_options->password_data.password_file); + if(conf__parse_string(&token, "password_file", &cur_security_options->password_data.password_file, &saveptr)) return MOSQ_ERR_INVAL; }else if(!strcmp(token, "per_listener_settings")){ OPTION_DEPRECATED(token, "Please see the documentation for how to achieve the same effect."); if(config->per_listener_settings){ diff --git a/src/listeners.c b/src/listeners.c index a10e38c7..6a2df04d 100644 --- a/src/listeners.c +++ b/src/listeners.c @@ -35,8 +35,8 @@ void listener__set_defaults(struct mosquitto__listener *listener) listener->protocol = mp_mqtt; mosquitto_FREE(listener->mount_point); - mosquitto_FREE(listener->security_options->acl_file); - mosquitto_FREE(listener->security_options->password_file); + mosquitto_FREE(listener->security_options->acl_data.acl_file); + mosquitto_FREE(listener->security_options->password_data.password_file); mosquitto_FREE(listener->security_options->psk_file); listener->security_options->allow_anonymous = -1; listener->security_options->allow_zero_length_clientid = true; diff --git a/src/mosquitto_broker_internal.h b/src/mosquitto_broker_internal.h index 7f803808..99c763e2 100644 --- a/src/mosquitto_broker_internal.h +++ b/src/mosquitto_broker_internal.h @@ -46,6 +46,7 @@ Contributors: #include "tls_mosq.h" #include "uthash.h" #include "acl_file.h" +#include "password_file.h" #ifndef __GNUC__ #define __attribute__(attrib) @@ -202,8 +203,7 @@ struct mosquitto__security_options { struct mosquitto__unpwd *unpwd; struct mosquitto__psk *psk_id; struct acl_file_data acl_data; - char *acl_file; - char *password_file; + struct password_file_data password_data; char *psk_file; mosquitto_plugin_id_t **plugins; int plugin_count; @@ -437,13 +437,6 @@ struct mosquitto__psk{ char *password; }; -struct mosquitto__unpwd{ - UT_hash_handle hh; - char *username; - char *clientid; - struct mosquitto_pw *pw; -}; - struct mosquitto__message_v5{ struct mosquitto__message_v5 *next, *prev; char *topic; @@ -908,12 +901,14 @@ int mosquitto_acl_check(struct mosquitto *context, const char *topic, uint32_t p int mosquitto_basic_auth(struct mosquitto *context); int mosquitto_psk_key_get(struct mosquitto *context, const char *hint, const char *identity, char *key, int max_key_len); -int mosquitto_security_init_default(bool reload); +int mosquitto_security_init_default(void); int mosquitto_security_apply_default(void); -int mosquitto_security_cleanup_default(bool reload); +int mosquitto_security_cleanup_default(void); int mosquitto_psk_key_get_default(struct mosquitto *context, const char *hint, const char *identity, char *key, int max_key_len); int broker_acl_file__init(void); void broker_acl_file__cleanup(void); +int broker_password_file__init(void); +void broker_password_file__cleanup(void); int psk_file__init(void); int psk_file__cleanup(void); diff --git a/src/password_file.c b/src/password_file.c new file mode 100644 index 00000000..c1b2671d --- /dev/null +++ b/src/password_file.c @@ -0,0 +1,86 @@ +/* +Copyright (c) 2011-2021 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +#include "config.h" + +#include +#include +#include + +#include "mosquitto_broker_internal.h" +#include "password_file.h" + + +int broker_password_file__init(void) +{ + int rc; + + /* Load username/password data if required. */ + if(db.config->per_listener_settings){ + for(int i=0; ilistener_count; i++){ + if(db.config->listeners[i].security_options->password_data.password_file){ + rc = password_file__parse(&db.config->listeners[i].security_options->password_data); + if(rc){ + return rc; + } + if(db.config->listeners[i].security_options->plugin_count == 0){ + config__plugin_add_secopt(db.config->listeners[i].security_options->pid, db.config->listeners[i].security_options); + } + + mosquitto_callback_register(db.config->listeners[i].security_options->pid, + MOSQ_EVT_BASIC_AUTH, password_file__check, NULL, &db.config->listeners[i].security_options->password_data); + } + } + }else{ + if(db.config->security_options.password_data.password_file){ + rc = password_file__parse(&db.config->security_options.password_data); + if(rc){ + return rc; + } + if(db.config->security_options.plugin_count == 0){ + config__plugin_add_secopt(db.config->security_options.pid, &db.config->security_options); + } + + mosquitto_callback_register(db.config->security_options.pid, + MOSQ_EVT_BASIC_AUTH, password_file__check, NULL, &db.config->security_options.password_data); + } + } + + return MOSQ_ERR_SUCCESS; +} + +void broker_password_file__cleanup(void) +{ + if(db.config->per_listener_settings){ + for(int i=0; ilistener_count; i++){ + if(db.config->listeners[i].security_options->pid){ + mosquitto_callback_unregister(db.config->listeners[i].security_options->pid, + MOSQ_EVT_BASIC_AUTH, password_file__check, NULL); + + password_file__cleanup(&db.config->listeners[i].security_options->password_data); + } + } + }else{ + if(db.config->security_options.pid){ + mosquitto_callback_unregister(db.config->security_options.pid, + MOSQ_EVT_BASIC_AUTH, password_file__check, NULL); + + password_file__cleanup(&db.config->security_options.password_data); + } + } +} diff --git a/src/password_file.h b/src/password_file.h new file mode 100644 index 00000000..620dcb49 --- /dev/null +++ b/src/password_file.h @@ -0,0 +1,39 @@ +/* +Copyright (c) 2011-2021 Roger Light + +All rights reserved. This program and the accompanying materials +are made available under the terms of the Eclipse Public License 2.0 +and Eclipse Distribution License v1.0 which accompany this distribution. + +The Eclipse Public License is available at + https://www.eclipse.org/legal/epl-2.0/ +and the Eclipse Distribution License is available at + http://www.eclipse.org/org/documents/edl-v10.php. + +SPDX-License-Identifier: EPL-2.0 OR BSD-3-Clause + +Contributors: + Roger Light - initial implementation and documentation. +*/ +#ifndef PASSWORD_FILE_H +#define PASSWORD_FILE_H + +#include + +struct mosquitto__unpwd{ + UT_hash_handle hh; + char *username; + char *clientid; + struct mosquitto_pw *pw; +}; + +struct password_file_data{ + struct mosquitto__unpwd *unpwd; + char *password_file; +}; + +int password_file__parse(struct password_file_data *data); +int password_file__check(int event, void *event_data, void *userdata); +void password_file__cleanup(struct password_file_data *data); + +#endif diff --git a/src/plugin_cleanup.c b/src/plugin_cleanup.c index 668c1b19..58c79b82 100644 --- a/src/plugin_cleanup.c +++ b/src/plugin_cleanup.c @@ -131,5 +131,5 @@ int mosquitto_security_cleanup(bool reload) plugin__security_cleanup_single(db.plugins[i], reload); } - return mosquitto_security_cleanup_default(reload); + return mosquitto_security_cleanup_default(); } diff --git a/src/plugin_init.c b/src/plugin_init.c index a92f108f..e1b28677 100644 --- a/src/plugin_init.c +++ b/src/plugin_init.c @@ -163,6 +163,6 @@ int mosquitto_security_init(bool reload) rc = plugin__security_init_single(db.plugins[i], reload); if(rc != MOSQ_ERR_SUCCESS) return rc; } - rc = mosquitto_security_init_default(reload); + rc = mosquitto_security_init_default(); return rc; } diff --git a/src/security_default.c b/src/security_default.c index cd2a0373..620ca153 100644 --- a/src/security_default.c +++ b/src/security_default.c @@ -24,20 +24,14 @@ Contributors: #include "mosquitto_broker_internal.h" #include "mosquitto/mqtt_protocol.h" +#include "password_file.h" #include "send_mosq.h" #include "util_mosq.h" -static int unpwd__file_parse(struct mosquitto__unpwd **unpwd, const char *password_file); -static int unpwd__cleanup(struct mosquitto__unpwd **unpwd, bool reload); -static int mosquitto_basic_auth_default(int event, void *event_data, void *userdata); - -int mosquitto_security_init_default(bool reload) +int mosquitto_security_init_default(void) { int rc; - char *pwf; - - UNUSED(reload); /* Configure plugin identifier */ if(db.config->per_listener_settings){ @@ -61,34 +55,8 @@ int mosquitto_security_init_default(bool reload) config__plugin_add_secopt(db.config->security_options.pid, &db.config->security_options); } - /* Load username/password data if required. */ - if(db.config->per_listener_settings){ - for(int i=0; ilistener_count; i++){ - pwf = db.config->listeners[i].security_options->password_file; - if(pwf){ - rc = unpwd__file_parse(&db.config->listeners[i].security_options->unpwd, pwf); - if(rc){ - log__printf(NULL, MOSQ_LOG_ERR, "Error opening password file \"%s\".", pwf); - return rc; - } - mosquitto_callback_register(db.config->listeners[i].security_options->pid, - MOSQ_EVT_BASIC_AUTH, mosquitto_basic_auth_default, NULL, NULL); - } - } - }else{ - if(db.config->security_options.password_file){ - pwf = db.config->security_options.password_file; - if(pwf){ - rc = unpwd__file_parse(&db.config->security_options.unpwd, pwf); - if(rc){ - log__printf(NULL, MOSQ_LOG_ERR, "Error opening password file \"%s\".", pwf); - return rc; - } - } - mosquitto_callback_register(db.config->security_options.pid, - MOSQ_EVT_BASIC_AUTH, mosquitto_basic_auth_default, NULL, NULL); - } - } + rc = broker_password_file__init(); + if(rc) return rc; rc = broker_acl_file__init(); if(rc) return rc; @@ -99,31 +67,20 @@ int mosquitto_security_init_default(bool reload) return MOSQ_ERR_SUCCESS; } -int mosquitto_security_cleanup_default(bool reload) + +int mosquitto_security_cleanup_default(void) { int rc = 0; + broker_password_file__cleanup(); broker_acl_file__cleanup(); - rc = unpwd__cleanup(&db.config->security_options.unpwd, reload); - if(rc != MOSQ_ERR_SUCCESS) return rc; - - for(int i=0; ilistener_count; i++){ - if(db.config->listeners[i].security_options->unpwd){ - rc = unpwd__cleanup(&db.config->listeners[i].security_options->unpwd, reload); - if(rc != MOSQ_ERR_SUCCESS) return rc; - } - } - rc = psk_file__cleanup(); if(rc != MOSQ_ERR_SUCCESS) return rc; if(db.config->per_listener_settings){ for(int i=0; ilistener_count; i++){ if(db.config->listeners[i].security_options->pid){ - mosquitto_callback_unregister(db.config->listeners[i].security_options->pid, - MOSQ_EVT_BASIC_AUTH, mosquitto_basic_auth_default, NULL); - mosquitto_FREE(db.config->listeners[i].security_options->pid->plugin_name); mosquitto_FREE(db.config->listeners[i].security_options->pid->config.security_options); mosquitto_FREE(db.config->listeners[i].security_options->pid); @@ -131,9 +88,6 @@ int mosquitto_security_cleanup_default(bool reload) } }else{ if(db.config->security_options.pid){ - mosquitto_callback_unregister(db.config->security_options.pid, - MOSQ_EVT_BASIC_AUTH, mosquitto_basic_auth_default, NULL); - mosquitto_FREE(db.config->security_options.pid->plugin_name); mosquitto_FREE(db.config->security_options.pid->config.security_options); mosquitto_FREE(db.config->security_options.pid); @@ -143,182 +97,6 @@ int mosquitto_security_cleanup_default(bool reload) } -static int pwfile__parse(const char *file, struct mosquitto__unpwd **root) -{ - FILE *pwfile; - struct mosquitto__unpwd *unpwd; - char *username, *password; - char *saveptr = NULL; - char *buf; - int buflen = 256; - - buf = mosquitto_malloc((size_t)buflen); - if(buf == NULL){ - log__printf(NULL, MOSQ_LOG_ERR, "Error: Out of memory."); - return MOSQ_ERR_NOMEM; - } - - pwfile = mosquitto_fopen(file, "rt", true); - if(!pwfile){ - log__printf(NULL, MOSQ_LOG_ERR, "Error: Unable to open pwfile \"%s\".", file); - mosquitto_FREE(buf); - return MOSQ_ERR_UNKNOWN; - } - - while(!feof(pwfile)){ - if(mosquitto_fgets(&buf, &buflen, pwfile)){ - if(buf[0] == '#') continue; - if(!strchr(buf, ':')) continue; - - username = strtok_r(buf, ":", &saveptr); - if(username){ - username = mosquitto_trimblanks(username); - if(strlen(username) > 65535){ - log__printf(NULL, MOSQ_LOG_NOTICE, "Warning: Invalid line in password file '%s', username too long.", file); - continue; - } - if(strlen(username) <= 0){ - log__printf(NULL, MOSQ_LOG_NOTICE, "Warning: Empty username in password file '%s', ingoring.", file); - continue; - } - - HASH_FIND(hh, *root, username, strlen(username), unpwd); - if(unpwd){ - log__printf(NULL, MOSQ_LOG_NOTICE, "Error: Duplicate user '%s' in password file '%s', ignoring.", username, file); - continue; - } - - unpwd = mosquitto_calloc(1, sizeof(struct mosquitto__unpwd)); - if(!unpwd){ - fclose(pwfile); - mosquitto_FREE(buf); - return MOSQ_ERR_NOMEM; - } - - unpwd->username = mosquitto_strdup(username); - if(!unpwd->username){ - mosquitto_FREE(unpwd); - mosquitto_FREE(buf); - fclose(pwfile); - return MOSQ_ERR_NOMEM; - } - password = strtok_r(NULL, ":", &saveptr); - if(password){ - password = mosquitto_trimblanks(password); - - if(strlen(password) > 65535){ - log__printf(NULL, MOSQ_LOG_NOTICE, "Warning: Invalid line in password file '%s', password too long.", file); - mosquitto_FREE(unpwd->username); - mosquitto_FREE(unpwd); - continue; - } - - if(mosquitto_pw_new(&unpwd->pw, MOSQ_PW_DEFAULT) - || mosquitto_pw_decode(unpwd->pw, password)){ - - log__printf(NULL, MOSQ_LOG_NOTICE, "Warning: Unable to decode line in password file '%s'.", file); - mosquitto_pw_cleanup(unpwd->pw); - mosquitto_FREE(unpwd->username); - mosquitto_FREE(unpwd); - continue; - } - - HASH_ADD_KEYPTR(hh, *root, unpwd->username, strlen(unpwd->username), unpwd); - }else{ - log__printf(NULL, MOSQ_LOG_NOTICE, "Warning: Invalid line in password file '%s': %s", file, buf); - mosquitto_pw_cleanup(unpwd->pw); - mosquitto_FREE(unpwd->username); - mosquitto_FREE(unpwd); - } - } - } - } - fclose(pwfile); - mosquitto_FREE(buf); - - return MOSQ_ERR_SUCCESS; -} - - -void unpwd__free_item(struct mosquitto__unpwd **unpwd, struct mosquitto__unpwd *item) -{ - mosquitto_FREE(item->username); - mosquitto_pw_cleanup(item->pw); - HASH_DEL(*unpwd, item); - mosquitto_FREE(item); -} - - -static int unpwd__file_parse(struct mosquitto__unpwd **unpwd, const char *password_file) -{ - int rc; - if(!unpwd) return MOSQ_ERR_INVAL; - - if(!password_file) return MOSQ_ERR_SUCCESS; - - rc = pwfile__parse(password_file, unpwd); - - return rc; -} - -static int mosquitto_basic_auth_default(int event, void *event_data, void *userdata) -{ - struct mosquitto_evt_basic_auth *ed = event_data; - struct mosquitto__unpwd *u; - struct mosquitto__unpwd *unpwd_ref; - - UNUSED(event); - UNUSED(userdata); - - if(ed->client->username == NULL){ - return MOSQ_ERR_PLUGIN_IGNORE; - } - - if(db.config->per_listener_settings){ - if(ed->client->bridge) return MOSQ_ERR_SUCCESS; - if(!ed->client->listener) return MOSQ_ERR_INVAL; - unpwd_ref = ed->client->listener->security_options->unpwd; - }else{ - unpwd_ref = db.config->security_options.unpwd; - } - - HASH_FIND(hh, unpwd_ref, ed->client->username, strlen(ed->client->username), u); - if(u){ - if(u->pw){ - if(ed->client->password){ - return mosquitto_pw_verify(u->pw, ed->client->password); - }else{ - return MOSQ_ERR_AUTH; - } - }else{ - return MOSQ_ERR_SUCCESS; - } - } - - return MOSQ_ERR_AUTH; -} - -static int unpwd__cleanup(struct mosquitto__unpwd **root, bool reload) -{ - struct mosquitto__unpwd *u, *tmp = NULL; - - UNUSED(reload); - - if(!root) return MOSQ_ERR_INVAL; - - HASH_ITER(hh, *root, u, tmp){ - HASH_DEL(*root, u); - mosquitto_pw_cleanup(u->pw); - mosquitto_FREE(u->username); - mosquitto_FREE(u); - } - - *root = NULL; - - return MOSQ_ERR_SUCCESS; -} - - #ifdef WITH_TLS static void security__disconnect_auth(struct mosquitto *context) { diff --git a/test/broker/01-plugin-connect-uname-password-denied.pwfile b/test/broker/01-plugin-connect-uname-password-denied.pwfile new file mode 100644 index 00000000..913477da --- /dev/null +++ b/test/broker/01-plugin-connect-uname-password-denied.pwfile @@ -0,0 +1 @@ +user:$6$vZY4TS+/HBxHw38S$vvjVFECzb8dyuu/mruD2QKTfdFn0WmKxbc+1TsdB0L8EdHk3v9JRmfjHd56+VaTnUcSZOZ/hzkdvWCtxlX7AUQ== diff --git a/test/broker/01-plugin-connect-uname-password-denied.py b/test/broker/01-plugin-connect-uname-password-denied.py new file mode 100755 index 00000000..61b55cf5 --- /dev/null +++ b/test/broker/01-plugin-connect-uname-password-denied.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 + +# Test whether a connection is denied if it provides a correct username but +# incorrect password. + +from mosq_test_helper import * + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write(f"plugin {mosq_test.get_build_root()}/plugins/password-file/mosquitto_password_file.so\n") + f.write("plugin_opt_password_file %s/%s\n" % (Path(__file__).resolve().parent, filename.replace('.conf', '.pwfile'))) + f.write("allow_anonymous false\n") + + +def do_test(proto_ver): + port = mosq_test.get_port() + conf_file = os.path.basename(__file__).replace('.py', '.conf') + write_config(conf_file, port) + + rc = 1 + connect_packet = mosq_test.gen_connect("connect-uname-pwd-test", username="user", password="password9", proto_ver=proto_ver) + if proto_ver == 5: + connack_packet = mosq_test.gen_connack(rc=mqtt5_rc.MQTT_RC_NOT_AUTHORIZED, proto_ver=proto_ver, properties=None) + else: + connack_packet = mosq_test.gen_connack(rc=5, proto_ver=proto_ver) + + + 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, port=port) + sock.close() + rc = 0 + + except mosq_test.TestError: + pass + finally: + os.remove(conf_file) + broker.terminate() + if mosq_test.wait_for_subprocess(broker): + print("broker not terminated") + if rc == 0: rc=1 + (stdo, stde) = broker.communicate() + if rc: + print(stde.decode('utf-8')) + print("proto_ver=%d" % (proto_ver)) + exit(rc) + + +do_test(proto_ver=4) +do_test(proto_ver=5) +exit(0) diff --git a/test/broker/Makefile b/test/broker/Makefile index 5a4b9806..912a0708 100644 --- a/test/broker/Makefile +++ b/test/broker/Makefile @@ -43,6 +43,7 @@ msg_sequence_test: ./01-connect-unix-socket.py ./01-connect-windows-line-endings.py ./01-connect-zero-length-id.py + ./01-plugin-connect-uname-password-denied.py 02 : diff --git a/test/broker/test.py b/test/broker/test.py index 98170aa1..5b29db39 100755 --- a/test/broker/test.py +++ b/test/broker/test.py @@ -26,6 +26,7 @@ tests = [ (1, './01-connect-unix-socket.py'), (1, './01-connect-windows-line-endings.py'), (2, './01-connect-zero-length-id.py'), + (1, './01-plugin-connect-uname-password-denied.py'), (1, './02-shared-nolocal.py'), (1, './02-shared-qos0-v5.py'),