diff --git a/ChangeLog.txt b/ChangeLog.txt index a04172e3..1a802369 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -18,6 +18,7 @@ Broker: - Report on what compile time options are enabled. Closes #2193. - Add `--tls-keylog` option which can be used to generate a file that can be used by wireshark to decrypt TLS traffic for debugging purposes. Closes #1818. +- Add support for delayed basic authentication in plugins. Client library: - Add MOSQ_OPT_DISABLE_SOCKETPAIR to allow the disabling of the socketpair diff --git a/include/mosquitto.h b/include/mosquitto.h index 03557639..46237e05 100644 --- a/include/mosquitto.h +++ b/include/mosquitto.h @@ -85,6 +85,7 @@ extern "C" { /* Error values */ enum mosq_err_t { + MOSQ_ERR_AUTH_DELAYED = -5, MOSQ_ERR_AUTH_CONTINUE = -4, MOSQ_ERR_NO_SUBSCRIBERS = -3, MOSQ_ERR_SUB_EXISTS = -2, diff --git a/include/mosquitto_broker.h b/include/mosquitto_broker.h index 631dc34f..6f818a96 100644 --- a/include/mosquitto_broker.h +++ b/include/mosquitto_broker.h @@ -592,4 +592,29 @@ mosq_EXPORT int mosquitto_broker_publish_copy( } #endif +/* Function: mosquitto_complete_basic_auth + * + * Complete a delayed authentication request. + * + * Useful for plugins that subscribe to the MOSQ_EVT_BASIC_AUTH event. If your + * plugin makes authentication requests that are not "instant", in particular + * if they communicate with an external service, then instead of blocking for a + * reply and returning MOSQ_ERR_SUCCESS or MOSQ_ERR_AUTH, the plugin can return + * MOSQ_ERR_AUTH_DELAYED. This means that the plugin is promising to tell the + * broker the authentication result in the future. Once the plugin has an + * answer, it should call `mosquitto_complete_basic_auth()` passing the client + * id and the result. + * + * Result: + * MOSQ_ERR_SUCCESS - the client successfully authenticated + * MOSQ_ERR_AUTH - the client authentication failed + * + * Other error codes can be used if more appropriate, and the client connection + * will still be rejected, e.g. MOSQ_ERR_NOMEM. + * + * The plugin may use extra threads to handle the authentication requests, but + * the call to `mosquitto_complete_basic_auth()` must happen in the main + * mosquitto thread. Using the MOSQ_EVT_TICK event for this is suggested. + */ +void mosquitto_complete_basic_auth(const char *client_id, int result); #endif diff --git a/lib/mosquitto_internal.h b/lib/mosquitto_internal.h index 4c9779b7..432e6c5a 100644 --- a/lib/mosquitto_internal.h +++ b/lib/mosquitto_internal.h @@ -114,6 +114,7 @@ enum mosquitto_client_state { mosq_cs_disused = 19, /* client that has been added to the disused list to be freed */ mosq_cs_authenticating = 20, /* Client has sent CONNECT but is still undergoing extended authentication */ mosq_cs_reauthenticating = 21, /* Client is undergoing reauthentication and shouldn't do anything else until complete */ + mosq_cs_delayed_auth = 22, /* Client is awaiting an authentication result from a plugin */ }; enum mosquitto__protocol { diff --git a/plugins/README.md b/plugins/README.md index 6c63c915..efea0c61 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -26,6 +26,14 @@ disconnect events. It publishes messages to $SYS/broker/connection/client//state for every client that connects to the broker, to indicate the connection state of that client. +## Examples / Deferred authentication +This is an **example** plugin to demonstrate how a plugin can carry out +delayed basic authentication. This method should be used where the plugin +sends an authentication request to an external server so that if there is a +delay in getting a response it does not block the broker. The plugin may spawn +extra threads to handle the authentication requests, but the call to +`mosquitto_complete_basic_auth()` must happen in the main Mosquitto thread. + ## Examples / 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 diff --git a/plugins/examples/CMakeLists.txt b/plugins/examples/CMakeLists.txt index ff51dfba..b75b228b 100644 --- a/plugins/examples/CMakeLists.txt +++ b/plugins/examples/CMakeLists.txt @@ -5,4 +5,5 @@ endif() add_subdirectory(auth-by-ip) add_subdirectory(client-properties) add_subdirectory(connection-state) +add_subdirectory(delayed-auth) add_subdirectory(payload-modification) diff --git a/plugins/examples/Makefile b/plugins/examples/Makefile index 9fd0e665..0a007958 100644 --- a/plugins/examples/Makefile +++ b/plugins/examples/Makefile @@ -3,6 +3,7 @@ DIRS= \ auth-by-ip \ client-properties \ connection-state \ + delayed-auth \ message-timestamp \ payload-modification diff --git a/plugins/examples/delayed-auth/CMakeLists.txt b/plugins/examples/delayed-auth/CMakeLists.txt new file mode 100644 index 00000000..9f96436c --- /dev/null +++ b/plugins/examples/delayed-auth/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(mosquitto_delayed_auth SHARED + mosquitto_delayed_auth.c +) + +target_include_directories(mosquitto_delayed_auth PRIVATE + "${STDBOOL_H_PATH}" + "${STDINT_H_PATH}" + "${mosquitto_SOURCE_DIR}" + "${mosquitto_SOURCE_DIR}/include" +) + +set_target_properties(mosquitto_delayed_auth PROPERTIES + PREFIX "" + POSITION_INDEPENDENT_CODE 1 +) + +# Don't install, these are example plugins only. +#install(TARGETS mosquitto_delayed_auth RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") diff --git a/plugins/examples/delayed-auth/Makefile b/plugins/examples/delayed-auth/Makefile new file mode 100644 index 00000000..f038e066 --- /dev/null +++ b/plugins/examples/delayed-auth/Makefile @@ -0,0 +1,28 @@ +include ../../../config.mk + +.PHONY : all binary check clean reallyclean test install uninstall + +PLUGIN_NAME=mosquitto_delayed_auth +PLUGIN_CFLAGS+=-I../../../include -I../../../ + +all : binary + +binary : ${PLUGIN_NAME}.so + +${PLUGIN_NAME}.so : ${PLUGIN_NAME}.c + $(CROSS_COMPILE)$(CC) $(PLUGIN_CPPFLAGS) $(PLUGIN_CFLAGS) $(PLUGIN_LDFLAGS) -fPIC -shared $< -o $@ + +reallyclean : clean +clean: + -rm -f *.o ${PLUGIN_NAME}.so *.gcda *.gcno + +check: test +test: + +install: ${PLUGIN_NAME}.so + # Don't install, these are examples only. + #$(INSTALL) -d "${DESTDIR}$(libdir)" + #$(INSTALL) ${STRIP_OPTS} ${PLUGIN_NAME}.so "${DESTDIR}${libdir}/${PLUGIN_NAME}.so" + +uninstall : + -rm -f "${DESTDIR}${libdir}/${PLUGIN_NAME}.so" diff --git a/plugins/examples/delayed-auth/mosquitto_delayed_auth.c b/plugins/examples/delayed-auth/mosquitto_delayed_auth.c new file mode 100644 index 00000000..92c26265 --- /dev/null +++ b/plugins/examples/delayed-auth/mosquitto_delayed_auth.c @@ -0,0 +1,176 @@ +/* +Copyright (c) 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 EDL-1.0 + +Contributors: + Roger Light - initial implementation and documentation. +*/ + +/* + * This is an example plugin showing how to carry out delayed authentication. + * The "authentication" in this example makes no checks whatsoever, but delays + * the response by 5 seconds, and randomly chooses whether it should succeed. + * + * Compile with: + * gcc -I -fPIC -shared mosquitto_delayed_auth.c -o mosquitto_delayed_auth.so + * + * Use in config with: + * + * plugin /path/to/mosquitto_delayed_auth.so + * + * Note that this only works on Mosquitto 2.0 or later. + */ + + +#include +#include +#include +#include +#include +#include + +#include "mosquitto_broker.h" +#include "mosquitto_plugin.h" +#include "mosquitto.h" +#include "mqtt_protocol.h" + +#ifndef UNUSED +# define UNUSED(A) (void)(A) +#endif + +struct client_list{ + UT_hash_handle hh; + char *id; + time_t request_time; +}; + +static mosquitto_plugin_id_t *mosq_pid = NULL; +static struct client_list *clients = NULL; +static time_t last_check = 0; + +bool authentication_check(struct client_list *client, time_t now) +{ + time_t secs; + + secs = now - client->request_time; + + return secs > 5 ? true : false; +} + +static int basic_auth_callback(int event, void *event_data, void *userdata) +{ + struct mosquitto_evt_basic_auth *ed = event_data; + static struct client_list *client; + const char *id; + + UNUSED(event); + UNUSED(userdata); + + id = mosquitto_client_id(ed->client); + + HASH_FIND(hh, clients, id, strlen(id), client); + if(client){ + client->request_time = time(NULL); + }else{ + client = mosquitto_malloc(sizeof(struct client_list)); + if(client == NULL){ + return MOSQ_ERR_NOMEM; + } + + client->id = mosquitto_strdup(id); + if(client->id == NULL){ + mosquitto_free(client); + return MOSQ_ERR_NOMEM; + } + client->request_time = time(NULL); + HASH_ADD_KEYPTR(hh, clients, client->id, strlen(client->id), client); + + mosquitto_log_printf(MOSQ_LOG_DEBUG, "Starting auth for %s at %d", client->id, time(NULL)); + } + + return MOSQ_ERR_AUTH_DELAYED; +} + + +static int tick_callback(int event, void *event_data, void *userdata) +{ + struct client_list *client, *client_tmp; + time_t now; + long r; + + UNUSED(event); + UNUSED(event_data); + UNUSED(userdata); + + now = time(NULL); + if(now > last_check){ + HASH_ITER(hh, clients, client, client_tmp){ + if(authentication_check(client, now)){ + /* Deny access 1/4 of the time, yes it's biased number generation. */ + r = random() % 1000; + if(r > 740){ + mosquitto_complete_basic_auth(client->id, MOSQ_ERR_AUTH); + }else{ + mosquitto_complete_basic_auth(client->id, MOSQ_ERR_SUCCESS); + } + mosquitto_log_printf(MOSQ_LOG_DEBUG, "Completing auth for %s at %d", client->id, now); + HASH_DELETE(hh, clients, client); + mosquitto_free(client->id); + mosquitto_free(client); + } + } + last_check = now; + } + return MOSQ_ERR_SUCCESS; +} + + +int mosquitto_plugin_version(int supported_version_count, const int *supported_versions) +{ + int i; + + for(i=0; isock = INVALID_SOCKET; context->last_msg_in = db.now_s; - context->next_msg_out = db.now_s + 60; - context->keepalive = 60; /* Default to 60s */ + context->next_msg_out = db.now_s + 20; + context->keepalive = 20; /* Default to 20s */ context->clean_start = true; context->id = NULL; context->last_mid = 0; @@ -294,6 +294,11 @@ void context__remove_from_by_id(struct mosquitto *context) struct mosquitto *context_found; if(context->removed_from_by_id == false && context->id){ + HASH_FIND(hh_id, db.contexts_by_id_delayed_auth, context->id, strlen(context->id), context_found); + if(context_found){ + HASH_DELETE(hh_id, db.contexts_by_id_delayed_auth, context_found); + } + HASH_FIND(hh_id, db.contexts_by_id, context->id, strlen(context->id), context_found); if(context_found){ HASH_DELETE(hh_id, db.contexts_by_id, context_found); diff --git a/src/handle_connect.c b/src/handle_connect.c index ecd7ade8..eeddf7f3 100644 --- a/src/handle_connect.c +++ b/src/handle_connect.c @@ -406,6 +406,7 @@ int handle__connect(struct mosquitto *context) uint8_t protocol_version; uint8_t connect_flags; char *client_id = NULL; + struct mosquitto *found_context; struct mosquitto_message_all *will_struct = NULL; uint8_t will, will_retain, will_qos, clean_start; uint8_t username_flag, password_flag; @@ -629,6 +630,13 @@ int handle__connect(struct mosquitto *context) } } + /* Check for an existing delayed auth check, reject if present */ + HASH_FIND(hh_id, db.contexts_by_id_delayed_auth, client_id, strlen(client_id), found_context); + if(found_context){ + rc = MOSQ_ERR_UNKNOWN; + goto handle_connect_error; + } + if(will){ rc = will__read(context, client_id, &will_struct, will_qos, will_retain); if(rc) goto handle_connect_error; @@ -889,7 +897,7 @@ int handle__connect(struct mosquitto *context) #endif { rc = mosquitto_unpwd_check(context); - if(rc != MOSQ_ERR_SUCCESS){ + if(rc != MOSQ_ERR_SUCCESS && rc != MOSQ_ERR_AUTH_DELAYED){ /* We must have context->id == NULL here so we don't later try and * remove the client from the by_id hash table */ mosquitto__free(context->id); @@ -898,6 +906,11 @@ int handle__connect(struct mosquitto *context) switch(rc){ case MOSQ_ERR_SUCCESS: break; + case MOSQ_ERR_AUTH_DELAYED: + mosquitto__set_state(context, mosq_cs_delayed_auth); + HASH_ADD_KEYPTR(hh_id, db.contexts_by_id_delayed_auth, context->id, strlen(context->id), context); + return MOSQ_ERR_SUCCESS; + break; case MOSQ_ERR_AUTH: if(context->protocol == mosq_p_mqtt5){ send__connack(context, 0, MQTT_RC_NOT_AUTHORIZED, NULL); diff --git a/src/linker-macosx.syms b/src/linker-macosx.syms index bbd9136a..dddda52c 100644 --- a/src/linker-macosx.syms +++ b/src/linker-macosx.syms @@ -12,6 +12,7 @@ _mosquitto_client_protocol _mosquitto_client_protocol_version _mosquitto_client_sub_count _mosquitto_client_username +_mosquitto_complete_basic_auth _mosquitto_free _mosquitto_kick_client_by_clientid _mosquitto_kick_client_by_username diff --git a/src/linker.syms b/src/linker.syms index 753d00aa..5792bc06 100644 --- a/src/linker.syms +++ b/src/linker.syms @@ -13,6 +13,7 @@ mosquitto_client_protocol_version; mosquitto_client_sub_count; mosquitto_client_username; + mosquitto_complete_basic_auth; mosquitto_free; mosquitto_kick_client_by_clientid; mosquitto_kick_client_by_username; diff --git a/src/mosquitto_broker_internal.h b/src/mosquitto_broker_internal.h index f5067288..4a2bc939 100644 --- a/src/mosquitto_broker_internal.h +++ b/src/mosquitto_broker_internal.h @@ -445,6 +445,7 @@ struct mosquitto_db{ struct mosquitto__retainhier *retains; struct mosquitto *contexts_by_id; struct mosquitto *contexts_by_sock; + struct mosquitto *contexts_by_id_delayed_auth; struct mosquitto *contexts_for_free; #ifdef WITH_BRIDGE struct mosquitto **bridges; diff --git a/src/plugin.c b/src/plugin.c index d45ed78c..92f69ac1 100644 --- a/src/plugin.c +++ b/src/plugin.c @@ -27,6 +27,7 @@ Contributors: #include "util_mosq.h" #include "utlist.h" #include "lib_load.h" +#include "will_mosq.h" static bool check_callback_exists(struct mosquitto__callback *cb_base, MOSQ_FUNC_generic_callback cb_func) @@ -345,3 +346,28 @@ int mosquitto_callback_unregister( return remove_callback(cb_base, cb_func); } + +void mosquitto_complete_basic_auth(const char *client_id, int result) +{ + struct mosquitto *context; + + if(client_id == NULL) return; + + HASH_FIND(hh_id, db.contexts_by_id_delayed_auth, client_id, strlen(client_id), context); + if(context){ + HASH_DELETE(hh_id, db.contexts_by_id_delayed_auth, context); + if(result == MOSQ_ERR_SUCCESS){ + connect__on_authorised(context, NULL, 0); + }else{ + if(context->protocol == mosq_p_mqtt5){ + send__connack(context, 0, MQTT_RC_NOT_AUTHORIZED, NULL); + }else{ + send__connack(context, 0, CONNACK_REFUSED_NOT_AUTHORIZED, NULL); + } + context->clean_start = true; + context->session_expiry_interval = 0; + will__clear(context); + do_disconnect(context, MOSQ_ERR_AUTH); + } + } +} diff --git a/test/broker/09-plugin-delayed-auth.py b/test/broker/09-plugin-delayed-auth.py new file mode 100755 index 00000000..95c8bf45 --- /dev/null +++ b/test/broker/09-plugin-delayed-auth.py @@ -0,0 +1,49 @@ +#!/usr/bin/env python3 + +# Test whether message parameters are passed to the plugin acl check function. + +from mosq_test_helper import * + +def write_config(filename, port): + with open(filename, 'w') as f: + f.write("listener %d\n" % (port)) + f.write("auth_plugin c/auth_plugin_delayed.so\n") + 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("delayed-auth-test", keepalive=42, username="delayed-username", password="good", proto_ver=proto_ver) + connack_packet = mosq_test.gen_connack(rc=0, proto_ver=proto_ver) + + connect_packet2 = mosq_test.gen_connect("delayed-auth-test", keepalive=42, username="delayed-username", password="bad", proto_ver=proto_ver) + if proto_ver == 5: + connack_packet2 = mosq_test.gen_connack(rc=mqtt5_rc.MQTT_RC_NOT_AUTHORIZED, proto_ver=proto_ver, property_helper=False) + else: + connack_packet2 = 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, timeout=20, port=port) + sock.close() + sock = mosq_test.do_client_connect(connect_packet2, connack_packet2, timeout=20, port=port) + 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) + +do_test(4) +do_test(5) diff --git a/test/broker/Makefile b/test/broker/Makefile index fad0f00b..0cce1a0a 100644 --- a/test/broker/Makefile +++ b/test/broker/Makefile @@ -194,6 +194,7 @@ endif ./09-plugin-auth-unpwd-success.py ./09-plugin-auth-v2-unpwd-fail.py ./09-plugin-auth-v2-unpwd-success.py + ./09-plugin-delayed-auth.py ./09-plugin-publish.py ./09-pwfile-parse-invalid.py diff --git a/test/broker/c/Makefile b/test/broker/c/Makefile index 6a561ba1..f4e980be 100644 --- a/test/broker/c/Makefile +++ b/test/broker/c/Makefile @@ -3,19 +3,20 @@ CFLAGS=-I../../../include -Wall -Werror PLUGIN_SRC = \ - auth_plugin_v4.c \ - auth_plugin_v5.c \ - auth_plugin_v5_handle_message.c \ - auth_plugin_pwd.c \ auth_plugin_acl.c \ auth_plugin_acl_sub_denied.c \ - auth_plugin_v2.c \ auth_plugin_context_params.c \ - auth_plugin_msg_params.c \ + auth_plugin_delayed.c \ auth_plugin_extended_multiple.c \ auth_plugin_extended_single.c \ auth_plugin_extended_single2.c \ + auth_plugin_msg_params.c \ auth_plugin_publish.c \ + auth_plugin_pwd.c \ + auth_plugin_v2.c \ + auth_plugin_v4.c \ + auth_plugin_v5.c \ + auth_plugin_v5_handle_message.c \ plugin_control.c PLUGINS = ${PLUGIN_SRC:.c=.so} diff --git a/test/broker/c/auth_plugin_delayed.c b/test/broker/c/auth_plugin_delayed.c new file mode 100644 index 00000000..0a81dfea --- /dev/null +++ b/test/broker/c/auth_plugin_delayed.c @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include +#include + +static int tick_callback(int event, void *event_data, void *user_data); +static int unpwd_check_callback(int event, void *event_data, void *user_data); + +static mosquitto_plugin_id_t *plg_id; + +static char *username = NULL; +static char *password = NULL; +static char *client_id = NULL; +static int auth_delay = -1; + +int mosquitto_plugin_version(int supported_version_count, const int *supported_versions) +{ + int i; + + for(i=0; i 0){ + auth_delay--; + } + + return MOSQ_ERR_SUCCESS; +} + +static int unpwd_check_callback(int event, void *event_data, void *user_data) +{ + struct mosquitto_evt_basic_auth *ed = event_data; + + free(username); + free(password); + free(client_id); + + if(ed->username){ + username = strdup(ed->username); + } + if(ed->password){ + password = strdup(ed->password); + } + client_id = strdup(mosquitto_client_id(ed->client)); + /* Delay for arbitrary 10 ticks */ + auth_delay = 10; + + return MOSQ_ERR_AUTH_DELAYED; +} diff --git a/test/broker/test.py b/test/broker/test.py index a535d2f3..667899fb 100755 --- a/test/broker/test.py +++ b/test/broker/test.py @@ -167,6 +167,7 @@ tests = [ (1, './09-plugin-auth-unpwd-success.py'), (1, './09-plugin-auth-v2-unpwd-fail.py'), (1, './09-plugin-auth-v2-unpwd-success.py'), + (1, './09-plugin-delayed-auth.py'), (1, './09-plugin-publish.py'), (1, './09-pwfile-parse-invalid.py'), diff --git a/test/mosq_test.py b/test/mosq_test.py index 91134ff4..c468e2c6 100644 --- a/test/mosq_test.py +++ b/test/mosq_test.py @@ -274,8 +274,15 @@ def to_string(packet): return s elif cmd == 0x20: # CONNACK - (cmd, rl, resv, rc) = struct.unpack('!BBBB', packet) - return "CONNACK, rl="+str(rl)+", res="+str(resv)+", rc="+str(rc) + if len(packet) == 4: + (cmd, rl, resv, rc) = struct.unpack('!BBBB', packet) + return "CONNACK, rl="+str(rl)+", res="+str(resv)+", rc="+str(rc) + elif len(packet) == 5: + (cmd, rl, flags, reason_code, proplen) = struct.unpack('!BBBBB', packet) + return "CONNACK, rl="+str(rl)+", flags="+str(flags)+", rc="+str(reason_code)+", proplen="+str(proplen) + else: + return "CONNACK, (not decoded)" + elif cmd == 0x30: # PUBLISH dup = (packet0 & 0x08)>>3