diff --git a/.github/lock.yml b/.github/lock.yml deleted file mode 100644 index 916ac1f7..00000000 --- a/.github/lock.yml +++ /dev/null @@ -1,35 +0,0 @@ -# Configuration for Lock Threads - https://github.com/dessant/lock-threads - -# Number of days of inactivity before a closed issue or pull request is locked -daysUntilLock: 90 - -# Skip issues and pull requests created before a given timestamp. Timestamp must -# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable -skipCreatedBefore: false - -# Issues and pull requests with these labels will be ignored. Set to `[]` to disable -exemptLabels: [] - -# Label to add before locking, such as `outdated`. Set to `false` to disable -lockLabel: false - -# Comment to post before locking. Set to `false` to disable -lockComment: false - -# Assign `resolved` as the reason for locking. Set to `false` to disable -setLockReason: true - -# Limit to only `issues` or `pulls` -only: issues - -# Optionally, specify configuration settings just for `issues` or `pulls` -# issues: -# exemptLabels: -# - help-wanted -# lockLabel: outdated - -# pulls: -# daysUntilLock: 30 - -# Repository to extend settings from -# _extends: repo diff --git a/.github/workflows/coverity-scan-develop.yml b/.github/workflows/coverity-scan-develop.yml new file mode 100644 index 00000000..0ef60c83 --- /dev/null +++ b/.github/workflows/coverity-scan-develop.yml @@ -0,0 +1,26 @@ +name: Coverity Scan develop branch on a weekly basis + +on: + workflow_dispatch: + schedule: + - cron: "7 3 * * 0" + +jobs: + coverity: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: develop + + - name: Dependencies + run: sudo apt-get install -y libcjson-dev libsqlite3-dev libssl-dev uthash-dev + + - uses: vapier/coverity-scan-action@v1 + with: + build_language: 'cxx' + project: "eclipse/mosquitto" + token: ${{ secrets.COVERITY_SCAN_TOKEN }} + email: ${{ secrets.COVERITY_SCAN_EMAIL }} + command: "make binary" + diff --git a/.github/workflows/coverity-scan-fixes.yml b/.github/workflows/coverity-scan-fixes.yml new file mode 100644 index 00000000..2f11d013 --- /dev/null +++ b/.github/workflows/coverity-scan-fixes.yml @@ -0,0 +1,26 @@ +name: Coverity Scan fixes branch on a weekly basis + +on: + workflow_dispatch: + schedule: + - cron: "7 3 * * 3" + +jobs: + coverity: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + ref: fixes + + - name: Dependencies + run: sudo apt-get install -y libcjson-dev libsqlite3-dev libssl-dev uthash-dev + + - uses: vapier/coverity-scan-action@v1 + with: + build_language: 'cxx' + project: "eclipse/mosquitto" + token: ${{ secrets.COVERITY_SCAN_TOKEN }} + email: ${{ secrets.COVERITY_SCAN_EMAIL }} + command: "make binary" + diff --git a/.github/workflows/lock.yml b/.github/workflows/lock.yml new file mode 100644 index 00000000..920a0259 --- /dev/null +++ b/.github/workflows/lock.yml @@ -0,0 +1,21 @@ +name: 'Lock Threads' + +on: + schedule: + - cron: '0 0 * * 0' + workflow_dispatch: + +permissions: + issues: write + pull-requests: write + +concurrency: + group: lock + +jobs: + action: + runs-on: ubuntu-latest + steps: + - uses: dessant/lock-threads@v3 + with: + issue-inactive-days: '90' diff --git a/CMakeLists.txt b/CMakeLists.txt index 1e38a1a3..07b24642 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -13,7 +13,7 @@ project(mosquitto LANGUAGES C CXX ) -list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/") +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/") add_definitions (-DCMAKE -DVERSION=\"${VERSION}\") diff --git a/ChangeLog.txt b/ChangeLog.txt index a69645ae..6ce25841 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -161,6 +161,73 @@ Build: - Add an CMake option `WITH_LTO` to enable/disable link time optimization. +2.0.15 - 2022-08-16 +=================== + +Security: +- Deleting the group configured as the anonymous group in the Dynamic Security + plugin, would leave a dangling pointer that could lead to a single crash. + This is considered a minor issue - only administrative users should have + access to dynsec, the impact on availability is one-off, and there is no + associated loss of data. It is now forbidden to delete the group configured + as the anonymous group. + +Broker: +- Fix memory leak when a plugin modifies the topic of a message in + MOSQ_EVT_MESSAGE. +- Fix bridge `restart_timeout` not being honoured. +- Fix potential memory leaks if a plugin modifies the message in the + MOSQ_EVT_MESSAGE event. +- Fix unused flags in CONNECT command being forced to be 0, which is not + required for MQTT v3.1. Closes #2522. +- Improve documentation of `persistent_client_expiration` option. + Closes #2404. +- Add clients to session expiry check list when restarting and reloading from + persistence. Closes #2546. +- Fix bridges not sending failure notification messages to the local broker if + the remote bridge connection fails. Closes #2467. Closes #1488. +- Fix some PUBLISH messages not being counted in $SYS stats. Closes #2448. +- Fix incorrect return code being sent in DISCONNECT when a client session is + taken over. Closes #2607. +- Fix confusing "out of memory" error when a client is kicked in the dynamic + security plugin. Closes #2525. +- Fix confusing error message when dynamic security config file was a + directory. Closes #2520. +- Fix bridge queued messages not being persisted when local_cleansession is + set to false and cleansession is set to true. Closes #2604. +- Dynamic security: Fix modifyClient and modifyGroup commands to not modify + the client/group if a new group/client being added is not valid. + Closes #2598. +- Dynamic security: Fix the plugin being able to be loaded twice. Currently + only a single plugin can interact with a unique $CONTROL topic. Using + multiple instances of the plugin would produce duplicate entries in the + config file. Closes #2601. Closes #2470. +- Fix case where expired messages were causing queued messages not to be + delivered. Closes #2609. +- Fix websockets not passing on the X-Forwarded-For header. + +Client library: +- Fix threads library detection on Windows under cmake. Bumps the minimum + cmake version to 3.1, which is still ancient. +- Fix use of `MOSQ_OPT_TLS_ENGINE` being unable to be used due to the openssl + ctx not being initialised until starting to connect. Closes #2537. +- Fix incorrect use of SSL_connect. Closes #2594. +- Don't set SIGPIPE to ignore, use MSG_NOSIGNAL instead. Closes #2564. +- Add documentation of struct mosquitto_message to header. Closes #2561. +- Fix documentation omission around mosquitto_reinitialise. Closes #2489. +- Fix use of MOSQ_OPT_SSL_CTX when used in conjunction with + MOSQ_OPT_SSL_CTX_DEFAULTS. Closes #2463. +- Fix failure to close thread in some situations. Closes #2545. + +Clients: +- Fix mosquitto_pub incorrectly reusing topic aliases when reconnecting. + Closes #2494. + +Apps: +- Fix `-o` not working in `mosquitto_ctrl`, and typo in related documentation. + Closes #2471. + + 2.0.14 - 2021-11-17 =================== @@ -188,6 +255,9 @@ Broker: - Fix broker sending duplicate CONNACK on failed MQTT v5 reauthentication. Closes #2339. - Fix mosquitto_plugin.h not including mosquitto_broker.h. Closes #2350. +- Fix unlimited message quota not being properly checked for incoming + messages. Closes #2593. +- Fixed build for openssl compiled with OPENSSL_NO_ENGINE. Closes #2589. Client library: - Initialise sockpairR/W to invalid in `mosquitto_reinitialise()` to avoid diff --git a/apps/db_dump/stubs.c b/apps/db_dump/stubs.c index 93273e1b..d9b5592d 100644 --- a/apps/db_dump/stubs.c +++ b/apps/db_dump/stubs.c @@ -152,3 +152,10 @@ void db__msg_add_to_queued_stats(struct mosquitto_msg_data *msg_data, struct mos UNUSED(msg_data); UNUSED(msg); } + +int session_expiry__add_from_persistence(struct mosquitto *context, time_t expiry_time) +{ + UNUSED(context); + UNUSED(expiry_time); + return 0; +} diff --git a/apps/mosquitto_ctrl/dynsec.c b/apps/mosquitto_ctrl/dynsec.c index 5c6854a7..df408bd4 100644 --- a/apps/mosquitto_ctrl/dynsec.c +++ b/apps/mosquitto_ctrl/dynsec.c @@ -22,6 +22,10 @@ Contributors: #include #include +#ifndef WIN32 +# include +#endif + #include "mosquitto_ctrl.h" #include "mosquitto.h" #include "base64_mosq.h" diff --git a/apps/mosquitto_ctrl/dynsec_role.c b/apps/mosquitto_ctrl/dynsec_role.c index 34a13d80..cae3a6fb 100644 --- a/apps/mosquitto_ctrl/dynsec_role.c +++ b/apps/mosquitto_ctrl/dynsec_role.c @@ -22,6 +22,10 @@ Contributors: #include #include +#ifndef WIN32 +# include +#endif + #include "mosquitto.h" #include "mosquitto_ctrl.h" #include "password_mosq.h" diff --git a/apps/mosquitto_ctrl/example.c b/apps/mosquitto_ctrl/example.c index 7693f296..67f0272f 100644 --- a/apps/mosquitto_ctrl/example.c +++ b/apps/mosquitto_ctrl/example.c @@ -22,6 +22,10 @@ Contributors: #include #include +#ifndef WIN32 +# include +#endif + #include "mosquitto_ctrl.h" void ctrl_help(void) diff --git a/apps/mosquitto_ctrl/mosquitto_ctrl.c b/apps/mosquitto_ctrl/mosquitto_ctrl.c index fbd90298..d998859b 100644 --- a/apps/mosquitto_ctrl/mosquitto_ctrl.c +++ b/apps/mosquitto_ctrl/mosquitto_ctrl.c @@ -24,6 +24,10 @@ Contributors: #include #include +#ifndef WIN32 +# include +#endif + #include "lib_load.h" #include "mosquitto.h" #include "mosquitto_ctrl.h" diff --git a/apps/mosquitto_ctrl/options.c b/apps/mosquitto_ctrl/options.c index b53dc580..f606089a 100644 --- a/apps/mosquitto_ctrl/options.c +++ b/apps/mosquitto_ctrl/options.c @@ -90,13 +90,14 @@ int ctrl_config_parse(struct mosq_config *cfg, int *argc, char **argv[]) init_config(cfg); - rc = client_config_load(cfg); - if(rc) return rc; - /* Deal with real argc/argv */ rc = client_config_line_proc(cfg, argc, argv); if(rc) return rc; + /* Load options from config file - this must be after `-o` has been processed */ + 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"); @@ -541,7 +542,7 @@ int client_config_load(struct mosq_config *cfg) fclose(fptr); return 1; } - while(fgets(line, 1024, fptr)){ + while(fgets(line, sizeof(line), fptr)){ if(line[0] == '#') continue; /* Comments */ while(line[strlen(line)-1] == 10 || line[strlen(line)-1] == 13){ diff --git a/client/pub_client.c b/client/pub_client.c index de0a1fc4..d8125d68 100644 --- a/client/pub_client.c +++ b/client/pub_client.c @@ -135,6 +135,7 @@ void my_connect_callback(struct mosquitto *mosq, void *obj, int result, int flag connack_result = result; if(!result){ + first_publish = true; switch(cfg.pub_mode){ case MSGMODE_CMD: case MSGMODE_FILE: diff --git a/config.mk b/config.mk index 345474ae..fd83cfe0 100644 --- a/config.mk +++ b/config.mk @@ -272,6 +272,7 @@ ifeq ($(WITH_THREADING),yes) BROKER_CFLAGS:=$(BROKER_CFLAGS) -pthread BROKER_LDFLAGS:=$(BROKER_LDFLAGS) -pthread LIB_CFLAGS:=$(LIB_CFLAGS) -pthread + LIB_LDFLAGS:=$(LIB_LDFLAGS) -pthread LIB_CPPFLAGS:=$(LIB_CPPFLAGS) -DWITH_THREADING LIB_LDFLAGS:=$(LIB_LDFLAGS) -pthread CLIENT_CFLAGS:=$(CLIENT_CFLAGS) -pthread diff --git a/docker/1.5-openssl/Dockerfile b/docker/1.5-openssl/Dockerfile index c4e0a3f7..2d258cbd 100644 --- a/docker/1.5-openssl/Dockerfile +++ b/docker/1.5-openssl/Dockerfile @@ -1,4 +1,4 @@ -FROM alpine:3.12 +FROM alpine:3.14 LABEL maintainer="Roger Light " \ description="Eclipse Mosquitto MQTT Broker" @@ -40,9 +40,8 @@ RUN set -x && \ export GNUPGHOME="$(mktemp -d)" && \ found=''; \ for server in \ - ha.pool.sks-keyservers.net \ + htps://keys.openpgp.org \ hkp://keyserver.ubuntu.com:80 \ - hkp://p80.pool.sks-keyservers.net:80 \ pgp.mit.edu \ ; do \ echo "Fetching GPG key $GPG_KEYS from $server"; \ diff --git a/docker/1.5/Dockerfile b/docker/1.5/Dockerfile index 00cfd691..45c3c162 100644 --- a/docker/1.5/Dockerfile +++ b/docker/1.5/Dockerfile @@ -42,9 +42,8 @@ RUN set -x && \ export GNUPGHOME="$(mktemp -d)" && \ found=''; \ for server in \ - ha.pool.sks-keyservers.net \ + hkps://keys.openpgp.org \ hkp://keyserver.ubuntu.com:80 \ - hkp://p80.pool.sks-keyservers.net:80 \ pgp.mit.edu \ ; do \ echo "Fetching GPG key $GPG_KEYS from $server"; \ @@ -65,7 +64,7 @@ RUN set -x && \ WITH_SHARED_LIBRARIES=yes \ WITH_SRV=no \ WITH_STRIP=yes \ - WITH_TLS_PSK=no \ + WITH_TLS_PSK=no \ WITH_WEBSOCKETS=yes \ prefix=/usr \ binary && \ @@ -82,8 +81,8 @@ RUN set -x && \ chown -R mosquitto:mosquitto /mosquitto && \ apk --no-cache add \ ca-certificates \ - libressl \ - libuuid && \ + libressl \ + libuuid && \ apk del build-deps && \ rm -rf /build diff --git a/docker/1.6-openssl/Dockerfile b/docker/1.6-openssl/Dockerfile index 13769d51..025d85f1 100644 --- a/docker/1.6-openssl/Dockerfile +++ b/docker/1.6-openssl/Dockerfile @@ -45,9 +45,8 @@ RUN set -x && \ export GNUPGHOME="$(mktemp -d)" && \ found=''; \ for server in \ - ha.pool.sks-keyservers.net \ + hkps://keys.openpgp.org \ hkp://keyserver.ubuntu.com:80 \ - hkp://p80.pool.sks-keyservers.net:80 \ pgp.mit.edu \ ; do \ echo "Fetching GPG key $GPG_KEYS from $server"; \ diff --git a/docker/1.6/Dockerfile b/docker/1.6/Dockerfile index 882da193..75b8cf42 100644 --- a/docker/1.6/Dockerfile +++ b/docker/1.6/Dockerfile @@ -45,9 +45,8 @@ RUN set -x && \ export GNUPGHOME="$(mktemp -d)" && \ found=''; \ for server in \ - ha.pool.sks-keyservers.net \ + hkps://keys.openpgp.org \ hkp://keyserver.ubuntu.com:80 \ - hkp://p80.pool.sks-keyservers.net:80 \ pgp.mit.edu \ ; do \ echo "Fetching GPG key $GPG_KEYS from $server"; \ diff --git a/docker/2.0-openssl/Dockerfile b/docker/2.0-openssl/Dockerfile index 44f09b2d..d0eb1ebb 100644 --- a/docker/2.0-openssl/Dockerfile +++ b/docker/2.0-openssl/Dockerfile @@ -1,10 +1,10 @@ -FROM alpine:3.14 +FROM alpine:3.16 LABEL maintainer="Roger Light " \ description="Eclipse Mosquitto MQTT Broker" -ENV VERSION=2.0.14 \ - DOWNLOAD_SHA256=d0dde8fdb12caf6e2426b4f28081919a2fce3448773bdb8af0d3cd5fe5776925 \ +ENV VERSION=2.0.15 \ + DOWNLOAD_SHA256=4735b1d32e3f91c7a8896741d88a3022e89730a1ee897946decfa0df27039ac6 \ GPG_KEYS=A0D6EEA1DCAE49A635A3B2F0779B22DFB3E717B7 \ LWS_VERSION=4.2.1 \ LWS_SHA256=842da21f73ccba2be59e680de10a8cce7928313048750eb6ad73b6fa50763c51 @@ -46,9 +46,8 @@ RUN set -x && \ export GNUPGHOME="$(mktemp -d)" && \ found=''; \ for server in \ - ha.pool.sks-keyservers.net \ + hkps://keys.openpgp.org \ hkp://keyserver.ubuntu.com:80 \ - hkp://p80.pool.sks-keyservers.net:80 \ pgp.mit.edu \ ; do \ echo "Fetching GPG key $GPG_KEYS from $server"; \ diff --git a/docker/2.0/Dockerfile b/docker/2.0/Dockerfile index 3df68360..a1056a4c 100644 --- a/docker/2.0/Dockerfile +++ b/docker/2.0/Dockerfile @@ -1,10 +1,10 @@ -FROM alpine:3.14 +FROM alpine:3.16 LABEL maintainer="Roger Light " \ description="Eclipse Mosquitto MQTT Broker" -ENV VERSION=2.0.14 \ - DOWNLOAD_SHA256=d0dde8fdb12caf6e2426b4f28081919a2fce3448773bdb8af0d3cd5fe5776925 \ +ENV VERSION=2.0.15 \ + DOWNLOAD_SHA256=4735b1d32e3f91c7a8896741d88a3022e89730a1ee897946decfa0df27039ac6 \ GPG_KEYS=A0D6EEA1DCAE49A635A3B2F0779B22DFB3E717B7 \ LWS_VERSION=4.2.1 \ LWS_SHA256=842da21f73ccba2be59e680de10a8cce7928313048750eb6ad73b6fa50763c51 @@ -46,9 +46,8 @@ RUN set -x && \ export GNUPGHOME="$(mktemp -d)" && \ found=''; \ for server in \ - ha.pool.sks-keyservers.net \ + hkps://keys.openpgp.org \ hkp://keyserver.ubuntu.com:80 \ - hkp://p80.pool.sks-keyservers.net:80 \ pgp.mit.edu \ ; do \ echo "Fetching GPG key $GPG_KEYS from $server"; \ diff --git a/docker/generic/Dockerfile b/docker/generic/Dockerfile index a855bd7c..656f8a15 100644 --- a/docker/generic/Dockerfile +++ b/docker/generic/Dockerfile @@ -63,9 +63,8 @@ RUN set -x && \ export GNUPGHOME="$(mktemp -d)" && \ found=''; \ for server in \ - ha.pool.sks-keyservers.net \ + hkps://keys.openpgp.org \ hkp://keyserver.ubuntu.com:80 \ - hkp://p80.pool.sks-keyservers.net:80 \ pgp.mit.edu \ ; do \ echo "Fetching GPG key $GPG_KEYS from $server"; \ diff --git a/include/mosquitto.h b/include/mosquitto.h index 161cb7e3..bd6f1af7 100644 --- a/include/mosquitto.h +++ b/include/mosquitto.h @@ -85,7 +85,8 @@ extern "C" { #define MOSQ_LOG_INTERNAL 0x80000000U #define MOSQ_LOG_ALL 0xFFFFFFFFU -/* Error values */ +/* Enum: mosq_err_t + * Integer values returned from many libmosquitto functions. */ enum mosq_err_t { MOSQ_ERR_QUOTA_EXCEEDED = -6, MOSQ_ERR_AUTH_DELAYED = -5, @@ -142,7 +143,12 @@ enum mosq_err_t { MOSQ_ERR_CONNECTION_RATE_EXCEEDED = 159, }; -/* Option values */ +/* Enum: mosq_opt_t + * + * Client options. + * + * See , , and . + */ enum mosq_opt_t { MOSQ_OPT_PROTOCOL_VERSION = 1, MOSQ_OPT_SSL_CTX = 2, @@ -175,6 +181,24 @@ enum mosq_transport_t { #define MQTT_PROTOCOL_V311 4 #define MQTT_PROTOCOL_V5 5 +/* Struct: mosquitto_message + * + * Contains details of a PUBLISH message. + * + * int mid - the message/packet ID of the PUBLISH message, assuming this is a + * QoS 1 or 2 message. Will be set to 0 for QoS 0 messages. + * + * char *topic - the topic the message was delivered on. + * + * void *payload - the message payload. This will be payloadlen bytes long, and + * may be NULL if a zero length payload was sent. + * + * int payloadlen - the length of the payload, in bytes. + * + * int qos - the quality of service of the message, 0, 1, or 2. + * + * bool retain - set to true for stale retained messages. + */ struct mosquitto_message{ int mid; char *topic; @@ -359,9 +383,10 @@ libmosq_EXPORT void mosquitto_destroy(struct mosquitto *mosq); * callbacks that are specified. * * Returns: - * MOSQ_ERR_SUCCESS - on success. - * MOSQ_ERR_INVAL - if the input parameters were invalid. - * MOSQ_ERR_NOMEM - if an out of memory condition occurred. + * MOSQ_ERR_SUCCESS - on success. + * MOSQ_ERR_INVAL - if the input parameters were invalid. + * MOSQ_ERR_NOMEM - if an out of memory condition occurred. + * MOSQ_ERR_MALFORMED_UTF8 - if the client id is not valid UTF-8. * * See Also: * , @@ -1623,6 +1648,9 @@ libmosq_EXPORT int mosquitto_int_option(struct mosquitto *mosq, enum mosq_opt_t * MOSQ_OPT_TLS_ENGINE - Configure the client for TLS Engine support. * Pass a TLS Engine ID to be used when creating TLS * connections. Must be set before . + * Must be a valid engine, and note that the string will not be used + * until a connection attempt is made so this function will return + * success even if an invalid engine string is passed. * * MOSQ_OPT_TLS_KEYFORM - Configure the client to treat the keyfile * differently depending on its type. Must be set diff --git a/lib/connect.c b/lib/connect.c index 58e7c53c..ba011006 100644 --- a/lib/connect.c +++ b/lib/connect.c @@ -78,6 +78,7 @@ static int mosquitto__connect_init(struct mosquitto *mosq, const char *host, int mosq->msgs_in.inflight_quota = mosq->msgs_in.inflight_maximum; mosq->msgs_out.inflight_quota = mosq->msgs_out.inflight_maximum; mosq->retain_available = 1; + mosquitto__set_request_disconnect(mosq, false); return MOSQ_ERR_SUCCESS; } @@ -265,6 +266,7 @@ int mosquitto_disconnect_v5(struct mosquitto *mosq, int reason_code, const mosqu } mosquitto__set_state(mosq, mosq_cs_disconnected); + mosquitto__set_request_disconnect(mosq, true); if(!net__is_connected(mosq)){ return MOSQ_ERR_NO_CONN; }else{ diff --git a/lib/loop.c b/lib/loop.c index 02a22e2d..6be75ee7 100644 --- a/lib/loop.c +++ b/lib/loop.c @@ -108,9 +108,11 @@ int mosquitto_loop(struct mosquitto *mosq, int timeout, int max_packets) } now = mosquitto_time(); + pthread_mutex_lock(&mosq->msgtime_mutex); if(mosq->next_msg_out && now + timeout_ms/1000 > mosq->next_msg_out){ timeout_ms = (mosq->next_msg_out - now)*1000; } + pthread_mutex_unlock(&mosq->msgtime_mutex); if(timeout_ms < 0){ /* There has been a delay somewhere which means we should have already @@ -161,7 +163,6 @@ int mosquitto_loop(struct mosquitto *mosq, int timeout, int max_packets) FD_SET(mosq->sock, &writefds); } } - if(net__is_connected(mosq) && FD_ISSET(mosq->sock, &writefds)){ rc = mosquitto_loop_write(mosq, max_packets); if(rc || !net__is_connected(mosq)){ @@ -242,7 +243,6 @@ int mosquitto_loop_forever(struct mosquitto *mosq, int timeout, int max_packets) int run = 1; int rc = MOSQ_ERR_SUCCESS; unsigned long reconnect_delay; - enum mosquitto_client_state state; if(!mosq) return MOSQ_ERR_INVAL; @@ -281,8 +281,7 @@ int mosquitto_loop_forever(struct mosquitto *mosq, int timeout, int max_packets) pthread_testcancel(); #endif rc = MOSQ_ERR_SUCCESS; - state = mosquitto__get_state(mosq); - if(state == mosq_cs_disconnecting || state == mosq_cs_disconnected){ + if(mosquitto__get_request_disconnect(mosq)){ run = 0; }else{ if(mosq->reconnect_delay_max > mosq->reconnect_delay){ @@ -304,8 +303,7 @@ int mosquitto_loop_forever(struct mosquitto *mosq, int timeout, int max_packets) rc = interruptible_sleep(mosq, (time_t)reconnect_delay); if(rc) return rc; - state = mosquitto__get_state(mosq); - if(state == mosq_cs_disconnecting || state == mosq_cs_disconnected){ + if(mosquitto__get_request_disconnect(mosq)){ run = 0; }else{ rc = mosquitto_reconnect(mosq); diff --git a/lib/mosquitto.c b/lib/mosquitto.c index 1fecc3c1..60f0d2c0 100644 --- a/lib/mosquitto.c +++ b/lib/mosquitto.c @@ -109,10 +109,6 @@ struct mosquitto *mosquitto_new(const char *id, bool clean_start, void *userdata return NULL; } -#ifndef WIN32 - signal(SIGPIPE, SIG_IGN); -#endif - mosq = (struct mosquitto *)mosquitto__calloc(1, sizeof(struct mosquitto)); if(mosq){ mosq->sock = INVALID_SOCKET; @@ -179,6 +175,9 @@ int mosquitto_reinitialise(struct mosquitto *mosq, const char *id, bool clean_st return MOSQ_ERR_MALFORMED_UTF8; } mosq->id = mosquitto__strdup(id); + if(!mosq->id){ + return MOSQ_ERR_NOMEM; + } } packet__cleanup(&mosq->in_packet); mosq->out_packet = NULL; diff --git a/lib/mosquitto_internal.h b/lib/mosquitto_internal.h index 56893ab3..eae0789c 100644 --- a/lib/mosquitto_internal.h +++ b/lib/mosquitto_internal.h @@ -404,8 +404,9 @@ struct mosquitto { unsigned int reconnect_delay_max; int callback_depth; uint16_t port; - bool reconnect_exponential_backoff; bool disable_socketpair; + bool reconnect_exponential_backoff; + bool request_disconnect; char threaded; struct mosquitto__packet *out_packet_last; mosquitto_property *connect_properties; diff --git a/lib/net_mosq.c b/lib/net_mosq.c index 41c5f06e..894d1d14 100644 --- a/lib/net_mosq.c +++ b/lib/net_mosq.c @@ -674,8 +674,8 @@ static int net__init_ssl_ctx(struct mosquitto *mosq) * has not been set, or if both of MOSQ_OPT_SSL_CTX and * MOSQ_OPT_SSL_CTX_WITH_DEFAULTS are set. */ if(mosq->tls_cafile || mosq->tls_capath || mosq->tls_psk || mosq->tls_use_os_certs){ + net__init_tls(); if(!mosq->ssl_ctx){ - net__init_tls(); #if OPENSSL_VERSION_NUMBER < 0x10100000L mosq->ssl_ctx = SSL_CTX_new(SSLv23_client_method()); @@ -1042,11 +1042,7 @@ ssize_t net__write(struct mosquitto *mosq, const void *buf, size_t count) /* Call normal write/send */ #endif -#ifndef WIN32 - return write(mosq->sock, buf, count); -#else - return send(mosq->sock, buf, count, 0); -#endif + return send(mosq->sock, buf, count, MSG_NOSIGNAL); #ifdef WITH_TLS } diff --git a/lib/net_mosq.h b/lib/net_mosq.h index d5bb45dc..54e4f982 100644 --- a/lib/net_mosq.h +++ b/lib/net_mosq.h @@ -19,6 +19,7 @@ Contributors: #define NET_MOSQ_H #ifndef WIN32 +# include # include #else # include @@ -51,6 +52,10 @@ typedef SSIZE_T ssize_t; #define INVALID_SOCKET -1 #endif +#ifndef MSG_NOSIGNAL +# define MSG_NOSIGNAL 0 +#endif + /* Macros for accessing the MSB and LSB of a uint16_t */ #define MOSQ_MSB(A) (uint8_t)((A & 0xFF00) >> 8) #define MOSQ_LSB(A) (uint8_t)(A & 0x00FF) diff --git a/lib/options.c b/lib/options.c index f6fdd77b..8f33f675 100644 --- a/lib/options.c +++ b/lib/options.c @@ -274,14 +274,17 @@ int mosquitto_string_option(struct mosquitto *mosq, enum mosq_opt_t option, cons switch(option){ case MOSQ_OPT_TLS_ENGINE: #if defined(WITH_TLS) && !defined(OPENSSL_NO_ENGINE) - eng = ENGINE_by_id(value); - if(!eng){ - return MOSQ_ERR_INVAL; - } - ENGINE_free(eng); /* release the structural reference from ENGINE_by_id() */ - mosq->tls_engine = mosquitto__strdup(value); - if(!mosq->tls_engine){ - return MOSQ_ERR_NOMEM; + mosquitto__free(mosq->tls_engine); + if(value){ + eng = ENGINE_by_id(value); + if(!eng){ + return MOSQ_ERR_INVAL; + } + ENGINE_free(eng); /* release the structural reference from ENGINE_by_id() */ + mosq->tls_engine = mosquitto__strdup(value); + if(!mosq->tls_engine){ + return MOSQ_ERR_NOMEM; + } } return MOSQ_ERR_SUCCESS; #else diff --git a/lib/packet_mosq.c b/lib/packet_mosq.c index d5ef8792..fa044941 100644 --- a/lib/packet_mosq.c +++ b/lib/packet_mosq.c @@ -558,7 +558,7 @@ int packet__read(struct mosquitto *mosq) mosq->in_packet.pos = 0; #ifdef WITH_BROKER G_MSGS_RECEIVED_INC(1); - if(((mosq->in_packet.command)&0xF5) == CMD_PUBLISH){ + if(((mosq->in_packet.command)&0xF0) == CMD_PUBLISH){ G_PUB_MSGS_RECEIVED_INC(1); } #endif diff --git a/lib/property_mosq.c b/lib/property_mosq.c index 64763487..ba319427 100644 --- a/lib/property_mosq.c +++ b/lib/property_mosq.c @@ -1206,6 +1206,7 @@ BROKER_EXPORT int mosquitto_property_copy_all(mosquitto_property **dest, const m } plast = pnew; + pnew->client_generated = src->client_generated; pnew->identifier = src->identifier; switch(pnew->identifier){ case MQTT_PROP_PAYLOAD_FORMAT_INDICATOR: diff --git a/lib/strings_mosq.c b/lib/strings_mosq.c index 6fd167ed..850ed9b5 100644 --- a/lib/strings_mosq.c +++ b/lib/strings_mosq.c @@ -21,6 +21,10 @@ Contributors: #include #include +#ifndef WIN32 +# include +#endif + #include "mosquitto.h" #include "mqtt_protocol.h" diff --git a/lib/util_mosq.c b/lib/util_mosq.c index 86fcfca3..3d846ae7 100644 --- a/lib/util_mosq.c +++ b/lib/util_mosq.c @@ -106,6 +106,11 @@ int mosquitto__check_keepalive(struct mosquitto *mosq) pthread_mutex_unlock(&mosq->msgtime_mutex); }else{ #ifdef WITH_BROKER +# ifdef WITH_BRIDGE + if(mosq->bridge){ + context__send_will(mosq); + } +# endif net__socket_close(mosq); #else net__socket_close(mosq); @@ -288,3 +293,23 @@ enum mosquitto_client_state mosquitto__get_state(struct mosquitto *mosq) return state; } + +#ifndef WITH_BROKER +void mosquitto__set_request_disconnect(struct mosquitto *mosq, bool request_disconnect) +{ + pthread_mutex_lock(&mosq->state_mutex); + mosq->request_disconnect = request_disconnect; + pthread_mutex_unlock(&mosq->state_mutex); +} + +bool mosquitto__get_request_disconnect(struct mosquitto *mosq) +{ + bool request_disconnect; + + pthread_mutex_lock(&mosq->state_mutex); + request_disconnect = mosq->request_disconnect; + pthread_mutex_unlock(&mosq->state_mutex); + + return request_disconnect; +} +#endif diff --git a/lib/util_mosq.h b/lib/util_mosq.h index ff1e1a46..ebd6881d 100644 --- a/lib/util_mosq.h +++ b/lib/util_mosq.h @@ -32,6 +32,10 @@ uint16_t mosquitto__mid_generate(struct mosquitto *mosq); int mosquitto__set_state(struct mosquitto *mosq, enum mosquitto_client_state state); enum mosquitto_client_state mosquitto__get_state(struct mosquitto *mosq); +#ifndef WITH_BROKER +void mosquitto__set_request_disconnect(struct mosquitto *mosq, bool request_disconnect); +bool mosquitto__get_request_disconnect(struct mosquitto *mosq); +#endif #ifdef WITH_TLS int mosquitto__hex2bin_sha1(const char *hex, unsigned char **bin); diff --git a/man/CMakeLists.txt b/man/CMakeLists.txt index e6d6545f..93b8b190 100644 --- a/man/CMakeLists.txt +++ b/man/CMakeLists.txt @@ -8,10 +8,10 @@ if(NOT WIN32) find_program(XSLTPROC xsltproc OPTIONAL) if(XSLTPROC) function(compile_manpage page) - add_custom_command(OUTPUT "${CMAKE_SOURCE_DIR}/man/${page}" - COMMAND xsltproc "${CMAKE_SOURCE_DIR}/man/${page}.xml" -o "${CMAKE_SOURCE_DIR}/man/" - MAIN_DEPENDENCY "${CMAKE_SOURCE_DIR}/man/${page}.xml") - add_custom_target(${page} ALL DEPENDS "${CMAKE_SOURCE_DIR}/man/${page}") + add_custom_command(OUTPUT ${PROJECT_SOURCE_DIR}/man/${page} + COMMAND xsltproc ${PROJECT_SOURCE_DIR}/man/${page}.xml -o ${PROJECT_SOURCE_DIR}/man/ + MAIN_DEPENDENCY ${PROJECT_SOURCE_DIR}/man/${page}.xml) + add_custom_target(${page} ALL DEPENDS ${PROJECT_SOURCE_DIR}/man/${page}) endfunction() compile_manpage("libmosquitto.3") diff --git a/man/mosquitto.conf.5.xml b/man/mosquitto.conf.5.xml index 9878e8a6..0296b77a 100644 --- a/man/mosquitto.conf.5.xml +++ b/man/mosquitto.conf.5.xml @@ -925,15 +925,21 @@ log_timestamp_format %Y-%m-%dT%H:%M:%S duration - This option allows persistent clients (those with - clean session set to false) to be removed if they do - not reconnect within a certain time frame. This is a - non-standard option. As far as the MQTT spec is - concerned, persistent clients persist forever. - Badly designed clients may set clean session to false - whilst using a randomly generated client id. This leads - to persistent clients that will never reconnect. This - option allows these clients to be removed. + + This option allows the session of persistent clients (those with clean + session set to false) that are not currently connected to be removed if they + do not reconnect within a certain time frame. This is a non-standard option + in MQTT v3.1. MQTT v3.1.1 and v5.0 allow brokers to remove client sessions. + + + + Badly designed clients may set clean session to false whilst using a randomly + generated client id. This leads to persistent clients that connect once and + never reconnect. This option allows these clients to be removed. This option + allows persistent clients (those with clean session set to false) to be + removed if they do not reconnect within a certain time frame. + + The expiration period should be an integer followed by one of h d w m y for hour, day, week, month and year respectively. For example: @@ -1043,7 +1049,9 @@ log_timestamp_format %Y-%m-%dT%H:%M:%S Set to true to queue messages with QoS 0 when a persistent client is - disconnected. These messages are included in the limit + disconnected. When bridges topics are configured with QoS level 1 or 2 incoming + QoS 0 messages for these topics are also queued. + These messages are included in the limit imposed by max_queued_messages. Defaults to false. Note that the MQTT v3.1.1 spec states that only QoS 1 @@ -1398,6 +1406,7 @@ accept_protocol_versions 3, 4 disconnected as not authorised when this option is set to true. Do not use in conjunction with . + This does not apply globally, but on a per-listener basis. See also . Not reloaded on reload signal. diff --git a/mosquitto.conf b/mosquitto.conf index 49e2265e..810be29f 100644 --- a/mosquitto.conf +++ b/mosquitto.conf @@ -160,14 +160,16 @@ # accepted. MQTT imposes a maximum payload size of 268435455 bytes. #message_size_limit 0 -# This option allows persistent clients (those with clean session set to false) -# to be removed if they do not reconnect within a certain time frame. -# -# This is a non-standard option in MQTT V3.1 but allowed in MQTT v3.1.1. +# This option allows the session of persistent clients (those with clean +# session set to false) that are not currently connected to be removed if they +# do not reconnect within a certain time frame. This is a non-standard option +# in MQTT v3.1. MQTT v3.1.1 and v5.0 allow brokers to remove client sessions. # # Badly designed clients may set clean session to false whilst using a randomly -# generated client id. This leads to persistent clients that will never -# reconnect. This option allows these clients to be removed. +# generated client id. This leads to persistent clients that connect once and +# never reconnect. This option allows these clients to be removed. This option +# allows persistent clients (those with clean session set to false) to be +# removed if they do not reconnect within a certain time frame. # # The expiration period should be an integer followed by one of h d w m y for # hour, day, week, month and year respectively. For example @@ -324,6 +326,7 @@ # authorised when this option is set to true. # Do not use in conjunction with clientid_prefixes. # See also use_identity_as_username. +# This does not apply globally, but on a per-listener basis. #use_username_as_clientid # Change the websockets headers size. This is a global option, it is not diff --git a/plugins/deny-protocol-version/CMakeLists.txt b/plugins/deny-protocol-version/CMakeLists.txt new file mode 100644 index 00000000..d998df75 --- /dev/null +++ b/plugins/deny-protocol-version/CMakeLists.txt @@ -0,0 +1,24 @@ +set (PLUGIN_NAME mosquitto_deny_protocol_version) + +add_library(${PLUGIN_NAME} MODULE + ${PLUGIN_NAME}.c +) + +target_include_directories(${PLUGIN_NAME} PRIVATE + "${OPENSSL_INCLUDE_DIR}" + "${STDBOOL_H_PATH} ${STDINT_H_PATH}" + "${mosquitto_SOURCE_DIR}" + "${mosquitto_SOURCE_DIR}/include" +) + +set_target_properties(${PLUGIN_NAME} PROPERTIES + PREFIX "" + POSITION_INDEPENDENT_CODE 1 +) + +if(WIN32) + target_link_libraries(${PLUGIN_NAME} PRIVATE mosquitto) +endif() + +# Don't install, these are example plugins only. +#install(TARGETS ${PLUGIN_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}") diff --git a/plugins/deny-protocol-version/Makefile b/plugins/deny-protocol-version/Makefile new file mode 100644 index 00000000..42e6c806 --- /dev/null +++ b/plugins/deny-protocol-version/Makefile @@ -0,0 +1,29 @@ +R=../.. +include ${R}/config.mk + +.PHONY : all binary check clean reallyclean test install uninstall + +PLUGIN_NAME=mosquitto_deny_protocol_version +PLUGIN_CFLAGS+=-I${R}/include -I${R}/ + +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/deny-protocol-version/mosquitto_deny_protocol_version.c b/plugins/deny-protocol-version/mosquitto_deny_protocol_version.c new file mode 100644 index 00000000..191cf710 --- /dev/null +++ b/plugins/deny-protocol-version/mosquitto_deny_protocol_version.c @@ -0,0 +1,110 @@ +/* +Copyright (c) 2022 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 deny access based on the version of + * the protocol spec a client connects with. It does no other authentication + * checks. + * + * It could be used with other authentication plugins by specifying it in the + * config file before another plugin, for example: + * + * plugin /usr/lib/mosquitto_deny_protocol_version.so + * plugin /usr/lib/mosquitto_dynamic_security.so + * + * or: + * + * plugin /usr/lib/mosquitto_deny_protocol_version.so + * password_file pwfile + * + * It will *not* work on its own. + * + * In Mosquitto 2.1, this can be achieved with the `accept_protocol_version` + * option instead. + * + * + * To compile: + * + * gcc -I -fPIC -shared mosquitto_deny_protocol_version.c -o mosquitto_deny_protocol_version.so + * + * Note that this only works on Mosquitto 2.0 or later. + */ +#include "config.h" + +#include +#include + +#include "mosquitto_broker.h" +#include "mosquitto_plugin.h" +#include "mosquitto.h" +#include "mqtt_protocol.h" + +static mosquitto_plugin_id_t *mosq_pid = NULL; + +int mosquitto_plugin_version(int supported_version_count, const int *supported_versions) +{ + int i; + + for(i=0; iclient); + + if(protocol_version == 5 || protocol_version == 4){ + /* Allow access to MQTT v5.0 and v3.1.1 - this passes on responsibility + * for the actual auth checks to the next plugin/password file in the + * config list. If no other plugins/password file is defined, then + * access will be denied. */ + return MOSQ_ERR_PLUGIN_DEFER; + }else{ + /* Deny access to all others */ + return MOSQ_ERR_AUTH; + } +} + +int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **user_data, struct mosquitto_opt *opts, int opt_count) +{ + UNUSED(user_data); + UNUSED(opts); + UNUSED(opt_count); + + mosq_pid = identifier; + return mosquitto_callback_register(mosq_pid, MOSQ_EVT_BASIC_AUTH, basic_auth_callback, NULL, NULL); +} + +int mosquitto_plugin_cleanup(void *user_data, struct mosquitto_opt *opts, int opt_count) +{ + UNUSED(user_data); + UNUSED(opts); + UNUSED(opt_count); + + return mosquitto_callback_unregister(mosq_pid, MOSQ_EVT_MESSAGE, basic_auth_callback, NULL); +} diff --git a/plugins/deny-protocol-version/test.conf b/plugins/deny-protocol-version/test.conf new file mode 100644 index 00000000..01021314 --- /dev/null +++ b/plugins/deny-protocol-version/test.conf @@ -0,0 +1,4 @@ +listener 1883 + +plugin ./mosquitto_deny_protocol_version.so +password_file pwfile diff --git a/plugins/deny-protocol-version/test.sh b/plugins/deny-protocol-version/test.sh new file mode 100755 index 00000000..3005da50 --- /dev/null +++ b/plugins/deny-protocol-version/test.sh @@ -0,0 +1,4 @@ +#!/bin/sh + +../../apps/mosquitto_passwd/mosquitto_passwd -c -b pwfile username password +../../src/mosquitto -c test.conf -v diff --git a/plugins/dynamic-security/clients.c b/plugins/dynamic-security/clients.c index a2c7cbd9..c75a9379 100644 --- a/plugins/dynamic-security/clients.c +++ b/plugins/dynamic-security/clients.c @@ -718,10 +718,12 @@ static void client__remove_all_roles(struct dynsec__client *client) int dynsec_clients__process_modify(struct dynsec__data *data, struct plugin_cmd *cmd, struct mosquitto *context) { char *username; - char *clientid; - char *password; - char *text_name, *text_description; + char *clientid = NULL; + char *password = NULL; + char *text_name = NULL, *text_description = NULL; + bool have_clientid = false, have_text_name = false, have_text_description = false, have_rolelist = false, have_password = false; struct dynsec__client *client; + struct dynsec__group *group; struct dynsec__rolelist *rolelist = NULL; char *str; int rc; @@ -744,81 +746,87 @@ int dynsec_clients__process_modify(struct dynsec__data *data, struct plugin_cmd return MOSQ_ERR_INVAL; } - if(json_get_string(cmd->j_command, "clientid", &clientid, false) == MOSQ_ERR_SUCCESS){ - if(clientid && strlen(clientid) > 0){ - str = mosquitto_strdup(clientid); - if(str == NULL){ + if(json_get_string(cmd->j_command, "clientid", &str, false) == MOSQ_ERR_SUCCESS){ + have_clientid = true; + if(str && strlen(str) > 0){ + clientid = mosquitto_strdup(str); + if(clientid == NULL){ plugin__command_reply(cmd, "Internal error"); - return MOSQ_ERR_NOMEM; + rc = MOSQ_ERR_NOMEM; + goto error; } }else{ - str = NULL; + clientid = NULL; } - mosquitto_free(client->clientid); - client->clientid = str; } if(json_get_string(cmd->j_command, "password", &password, false) == MOSQ_ERR_SUCCESS){ if(strlen(password) > 0){ - /* If password == "", we just ignore it */ - rc = client__set_password(client, password); - if(rc != MOSQ_ERR_SUCCESS){ - plugin__command_reply(cmd, "Internal error"); - dynsec_kicklist__add(data, username); - return MOSQ_ERR_NOMEM; - } + have_password = true; } } - if(json_get_string(cmd->j_command, "textname", &text_name, false) == MOSQ_ERR_SUCCESS){ - str = mosquitto_strdup(text_name); - if(str == NULL){ + if(json_get_string(cmd->j_command, "textname", &str, false) == MOSQ_ERR_SUCCESS){ + have_text_name = true; + text_name = mosquitto_strdup(str); + if(text_name == NULL){ plugin__command_reply(cmd, "Internal error"); - dynsec_kicklist__add(data, username); - return MOSQ_ERR_NOMEM; + rc = MOSQ_ERR_NOMEM; + goto error; } - mosquitto_free(client->text_name); - client->text_name = str; } - if(json_get_string(cmd->j_command, "textdescription", &text_description, false) == MOSQ_ERR_SUCCESS){ - str = mosquitto_strdup(text_description); - if(str == NULL){ + if(json_get_string(cmd->j_command, "textdescription", &str, false) == MOSQ_ERR_SUCCESS){ + have_text_description = true; + text_description = mosquitto_strdup(str); + if(text_description == NULL){ plugin__command_reply(cmd, "Internal error"); - dynsec_kicklist__add(data, username); - return MOSQ_ERR_NOMEM; + rc = MOSQ_ERR_NOMEM; + goto error; } - mosquitto_free(client->text_description); - client->text_description = str; } rc = dynsec_rolelist__load_from_json(data, cmd->j_command, &rolelist); if(rc == MOSQ_ERR_SUCCESS){ - client__remove_all_roles(client); - client__add_new_roles(client, rolelist); - dynsec_rolelist__cleanup(&rolelist); + have_rolelist = true; }else if(rc == ERR_LIST_NOT_FOUND){ /* There was no list in the JSON, so no modification */ }else if(rc == MOSQ_ERR_NOT_FOUND){ plugin__command_reply(cmd, "Role not found"); - dynsec_rolelist__cleanup(&rolelist); - dynsec_kicklist__add(data, username); - return MOSQ_ERR_INVAL; + rc = MOSQ_ERR_INVAL; + goto error; }else{ if(rc == MOSQ_ERR_INVAL){ plugin__command_reply(cmd, "'roles' not an array or missing/invalid rolename"); }else{ plugin__command_reply(cmd, "Internal error"); } - dynsec_rolelist__cleanup(&rolelist); - dynsec_kicklist__add(data, username); - return MOSQ_ERR_INVAL; + rc = MOSQ_ERR_INVAL; + goto error; } j_groups = cJSON_GetObjectItem(cmd->j_command, "groups"); if(j_groups && cJSON_IsArray(j_groups)){ - dynsec__remove_client_from_all_groups(data, username); + /* Iterate through list to check all groups are valid */ + cJSON_ArrayForEach(j_group, j_groups){ + if(cJSON_IsObject(j_group)){ + jtmp = cJSON_GetObjectItem(j_group, "groupname"); + if(jtmp && cJSON_IsString(jtmp)){ + group = dynsec_groups__find(data, jtmp->valuestring); + if(group == NULL){ + plugin__command_reply(cmd, "'groups' contains an object with a 'groupname' that does not exist"); + rc = MOSQ_ERR_INVAL; + goto error; + } + }else{ + plugin__command_reply(cmd, "'groups' contains an object with an invalid 'groupname'"); + rc = MOSQ_ERR_INVAL; + goto error; + } + } + } + dynsec__remove_client_from_all_groups(data, username); cJSON_ArrayForEach(j_group, j_groups){ if(cJSON_IsObject(j_group)){ jtmp = cJSON_GetObjectItem(j_group, "groupname"); @@ -830,6 +838,44 @@ int dynsec_clients__process_modify(struct dynsec__data *data, struct plugin_cmd } } + if(have_password){ + /* FIXME - This is the one call that will result in modification on internal error - note that groups have already been modified */ + rc = client__set_password(client, password); + if(rc != MOSQ_ERR_SUCCESS){ + plugin__command_reply(cmd, "Internal error"); + mosquitto_kick_client_by_username(username, false); + /* If this fails we have the situation that the password is set as + * invalid, but the config isn't saved, so restarting the broker + * *now* will mean the client can log in again. This might be + * "good", but is inconsistent, so save the config to be + * consistent. */ + dynsec__config_save(data); + rc = MOSQ_ERR_NOMEM; + goto error; + } + } + + if(have_clientid){ + mosquitto_free(client->clientid); + client->clientid = clientid; + } + + if(have_text_name){ + mosquitto_free(client->text_name); + client->text_name = text_name; + } + + if(have_text_description){ + mosquitto_free(client->text_description); + client->text_description = text_description; + } + + if(have_rolelist){ + client__remove_all_roles(client); + client__add_new_roles(client, rolelist); + dynsec_rolelist__cleanup(&rolelist); + } + dynsec__config_save(data); plugin__command_reply(cmd, NULL); @@ -841,6 +887,12 @@ int dynsec_clients__process_modify(struct dynsec__data *data, struct plugin_cmd mosquitto_log_printf(MOSQ_LOG_INFO, "dynsec: %s/%s | modifyClient | username=%s", admin_clientid, admin_username, username); return MOSQ_ERR_SUCCESS; +error: + mosquitto_free(clientid); + mosquitto_free(text_name); + mosquitto_free(text_description); + dynsec_rolelist__cleanup(&rolelist); + return rc; } diff --git a/plugins/dynamic-security/dynamic_security.h b/plugins/dynamic-security/dynamic_security.h index a236ae23..36483bc1 100644 --- a/plugins/dynamic-security/dynamic_security.h +++ b/plugins/dynamic-security/dynamic_security.h @@ -305,5 +305,6 @@ cJSON *dynsec_rolelist__all_to_json(struct dynsec__rolelist *base_rolelist); int dynsec_kicklist__add(struct dynsec__data *data, const char *username); void dynsec_kicklist__kick(struct dynsec__data *data); int dynsec__tick_callback(int event, void *event_data, void *userdata); +void dynsec_kicklist__cleanup(struct dynsec__data *data); #endif diff --git a/plugins/dynamic-security/groups.c b/plugins/dynamic-security/groups.c index 2f718289..7b81495c 100644 --- a/plugins/dynamic-security/groups.c +++ b/plugins/dynamic-security/groups.c @@ -909,10 +909,12 @@ int dynsec_groups__process_remove_role(struct dynsec__data *data, struct plugin_ int dynsec_groups__process_modify(struct dynsec__data *data, struct plugin_cmd *cmd, struct mosquitto *context) { - char *groupname; - char *text_name, *text_description; - struct dynsec__group *group; + char *groupname = NULL; + char *text_name = NULL, *text_description = NULL; + struct dynsec__client *client = NULL; + struct dynsec__group *group = NULL; struct dynsec__rolelist *rolelist = NULL; + bool have_text_name = false, have_text_description = false, have_rolelist = false; char *str; int rc; int priority; @@ -934,52 +936,73 @@ int dynsec_groups__process_modify(struct dynsec__data *data, struct plugin_cmd * return MOSQ_ERR_INVAL; } - if(json_get_string(cmd->j_command, "textname", &text_name, false) == MOSQ_ERR_SUCCESS){ - str = mosquitto_strdup(text_name); - if(str == NULL){ + if(json_get_string(cmd->j_command, "textname", &str, false) == MOSQ_ERR_SUCCESS){ + have_text_name = true; + text_name = mosquitto_strdup(str); + if(text_name == NULL){ plugin__command_reply(cmd, "Internal error"); - return MOSQ_ERR_NOMEM; + rc = MOSQ_ERR_NOMEM; + goto error; } - mosquitto_free(group->text_name); - group->text_name = str; } - if(json_get_string(cmd->j_command, "textdescription", &text_description, false) == MOSQ_ERR_SUCCESS){ - str = mosquitto_strdup(text_description); - if(str == NULL){ + if(json_get_string(cmd->j_command, "textdescription", &str, false) == MOSQ_ERR_SUCCESS){ + have_text_description = true; + text_description = mosquitto_strdup(str); + if(text_description == NULL){ plugin__command_reply(cmd, "Internal error"); - return MOSQ_ERR_NOMEM; + rc = MOSQ_ERR_NOMEM; + goto error; } - mosquitto_free(group->text_description); - group->text_description = str; } rc = dynsec_rolelist__load_from_json(data, cmd->j_command, &rolelist); if(rc == MOSQ_ERR_SUCCESS){ - dynsec_rolelist__cleanup(&group->rolelist); - group->rolelist = rolelist; + /* Apply changes below */ + have_rolelist = true; }else if(rc == ERR_LIST_NOT_FOUND){ /* There was no list in the JSON, so no modification */ + rolelist = NULL; }else if(rc == MOSQ_ERR_NOT_FOUND){ plugin__command_reply(cmd, "Role not found"); - dynsec_rolelist__cleanup(&rolelist); - group__kick_all(data, group); - return MOSQ_ERR_INVAL; + rc = MOSQ_ERR_INVAL; + goto error; }else{ if(rc == MOSQ_ERR_INVAL){ plugin__command_reply(cmd, "'roles' not an array or missing/invalid rolename"); }else{ plugin__command_reply(cmd, "Internal error"); } - dynsec_rolelist__cleanup(&rolelist); - group__kick_all(data, group); - return MOSQ_ERR_INVAL; + rc = MOSQ_ERR_INVAL; + goto error; } j_clients = cJSON_GetObjectItem(cmd->j_command, "clients"); if(j_clients && cJSON_IsArray(j_clients)){ + /* Iterate over array to check clients are valid before proceeding */ + cJSON_ArrayForEach(j_client, j_clients){ + if(cJSON_IsObject(j_client)){ + jtmp = cJSON_GetObjectItem(j_client, "username"); + if(jtmp && cJSON_IsString(jtmp)){ + client = dynsec_clients__find(data, jtmp->valuestring); + if(client == NULL){ + plugin__command_reply(cmd, "'clients' contains an object with a 'username' that does not exist"); + rc = MOSQ_ERR_INVAL; + goto error; + } + }else{ + plugin__command_reply(cmd, "'clients' contains an object with an invalid 'username'"); + rc = MOSQ_ERR_INVAL; + goto error; + } + } + } + + /* Kick all clients in the *current* group */ + group__kick_all(data, group); dynsec__remove_all_clients_from_group(group); + /* Now we can add the new clients to the group */ cJSON_ArrayForEach(j_client, j_clients){ if(cJSON_IsObject(j_client)){ jtmp = cJSON_GetObjectItem(j_client, "username"); @@ -991,11 +1014,28 @@ int dynsec_groups__process_modify(struct dynsec__data *data, struct plugin_cmd * } } + /* Apply remaining changes to group, note that user changes are already applied */ + if(have_text_name){ + mosquitto_free(group->text_name); + group->text_name = text_name; + } + + if(have_text_description){ + mosquitto_free(group->text_description); + group->text_description = text_description; + } + + if(have_rolelist){ + dynsec_rolelist__cleanup(&group->rolelist); + group->rolelist = rolelist; + } + + /* And save */ dynsec__config_save(data); plugin__command_reply(cmd, NULL); - /* Enforce any changes */ + /* Enforce any changes - kick any clients in the *new* group */ group__kick_all(data, group); admin_clientid = mosquitto_client_id(context); @@ -1004,6 +1044,17 @@ int dynsec_groups__process_modify(struct dynsec__data *data, struct plugin_cmd * admin_clientid, admin_username, groupname); return MOSQ_ERR_SUCCESS; +error: + mosquitto_free(text_name); + mosquitto_free(text_description); + dynsec_rolelist__cleanup(&rolelist); + + admin_clientid = mosquitto_client_id(context); + admin_username = mosquitto_client_username(context); + mosquitto_log_printf(MOSQ_LOG_INFO, "dynsec: %s/%s | modifyGroup | groupname=%s", + admin_clientid, admin_username, groupname); + + return rc; } diff --git a/plugins/dynamic-security/kicklist.c b/plugins/dynamic-security/kicklist.c index fd06e081..2d8ab329 100644 --- a/plugins/dynamic-security/kicklist.c +++ b/plugins/dynamic-security/kicklist.c @@ -64,3 +64,13 @@ void dynsec_kicklist__kick(struct dynsec__data *data) free(kick); } } + +void dynsec_kicklist__cleanup(struct dynsec__data *data) +{ + struct dynsec__kicklist *kick, *tmp; + + DL_FOREACH_SAFE(data->kicklist, kick, tmp){ + DL_DELETE(data->kicklist, kick); + free(kick); + } +} diff --git a/plugins/dynamic-security/plugin.c b/plugins/dynamic-security/plugin.c index 788efacd..15e2a8d2 100644 --- a/plugins/dynamic-security/plugin.c +++ b/plugins/dynamic-security/plugin.c @@ -24,6 +24,11 @@ Contributors: #include #include +#ifndef WIN32 +# include +#endif + +#include "json_help.h" #include "mosquitto.h" #include "mosquitto_broker.h" #include "mosquitto_plugin.h" @@ -39,6 +44,7 @@ static mosquitto_plugin_id_t *plg_id = NULL; int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **user_data, struct mosquitto_opt *options, int option_count) { int i; + int rc; UNUSED(user_data); @@ -66,12 +72,54 @@ int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **user_data, s mosquitto_plugin_set_info(identifier, "dynamic-security", NULL); dynsec__config_load(&dynsec_data); - mosquitto_callback_register(plg_id, MOSQ_EVT_CONTROL, dynsec_control_callback, "$CONTROL/dynamic-security/v1", &dynsec_data); - mosquitto_callback_register(plg_id, MOSQ_EVT_BASIC_AUTH, dynsec_auth__basic_auth_callback, NULL, &dynsec_data); - mosquitto_callback_register(plg_id, MOSQ_EVT_ACL_CHECK, dynsec__acl_check_callback, NULL, &dynsec_data); - mosquitto_callback_register(plg_id, MOSQ_EVT_TICK, dynsec__tick_callback, NULL, &dynsec_data); + + rc = mosquitto_callback_register(plg_id, MOSQ_EVT_CONTROL, dynsec_control_callback, "$CONTROL/dynamic-security/v1", &dynsec_data); + if(rc == MOSQ_ERR_ALREADY_EXISTS){ + mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Dynamic security plugin can currently only be loaded once."); + mosquitto_log_printf(MOSQ_LOG_ERR, "Note that this was previously incorrectly allowed but could cause problems with duplicate entries in the config."); + goto error; + }else if(rc == MOSQ_ERR_NOMEM){ + mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Out of memory."); + goto error; + }else if(rc != MOSQ_ERR_SUCCESS){ + goto error; + } + + rc = mosquitto_callback_register(plg_id, MOSQ_EVT_BASIC_AUTH, dynsec_auth__basic_auth_callback, NULL, &dynsec_data); + if(rc == MOSQ_ERR_ALREADY_EXISTS){ + mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Dynamic security plugin can only be loaded once."); + goto error; + }else if(rc == MOSQ_ERR_NOMEM){ + mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Out of memory."); + goto error; + }else if(rc != MOSQ_ERR_SUCCESS){ + goto error; + } + + rc = mosquitto_callback_register(plg_id, MOSQ_EVT_ACL_CHECK, dynsec__acl_check_callback, NULL, &dynsec_data); + if(rc == MOSQ_ERR_ALREADY_EXISTS){ + mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Dynamic security plugin can only be loaded once."); + goto error; + }else if(rc == MOSQ_ERR_NOMEM){ + mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Out of memory."); + goto error; + }else if(rc != MOSQ_ERR_SUCCESS){ + goto error; + } + + rc = mosquitto_callback_register(plg_id, MOSQ_EVT_TICK, dynsec__tick_callback, NULL, &dynsec_data); + if(rc == MOSQ_ERR_NOMEM){ + mosquitto_log_printf(MOSQ_LOG_ERR, "Error: Out of memory."); + goto error; + }else if(rc != MOSQ_ERR_SUCCESS){ + goto error; + } return MOSQ_ERR_SUCCESS; +error: + mosquitto_free(dynsec_data.config_file); + dynsec_data.config_file = NULL; + return rc; } int mosquitto_plugin_cleanup(void *user_data, struct mosquitto_opt *options, int option_count) @@ -83,6 +131,7 @@ int mosquitto_plugin_cleanup(void *user_data, struct mosquitto_opt *options, int dynsec_groups__cleanup(&dynsec_data); dynsec_clients__cleanup(&dynsec_data); dynsec_roles__cleanup(&dynsec_data); + dynsec_kicklist__cleanup(&dynsec_data); mosquitto_free(dynsec_data.config_file); dynsec_data.config_file = NULL; diff --git a/plugins/dynamic-security/roles.c b/plugins/dynamic-security/roles.c index 321bbd60..d69be20d 100644 --- a/plugins/dynamic-security/roles.c +++ b/plugins/dynamic-security/roles.c @@ -24,6 +24,10 @@ Contributors: #include #include +#ifndef WIN32 +# include +#endif + #include "dynamic_security.h" #include "json_help.h" #include "mosquitto.h" diff --git a/service/systemd/mosquitto.service.notify b/service/systemd/mosquitto.service.notify index d88a8868..06772dda 100644 --- a/service/systemd/mosquitto.service.notify +++ b/service/systemd/mosquitto.service.notify @@ -11,9 +11,9 @@ ExecStart=/usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure ExecStartPre=/bin/mkdir -m 740 -p /var/log/mosquitto -ExecStartPre=/bin/chown mosquitto /var/log/mosquitto +ExecStartPre=/bin/chown mosquitto:mosquitto /var/log/mosquitto ExecStartPre=/bin/mkdir -m 740 -p /run/mosquitto -ExecStartPre=/bin/chown mosquitto /run/mosquitto +ExecStartPre=/bin/chown mosquitto:mosquitto /run/mosquitto [Install] WantedBy=multi-user.target diff --git a/service/systemd/mosquitto.service.simple b/service/systemd/mosquitto.service.simple index 80efecd4..15ee0d62 100644 --- a/service/systemd/mosquitto.service.simple +++ b/service/systemd/mosquitto.service.simple @@ -9,9 +9,9 @@ ExecStart=/usr/sbin/mosquitto -c /etc/mosquitto/mosquitto.conf ExecReload=/bin/kill -HUP $MAINPID Restart=on-failure ExecStartPre=/bin/mkdir -m 740 -p /var/log/mosquitto -ExecStartPre=/bin/chown mosquitto /var/log/mosquitto +ExecStartPre=/bin/chown mosquitto:mosquitto /var/log/mosquitto ExecStartPre=/bin/mkdir -m 740 -p /run/mosquitto -ExecStartPre=/bin/chown mosquitto /run/mosquitto +ExecStartPre=/bin/chown mosquitto:mosquitto /run/mosquitto [Install] WantedBy=multi-user.target diff --git a/src/conf.c b/src/conf.c index 9e61e115..3f1d9b35 100644 --- a/src/conf.c +++ b/src/conf.c @@ -2174,6 +2174,8 @@ static int config__read_file_core(struct mosquitto__config *config, bool reload, return MOSQ_ERR_INVAL; } cur_bridge->restart_timeout = atoi(token); + cur_bridge->backoff_base = 0; + cur_bridge->backoff_cap = 0; if(cur_bridge->restart_timeout < 1){ log__printf(NULL, MOSQ_LOG_NOTICE, "restart_timeout interval too low, using 1 second."); cur_bridge->restart_timeout = 1; diff --git a/src/database.c b/src/database.c index f73e84b8..24e2fabd 100644 --- a/src/database.c +++ b/src/database.c @@ -1223,6 +1223,40 @@ int db__message_release_incoming(struct mosquitto *context, uint16_t mid) } } + +void db__expire_all_messages(struct mosquitto *context) +{ + struct mosquitto_client_msg *msg, *tmp; + + DL_FOREACH_SAFE(context->msgs_out.inflight, msg, tmp){ + if(msg->base_msg->message_expiry_time && db.now_real_s > msg->base_msg->message_expiry_time){ + if(msg->qos > 0){ + util__increment_send_quota(context); + } + db__message_remove_inflight(context, &context->msgs_out, msg); + } + } + DL_FOREACH_SAFE(context->msgs_out.queued, msg, tmp){ + if(msg->base_msg->message_expiry_time && db.now_real_s > msg->base_msg->message_expiry_time){ + db__message_remove_queued(context, &context->msgs_out, msg); + } + } + DL_FOREACH_SAFE(context->msgs_in.inflight, msg, tmp){ + if(msg->base_msg->message_expiry_time && db.now_real_s > msg->base_msg->message_expiry_time){ + if(msg->qos > 0){ + util__increment_receive_quota(context); + } + db__message_remove_inflight(context, &context->msgs_in, msg); + } + } + DL_FOREACH_SAFE(context->msgs_in.queued, msg, tmp){ + if(msg->base_msg->message_expiry_time && db.now_real_s > msg->base_msg->message_expiry_time){ + db__message_remove_queued(context, &context->msgs_in, msg); + } + } +} + + static int db__message_write_inflight_out_single(struct mosquitto *context, struct mosquitto_client_msg *msg) { mosquitto_property *base_msg_props = NULL; @@ -1392,7 +1426,7 @@ int db__message_write_queued_in(struct mosquitto *context) } DL_FOREACH_SAFE(context->msgs_in.queued, tail, tmp){ - if(context->msgs_out.inflight_maximum != 0 && context->msgs_in.inflight_quota == 0){ + if(context->msgs_in.inflight_maximum != 0 && context->msgs_in.inflight_quota == 0){ break; } diff --git a/src/handle_connack.c b/src/handle_connack.c index 4906ffb5..925084ac 100644 --- a/src/handle_connack.c +++ b/src/handle_connack.c @@ -175,7 +175,7 @@ int handle__connack(struct mosquitto *context) log__printf(NULL, MOSQ_LOG_ERR, "Connection Refused: broker unavailable"); return MOSQ_ERR_CONN_LOST; case CONNACK_REFUSED_BAD_USERNAME_PASSWORD: - log__printf(NULL, MOSQ_LOG_ERR, "Connection Refused: broker unavailable"); + log__printf(NULL, MOSQ_LOG_ERR, "Connection Refused: bad user name or password"); return MOSQ_ERR_CONN_LOST; case CONNACK_REFUSED_NOT_AUTHORIZED: log__printf(NULL, MOSQ_LOG_ERR, "Connection Refused: not authorised"); diff --git a/src/handle_connect.c b/src/handle_connect.c index 18ed4679..cc89d58a 100644 --- a/src/handle_connect.c +++ b/src/handle_connect.c @@ -334,6 +334,7 @@ int connect__on_authorised(struct mosquitto *context, void *auth_data_out, uint1 rc = send__connack(context, connect_ack, CONNACK_ACCEPTED, connack_props); mosquitto_property_free_all(&connack_props); if(rc) return rc; + db__expire_all_messages(context); rc = db__message_write_queued_out(context); if(rc) return rc; rc = db__message_write_inflight_out_all(context); @@ -600,9 +601,6 @@ int handle__connect(struct mosquitto *context) rc = MOSQ_ERR_PROTOCOL; goto handle_connect_error; } - if(context->in_packet.command != CMD_CONNECT){ - return MOSQ_ERR_MALFORMED_PACKET; - } /* Read protocol name as length then bytes rather than with read_string * because the length is fixed and we can check that. Removes the need @@ -681,6 +679,9 @@ int handle__connect(struct mosquitto *context) rc = MOSQ_ERR_PROTOCOL; goto handle_connect_error; } + if((protocol_version&0x7F) != PROTOCOL_VERSION_v31 && context->in_packet.command != CMD_CONNECT){ + return MOSQ_ERR_MALFORMED_PACKET; + } if(packet__read_byte(&context->in_packet, &connect_flags)){ rc = MOSQ_ERR_PROTOCOL; diff --git a/src/mosquitto_broker_internal.h b/src/mosquitto_broker_internal.h index 467fc0aa..796a780c 100644 --- a/src/mosquitto_broker_internal.h +++ b/src/mosquitto_broker_internal.h @@ -753,6 +753,7 @@ int db__message_write_queued_in(struct mosquitto *context); void db__msg_add_to_inflight_stats(struct mosquitto_msg_data *msg_data, struct mosquitto_client_msg *msg); void db__msg_add_to_queued_stats(struct mosquitto_msg_data *msg_data, struct mosquitto_client_msg *msg); uint64_t db__new_msg_id(void); +void db__expire_all_messages(struct mosquitto *context); /* ============================================================ * Subscription functions @@ -933,6 +934,7 @@ void unpwd__free_item(struct mosquitto__unpwd **unpwd, struct mosquitto__unpwd * * Session expiry * ============================================================ */ int session_expiry__add(struct mosquitto *context); +int session_expiry__add_from_persistence(struct mosquitto *context, time_t expiry_time); void session_expiry__remove(struct mosquitto *context); void session_expiry__remove_all(void); void session_expiry__check(void); diff --git a/src/net.c b/src/net.c index cb75ce9b..f79b7ebc 100644 --- a/src/net.c +++ b/src/net.c @@ -19,15 +19,16 @@ Contributors: #include "config.h" #ifndef WIN32 -#include -#include -#include -#include -#include -#include +# include +# include +# include +# include +# include +# include +# include #else -#include -#include +# include +# include #endif #include @@ -36,7 +37,7 @@ Contributors: #include #include #ifdef WITH_WRAP -#include +# include #endif #ifdef HAVE_NETINET_IN_H @@ -49,7 +50,7 @@ Contributors: #endif #ifdef __QNX__ -#include +# include #endif #include "mosquitto_broker_internal.h" @@ -59,8 +60,8 @@ Contributors: #include "util_mosq.h" #ifdef WITH_TLS -#include "tls_mosq.h" -#include +# include "tls_mosq.h" +# include static int tls_ex_index_context = -1; static int tls_ex_index_listener = -1; #endif @@ -611,7 +612,7 @@ int net__tls_load_verify(struct mosquitto__listener *listener) #ifdef WITH_TLS int rc; -#if OPENSSL_VERSION_NUMBER < 0x30000000L +# if OPENSSL_VERSION_NUMBER < 0x30000000L if(listener->cafile || listener->capath){ rc = SSL_CTX_load_verify_locations(listener->ssl_ctx, listener->cafile, listener->capath); if(rc == 0){ @@ -624,7 +625,7 @@ int net__tls_load_verify(struct mosquitto__listener *listener) } } } -#else +# else if(listener->cafile){ rc = SSL_CTX_load_verify_file(listener->ssl_ctx, listener->cafile); if(rc == 0){ @@ -641,11 +642,13 @@ int net__tls_load_verify(struct mosquitto__listener *listener) return MOSQ_ERR_TLS; } } -#endif +# endif +# if !defined(OPENSSL_NO_ENGINE) if(net__load_engine(listener)){ return MOSQ_ERR_TLS; } +# endif #endif return net__load_certificates(listener); } diff --git a/src/persist_read.c b/src/persist_read.c index ac56b3b8..7e189361 100644 --- a/src/persist_read.c +++ b/src/persist_read.c @@ -208,7 +208,7 @@ static int persist__client_chunk_restore(FILE *db_fptr) } } } - /* FIXME - we should expire clients here if they have exceeded their time */ + session_expiry__add_from_persistence(context, chunk.F.session_expiry_time); }else{ rc = 1; } diff --git a/src/persist_write.c b/src/persist_write.c index eb429782..eec74ee0 100644 --- a/src/persist_write.c +++ b/src/persist_write.c @@ -164,8 +164,17 @@ static int persist__client_save(FILE *db_fptr) memset(&chunk, 0, sizeof(struct P_client)); HASH_ITER(hh_id, db.contexts_by_id, context, ctxt_tmp){ - if(context && context->clean_start == false){ + if(context && (context->clean_start == false +#ifdef WITH_BRIDGE + || (context->bridge && context->bridge->clean_start_local == false) +#endif + )){ chunk.F.session_expiry_time = context->session_expiry_time; + if(context->session_expiry_interval != 0 && context->session_expiry_interval != UINT32_MAX && context->session_expiry_time == 0){ + chunk.F.session_expiry_time = context->session_expiry_interval + db.now_real_s; + }else{ + chunk.F.session_expiry_time = context->session_expiry_time; + } chunk.F.session_expiry_interval = context->session_expiry_interval; chunk.F.last_mid = context->last_mid; chunk.F.id_len = (uint16_t)strlen(context->id); diff --git a/src/session_expiry.c b/src/session_expiry.c index 4e3ea001..9f1cd86f 100644 --- a/src/session_expiry.c +++ b/src/session_expiry.c @@ -84,6 +84,31 @@ int session_expiry__add(struct mosquitto *context) } +int session_expiry__add_from_persistence(struct mosquitto *context, time_t expiry_time) +{ + struct session_expiry_list *item; + + if(db.config->persistent_client_expiration == 0){ + if(context->session_expiry_interval == UINT32_MAX){ + /* There isn't a global expiry set, and the client has asked to + * never expire, so we don't add it to the list. */ + return MOSQ_ERR_SUCCESS; + } + } + + item = mosquitto__calloc(1, sizeof(struct session_expiry_list)); + if(!item) return MOSQ_ERR_NOMEM; + + item->context = context; + item->context->session_expiry_time = expiry_time; + context->expiry_list_item = item; + + DL_INSERT_INORDER(expiry_list, item, session_expiry__cmp); + + return MOSQ_ERR_SUCCESS; +} + + void session_expiry__remove(struct mosquitto *context) { if(context->expiry_list_item){ diff --git a/src/websockets.c b/src/websockets.c index 03cd49ea..20e3f345 100644 --- a/src/websockets.c +++ b/src/websockets.c @@ -136,6 +136,7 @@ static int callback_mqtt( uint8_t *buf; int rc; uint8_t byte; + char ip_addr_buff[1024]; switch (reason) { case LWS_CALLBACK_ESTABLISHED: @@ -160,7 +161,12 @@ static int callback_mqtt( }else{ return -1; } - easy_address(lws_get_socket_fd(wsi), mosq); + + if (lws_hdr_copy(wsi, ip_addr_buff, sizeof(ip_addr_buff), WSI_TOKEN_X_FORWARDED_FOR) > 0) { + mosq->address = mosquitto__strdup(ip_addr_buff); + } else { + easy_address(lws_get_socket_fd(wsi), mosq); + } if(!mosq->address){ /* getpeername and inet_ntop failed and not a bridge */ mosquitto__FREE(mosq); @@ -246,7 +252,7 @@ static int callback_mqtt( #ifdef WITH_SYS_TREE g_msgs_sent++; - if(((packet->command)&0xF6) == CMD_PUBLISH){ + if(((packet->command)&0xF0) == CMD_PUBLISH){ g_pub_msgs_sent++; } #endif @@ -331,7 +337,7 @@ static int callback_mqtt( #ifdef WITH_SYS_TREE G_MSGS_RECEIVED_INC(1); - if(((mosq->in_packet.command)&0xF5) == CMD_PUBLISH){ + if(((mosq->in_packet.command)&0xF0) == CMD_PUBLISH){ G_PUB_MSGS_RECEIVED_INC(1); } #endif diff --git a/test/broker/08-ssl-bridge.py b/test/broker/08-ssl-bridge.py index 9923a2a8..de765254 100755 --- a/test/broker/08-ssl-bridge.py +++ b/test/broker/08-ssl-bridge.py @@ -36,13 +36,9 @@ publish_packet = mosq_test.gen_publish("bridge/ssl/test", qos=0, payload="messag sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -ssock = ssl.wrap_socket( - sock, - ca_certs=ssl_dir / "all-ca.crt", - keyfile=ssl_dir / "server.key", - certfile=ssl_dir / "server.crt", - server_side=True -) +context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=f"{ssl_dir}/all-ca.crt") +context.load_cert_chain(certfile=f"{ssl_dir}/server.crt", keyfile=f"{ssl_dir}/server.key") +ssock = context.wrap_socket(sock, server_side=True) ssock.settimeout(20) ssock.bind(('', port1)) ssock.listen(5) diff --git a/test/broker/08-ssl-connect-cert-auth-crl.py b/test/broker/08-ssl-connect-cert-auth-crl.py index 85a89f08..6834d490 100755 --- a/test/broker/08-ssl-connect-cert-auth-crl.py +++ b/test/broker/08-ssl-connect-cert-auth-crl.py @@ -30,7 +30,9 @@ broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", certfile=f"{ssl_dir}/client.crt", keyfile=f"{ssl_dir}/client.key", cert_reqs=ssl.CERT_REQUIRED) + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-root-ca.crt") + context.load_cert_chain(certfile=f"{ssl_dir}/client.crt", keyfile=f"{ssl_dir}/client.key") + ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-connect-cert-auth-expired.py b/test/broker/08-ssl-connect-cert-auth-expired.py index 14e6225e..8a9d3d65 100755 --- a/test/broker/08-ssl-connect-cert-auth-expired.py +++ b/test/broker/08-ssl-connect-cert-auth-expired.py @@ -30,7 +30,9 @@ broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", certfile=f"{ssl_dir}/client-expired.crt", keyfile=f"{ssl_dir}/client-expired.key", cert_reqs=ssl.CERT_REQUIRED) + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-root-ca.crt") + context.load_cert_chain(certfile=f"{ssl_dir}/client-expired.crt", keyfile=f"{ssl_dir}/client-expired.key") + ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) try: ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-connect-cert-auth-revoked.py b/test/broker/08-ssl-connect-cert-auth-revoked.py index f7057c5b..ed3ee0de 100755 --- a/test/broker/08-ssl-connect-cert-auth-revoked.py +++ b/test/broker/08-ssl-connect-cert-auth-revoked.py @@ -29,7 +29,9 @@ broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", certfile=f"{ssl_dir}/client-revoked.crt", keyfile=f"{ssl_dir}/client-revoked.key", cert_reqs=ssl.CERT_REQUIRED) + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-root-ca.crt") + context.load_cert_chain(certfile=f"{ssl_dir}/client-revoked.crt", keyfile=f"{ssl_dir}/client-revoked.key") + ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) try: ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-connect-cert-auth-without.py b/test/broker/08-ssl-connect-cert-auth-without.py index 7377e1bf..666af7d6 100755 --- a/test/broker/08-ssl-connect-cert-auth-without.py +++ b/test/broker/08-ssl-connect-cert-auth-without.py @@ -27,7 +27,8 @@ connect_packet = mosq_test.gen_connect("connect-cert-test") broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, use_conf=True) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", cert_reqs=ssl.CERT_REQUIRED) +context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH) +ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) try: ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-connect-cert-auth.py b/test/broker/08-ssl-connect-cert-auth.py index bd8c93fb..c60d950e 100755 --- a/test/broker/08-ssl-connect-cert-auth.py +++ b/test/broker/08-ssl-connect-cert-auth.py @@ -31,7 +31,9 @@ broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", certfile=f"{ssl_dir}/client.crt", keyfile=f"{ssl_dir}/client.key", cert_reqs=ssl.CERT_REQUIRED) + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-root-ca.crt") + context.load_cert_chain(certfile=f"{ssl_dir}/client.crt", keyfile=f"{ssl_dir}/client.key") + ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-connect-identity.py b/test/broker/08-ssl-connect-identity.py index bd1e76d2..0b1ec180 100755 --- a/test/broker/08-ssl-connect-identity.py +++ b/test/broker/08-ssl-connect-identity.py @@ -32,7 +32,9 @@ broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", certfile=f"{ssl_dir}/client.crt", keyfile=f"{ssl_dir}/client.key", cert_reqs=ssl.CERT_REQUIRED) + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-root-ca.crt") + context.load_cert_chain(certfile=f"{ssl_dir}/client.crt", keyfile=f"{ssl_dir}/client.key") + ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-connect-no-auth-wrong-ca.py b/test/broker/08-ssl-connect-no-auth-wrong-ca.py index 52a71b80..b57f467f 100755 --- a/test/broker/08-ssl-connect-no-auth-wrong-ca.py +++ b/test/broker/08-ssl-connect-no-auth-wrong-ca.py @@ -28,7 +28,8 @@ connack_packet = mosq_test.gen_connack(rc=0) broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, use_conf=True) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) -ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-alt-ca.crt", cert_reqs=ssl.CERT_REQUIRED) +context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-alt-ca.crt") +ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) try: ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-connect-no-auth.py b/test/broker/08-ssl-connect-no-auth.py index c9134132..8d94bbcb 100755 --- a/test/broker/08-ssl-connect-no-auth.py +++ b/test/broker/08-ssl-connect-no-auth.py @@ -31,7 +31,8 @@ broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", cert_reqs=ssl.CERT_REQUIRED) + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-root-ca.crt") + ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-connect-no-identity.py b/test/broker/08-ssl-connect-no-identity.py index 1573c609..631dd3f1 100755 --- a/test/broker/08-ssl-connect-no-identity.py +++ b/test/broker/08-ssl-connect-no-identity.py @@ -31,7 +31,8 @@ broker = mosq_test.start_broker(filename=os.path.basename(__file__), port=port2, try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", cert_reqs=ssl.CERT_REQUIRED) + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-root-ca.crt") + ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) ssock.connect(("localhost", port1)) diff --git a/test/broker/08-ssl-hup-disconnect.py b/test/broker/08-ssl-hup-disconnect.py index 8de8c500..1fbfac20 100755 --- a/test/broker/08-ssl-hup-disconnect.py +++ b/test/broker/08-ssl-hup-disconnect.py @@ -42,7 +42,9 @@ def do_test(option): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/test-root-ca.crt", certfile=f"{ssl_dir}/client.crt", keyfile=f"{ssl_dir}/client.key", cert_reqs=ssl.CERT_REQUIRED) + context = ssl.create_default_context(ssl.Purpose.SERVER_AUTH, cafile=f"{ssl_dir}/test-root-ca.crt") + context.load_cert_chain(certfile=f"{ssl_dir}/client.crt", keyfile=f"{ssl_dir}/client.key") + ssock = context.wrap_socket(sock, server_hostname="localhost") ssock.settimeout(20) ssock.connect(("localhost", port)) mosq_test.do_send_receive(ssock, connect_packet, connack_packet, "connack") diff --git a/test/broker/09-plugin-tick.py b/test/broker/09-plugin-tick.py new file mode 100755 index 00000000..f6af1dbd --- /dev/null +++ b/test/broker/09-plugin-tick.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +# Test whether a plugin can subscribe to the tick event + +from mosq_test_helper import * + +def write_config(filename, port, per_listener_settings="false"): + with open(filename, 'w') as f: + f.write("per_listener_settings %s\n" % (per_listener_settings)) + f.write("listener %d\n" % (port)) + f.write("plugin c/auth_plugin_v5_handle_tick.so\n") + f.write("allow_anonymous true\n") + +def do_test(per_listener_settings): + proto_ver = 5 + port = mosq_test.get_port() + conf_file = os.path.basename(__file__).replace('.py', '.conf') + write_config(conf_file, port, per_listener_settings) + + rc = 1 + keepalive = 10 + connect_packet = mosq_test.gen_connect("plugin-tick-test", keepalive=keepalive, username="readwrite", clean_session=False, proto_ver=proto_ver) + connack_packet = mosq_test.gen_connack(rc=0, proto_ver=proto_ver) + + tick_packet = mosq_test.gen_publish("topic/tick", qos=0, payload="test-message", 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=10, port=port) + + mosq_test.expect_packet(sock, "tick message", tick_packet) + mosq_test.expect_packet(sock, "tick message", tick_packet) + mosq_test.expect_packet(sock, "tick message", tick_packet) + + mosq_test.do_ping(sock) + + 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("false") +do_test("true") diff --git a/test/broker/14-dynsec-anon-group.py b/test/broker/14-dynsec-anon-group.py index dcb7f470..a5376e4d 100755 --- a/test/broker/14-dynsec-anon-group.py +++ b/test/broker/14-dynsec-anon-group.py @@ -78,6 +78,15 @@ delete_anon_group_response = {'responses': [ {'command': 'deleteGroup', "error":'Deleting the anonymous group is forbidden', 'correlationData': '40'} ]} +delete_anon_group_command = { "commands": [ + { "command": "deleteGroup", "groupname": "anon-clients", "correlationData": "40" } + ] +} +delete_anon_group_response = {'responses': [ + {'command': 'deleteGroup', "error":'Deleting the anonymous group is forbidden', 'correlationData': '40'} + ]} + + rc = 1 diff --git a/test/broker/Makefile b/test/broker/Makefile index 4248c6e2..89214cc5 100644 --- a/test/broker/Makefile +++ b/test/broker/Makefile @@ -203,6 +203,7 @@ endif ./09-plugin-change-id.py ./09-plugin-delayed-auth.py ./09-plugin-publish.py + ./09-plugin-tick.py ./09-pwfile-parse-invalid.py 10 : diff --git a/test/broker/c/Makefile b/test/broker/c/Makefile index e35ffce0..c65d66f6 100644 --- a/test/broker/c/Makefile +++ b/test/broker/c/Makefile @@ -23,6 +23,7 @@ PLUGIN_SRC = \ auth_plugin_v5.c \ auth_plugin_v5_control.c \ auth_plugin_v5_handle_message.c \ + auth_plugin_v5_handle_tick.c \ plugin_control.c PLUGINS = ${PLUGIN_SRC:.c=.so} diff --git a/test/broker/c/auth_plugin_v5_handle_tick.c b/test/broker/c/auth_plugin_v5_handle_tick.c new file mode 100644 index 00000000..d6435bb0 --- /dev/null +++ b/test/broker/c/auth_plugin_v5_handle_tick.c @@ -0,0 +1,38 @@ +#include +#include +#include +#include +#include +#include + +static int handle_tick(int event, void *event_data, void *user_data); + +static mosquitto_plugin_id_t *plg_id; + + +int mosquitto_plugin_version(int supported_version_count, const int *supported_versions) +{ + return 5; +} + +int mosquitto_plugin_init(mosquitto_plugin_id_t *identifier, void **user_data, struct mosquitto_opt *auth_opts, int auth_opt_count) +{ + plg_id = identifier; + + mosquitto_callback_register(plg_id, MOSQ_EVT_TICK, handle_tick, NULL, NULL); + + return MOSQ_ERR_SUCCESS; +} + +int mosquitto_plugin_cleanup(void *user_data, struct mosquitto_opt *auth_opts, int auth_opt_count) +{ + mosquitto_callback_unregister(plg_id, MOSQ_EVT_TICK, handle_tick, NULL); + + return MOSQ_ERR_SUCCESS; +} + +int handle_tick(int event, void *event_data, void *user_data) +{ + mosquitto_broker_publish_copy("plugin-tick-test", "topic/tick", strlen("test-message"), "test-message", 0, false, NULL); + return MOSQ_ERR_SUCCESS; +} diff --git a/test/broker/data/CONNECT.json b/test/broker/data/CONNECT.json index 2cc5000f..54d24fb0 100644 --- a/test/broker/data/CONNECT.json +++ b/test/broker/data/CONNECT.json @@ -3,6 +3,14 @@ "comment": "CONNECT TESTS ARE INCOMPLETE", "group": "v3.1 CONNECT", "tests": [ + { "name": "10 ok ", "connect":false, "expect_disconnect":false, "msgs":[ + {"type":"send", "payload":"10 0F 0006 4D5149736470 03 01 000A 0001 70", "comment":"minimal valid CONNECT"}, + {"type":"recv", "payload":"20 02 00 00", "comment": "CONNACK"} + ]}, + { "name": "14 ok ", "connect":false, "expect_disconnect":false, "msgs":[ + {"type":"send", "payload":"14 0F 0006 4D5149736470 03 01 000A 0001 70", "comment":"CONNECT with QoS=1"}, + {"type":"recv", "payload":"20 02 00 00", "comment": "CONNACK"} + ]}, { "name": "10 proto ver 2", "connect":false, "msgs":[ {"type":"send", "payload":"10 0F 0006 4D5149736470 02 00 000A 0001 70", "comment":"CONNECT"}, {"type":"recv", "payload":"20 03 00 84 00", "comment": "CONNACK identifier rejected"} diff --git a/test/broker/test.py b/test/broker/test.py index d1fe29d8..f1c7d272 100755 --- a/test/broker/test.py +++ b/test/broker/test.py @@ -172,6 +172,7 @@ tests = [ (1, './09-plugin-change-id.py'), (1, './09-plugin-delayed-auth.py'), (1, './09-plugin-publish.py'), + (1, './09-plugin-tick.py'), (1, './09-pwfile-parse-invalid.py'), (2, './10-listener-mount-point.py'), diff --git a/test/lib/08-ssl-connect-cert-auth-enc.py b/test/lib/08-ssl-connect-cert-auth-enc.py index afc45bd0..5debedf8 100755 --- a/test/lib/08-ssl-connect-cert-auth-enc.py +++ b/test/lib/08-ssl-connect-cert-auth-enc.py @@ -26,9 +26,10 @@ disconnect_packet = mosq_test.gen_disconnect() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/all-ca.crt", - keyfile=f"{ssl_dir}/server.key", certfile=f"{ssl_dir}/server.crt", - server_side=True, cert_reqs=ssl.CERT_REQUIRED) +context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=f"{ssl_dir}/all-ca.crt") +context.load_cert_chain(certfile=f"{ssl_dir}/server.crt", keyfile=f"{ssl_dir}/server.key") +context.verify_mode = ssl.CERT_REQUIRED +ssock = context.wrap_socket(sock, server_side=True) ssock.settimeout(10) ssock.bind(('', port)) ssock.listen(5) diff --git a/test/lib/08-ssl-connect-cert-auth.py b/test/lib/08-ssl-connect-cert-auth.py index 5ae510ab..7188132f 100755 --- a/test/lib/08-ssl-connect-cert-auth.py +++ b/test/lib/08-ssl-connect-cert-auth.py @@ -26,9 +26,10 @@ disconnect_packet = mosq_test.gen_disconnect() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/all-ca.crt", - keyfile=f"{ssl_dir}/server.key", certfile=f"{ssl_dir}/server.crt", - server_side=True, cert_reqs=ssl.CERT_REQUIRED) +context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=f"{ssl_dir}/all-ca.crt") +context.load_cert_chain(certfile=f"{ssl_dir}/server.crt", keyfile=f"{ssl_dir}/server.key") +context.verify_mode = ssl.CERT_REQUIRED +ssock = context.wrap_socket(sock, server_side=True) ssock.settimeout(10) ssock.bind(('', port)) ssock.listen(5) diff --git a/test/lib/08-ssl-connect-no-auth.py b/test/lib/08-ssl-connect-no-auth.py index 341dc86b..4587912b 100755 --- a/test/lib/08-ssl-connect-no-auth.py +++ b/test/lib/08-ssl-connect-no-auth.py @@ -25,7 +25,9 @@ disconnect_packet = mosq_test.gen_disconnect() sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -ssock = ssl.wrap_socket(sock, ca_certs=f"{ssl_dir}/all-ca.crt", keyfile=f"{ssl_dir}/server.key", certfile=f"{ssl_dir}/server.crt", server_side=True) +context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=f"{ssl_dir}/all-ca.crt") +context.load_cert_chain(certfile=f"{ssl_dir}/server.crt", keyfile=f"{ssl_dir}/server.key") +ssock = context.wrap_socket(sock, server_side=True) ssock.settimeout(10) ssock.bind(('', port)) ssock.listen(5) diff --git a/test/lib/08-ssl-fake-cacert.py b/test/lib/08-ssl-fake-cacert.py index 3d903704..e9290cef 100755 --- a/test/lib/08-ssl-fake-cacert.py +++ b/test/lib/08-ssl-fake-cacert.py @@ -10,14 +10,10 @@ if sys.version < '2.7': sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) -ssock = ssl.wrap_socket( - sock, - ca_certs=f"{ssl_dir}/all-ca.crt", - keyfile=f"{ssl_dir}/server.key", - certfile=f"{ssl_dir}/server.crt", - server_side=True, - cert_reqs=ssl.CERT_REQUIRED -) +context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=f"{ssl_dir}/all-ca.crt") +context.load_cert_chain(certfile=f"{ssl_dir}/server.crt", keyfile=f"{ssl_dir}/server.key") +context.verify_mode = ssl.CERT_REQUIRED +ssock = context.wrap_socket(sock, server_side=True) ssock.settimeout(10) ssock.bind(('', port)) ssock.listen(5) diff --git a/test/lib/Makefile b/test/lib/Makefile index 2405fb26..08b92c37 100644 --- a/test/lib/Makefile +++ b/test/lib/Makefile @@ -71,6 +71,8 @@ ifeq ($(WITH_TLS),yes) ./08-ssl-bad-cacert.py $@/08-ssl-bad-cacert.test ./08-ssl-connect-cert-auth-enc.py $@/08-ssl-connect-cert-auth-enc.test ./08-ssl-connect-cert-auth.py $@/08-ssl-connect-cert-auth.test + ./08-ssl-connect-cert-auth.py $@/08-ssl-connect-cert-auth-custom-ssl-ctx.test + ./08-ssl-connect-cert-auth.py $@/08-ssl-connect-cert-auth-custom-ssl-ctx-default.test ./08-ssl-connect-no-auth.py $@/08-ssl-connect-no-auth.test endif ./09-util-topic-tokenise.py $@/09-util-topic-tokenise.test diff --git a/test/lib/c/08-ssl-connect-cert-auth-custom-ssl-ctx-default.c b/test/lib/c/08-ssl-connect-cert-auth-custom-ssl-ctx-default.c new file mode 100644 index 00000000..f92fcb9c --- /dev/null +++ b/test/lib/c/08-ssl-connect-cert-auth-custom-ssl-ctx-default.c @@ -0,0 +1,67 @@ +#include +#include +#include +#include +#include +#include +#include + +static int run = -1; + +void handle_sigint(int signal) +{ + run = 0; +} + +void on_connect(struct mosquitto *mosq, void *obj, int rc) +{ + if(rc){ + exit(1); + }else{ + mosquitto_disconnect(mosq); + } +} + +void on_disconnect(struct mosquitto *mosq, void *obj, int rc) +{ + run = rc; +} + +int main(int argc, char *argv[]) +{ + int rc; + struct mosquitto *mosq; + SSL_CTX *ssl_ctx; + int port = atoi(argv[1]); + + mosquitto_lib_init(); + + OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS \ + | OPENSSL_INIT_ADD_ALL_DIGESTS \ + | OPENSSL_INIT_LOAD_CONFIG, NULL); + ssl_ctx = SSL_CTX_new(TLS_client_method()); + + mosq = mosquitto_new("08-ssl-connect-crt-auth", true, NULL); + if(mosq == NULL){ + return 1; + } + + mosquitto_int_option(mosq, MOSQ_OPT_SSL_CTX_WITH_DEFAULTS, 1); + mosquitto_void_option(mosq, MOSQ_OPT_SSL_CTX, ssl_ctx); + + mosquitto_tls_set(mosq, "../ssl/test-root-ca.crt", "../ssl/certs", "../ssl/client.crt", "../ssl/client.key", NULL); + mosquitto_connect_callback_set(mosq, on_connect); + mosquitto_disconnect_callback_set(mosq, on_disconnect); + + rc = mosquitto_connect(mosq, "localhost", port, 60); + + signal(SIGINT, handle_sigint); + while(run == -1){ + mosquitto_loop(mosq, -1, 1); + } + SSL_CTX_free(ssl_ctx); + mosquitto_destroy(mosq); + + mosquitto_lib_cleanup(); + return run; +} diff --git a/test/lib/c/08-ssl-connect-cert-auth-custom-ssl-ctx.c b/test/lib/c/08-ssl-connect-cert-auth-custom-ssl-ctx.c new file mode 100644 index 00000000..e0fe94ef --- /dev/null +++ b/test/lib/c/08-ssl-connect-cert-auth-custom-ssl-ctx.c @@ -0,0 +1,71 @@ +#include +#include +#include +#include +#include +#include +#include + +static int run = -1; + +void handle_sigint(int signal) +{ + run = 0; +} + +void on_connect(struct mosquitto *mosq, void *obj, int rc) +{ + if(rc){ + exit(1); + }else{ + mosquitto_disconnect(mosq); + } +} + +void on_disconnect(struct mosquitto *mosq, void *obj, int rc) +{ + run = rc; +} + +int main(int argc, char *argv[]) +{ + int rc; + struct mosquitto *mosq; + SSL_CTX *ssl_ctx; + int port = atoi(argv[1]); + + mosquitto_lib_init(); + + OPENSSL_init_crypto(OPENSSL_INIT_ADD_ALL_CIPHERS \ + | OPENSSL_INIT_ADD_ALL_DIGESTS \ + | OPENSSL_INIT_LOAD_CONFIG, NULL); + ssl_ctx = SSL_CTX_new(TLS_client_method()); + + SSL_CTX_set_verify(ssl_ctx, SSL_VERIFY_PEER, NULL); + SSL_CTX_use_certificate_chain_file(ssl_ctx, "../ssl/client.crt"); + SSL_CTX_use_PrivateKey_file(ssl_ctx, "../ssl/client.key", SSL_FILETYPE_PEM); + SSL_CTX_load_verify_locations(ssl_ctx, "../ssl/test-root-ca.crt", "../ssl/certs"); + + mosq = mosquitto_new("08-ssl-connect-crt-auth", true, NULL); + if(mosq == NULL){ + return 1; + } + mosquitto_tls_set(mosq, "../ssl/test-root-ca.crt", "../ssl/certs", "../ssl/client.crt", "../ssl/client.key", NULL); + mosquitto_connect_callback_set(mosq, on_connect); + mosquitto_disconnect_callback_set(mosq, on_disconnect); + + mosquitto_int_option(mosq, MOSQ_OPT_SSL_CTX_WITH_DEFAULTS, 0); + mosquitto_void_option(mosq, MOSQ_OPT_SSL_CTX, ssl_ctx); + + rc = mosquitto_connect(mosq, "localhost", port, 60); + + signal(SIGINT, handle_sigint); + while(run == -1){ + mosquitto_loop(mosq, -1, 1); + } + SSL_CTX_free(ssl_ctx); + mosquitto_destroy(mosq); + + mosquitto_lib_cleanup(); + return run; +} diff --git a/test/lib/c/Makefile b/test/lib/c/Makefile index 5c9b3d04..c86e502a 100644 --- a/test/lib/c/Makefile +++ b/test/lib/c/Makefile @@ -1,4 +1,6 @@ R=../../.. +include ${R}/config.mk + .PHONY: all clean reallyclean CFLAGS=-I${R}/include -I${R}/test -Werror @@ -61,6 +63,13 @@ SRC = \ 11-prop-send-payload-format.c \ 11-prop-send-content-type.c +ifeq ($(WITH_TLS),yes) +SRC += \ + 08-ssl-connect-cert-auth-custom-ssl-ctx.c \ + 08-ssl-connect-cert-auth-custom-ssl-ctx-default.c +LIBS += -lssl -lcrypto +endif + TESTS = ${SRC:.c=.test} all : ${TESTS} diff --git a/test/lib/test.py b/test/lib/test.py index 700f8c36..dc15f309 100755 --- a/test/lib/test.py +++ b/test/lib/test.py @@ -52,6 +52,8 @@ tests = [ (1, ['./08-ssl-bad-cacert.py', 'c/08-ssl-bad-cacert.test']), (1, ['./08-ssl-connect-cert-auth-enc.py', 'c/08-ssl-connect-cert-auth-enc.test']), (1, ['./08-ssl-connect-cert-auth.py', 'c/08-ssl-connect-cert-auth.test']), + (1, ['./08-ssl-connect-cert-auth.py', 'c/08-ssl-connect-cert-auth-custom-ssl-ctx.test']), + (1, ['./08-ssl-connect-cert-auth.py', 'c/08-ssl-connect-cert-auth-custom-ssl-ctx-default.test']), (1, ['./08-ssl-connect-no-auth.py', 'c/08-ssl-connect-no-auth.test']), (1, ['./09-util-topic-tokenise.py', 'c/09-util-topic-tokenise.test']), diff --git a/test/unit/persist_read_stubs.c b/test/unit/persist_read_stubs.c index 4f1da80e..6d3fbf22 100644 --- a/test/unit/persist_read_stubs.c +++ b/test/unit/persist_read_stubs.c @@ -250,3 +250,9 @@ void plugin_persist__queue_retain_event(struct mosquitto_base_msg *msg, int even UNUSED(msg); UNUSED(event); } +int session_expiry__add_from_persistence(struct mosquitto *context, time_t expiry_time) +{ + UNUSED(context); + UNUSED(expiry_time); + return 0; +} diff --git a/test/unit/persist_write_stubs.c b/test/unit/persist_write_stubs.c index 56100e22..c661b185 100644 --- a/test/unit/persist_write_stubs.c +++ b/test/unit/persist_write_stubs.c @@ -222,3 +222,10 @@ void plugin_persist__queue_retain_event(struct mosquitto_base_msg *msg, int even UNUSED(msg); UNUSED(event); } + +int session_expiry__add_from_persistence(struct mosquitto *context, time_t expiry_time) +{ + UNUSED(context); + UNUSED(expiry_time); + return 0; +} diff --git a/test/unit/subs_stubs.c b/test/unit/subs_stubs.c index ed7e0570..65f5ac25 100644 --- a/test/unit/subs_stubs.c +++ b/test/unit/subs_stubs.c @@ -231,3 +231,9 @@ void plugin_persist__handle_subscription_delete(struct mosquitto *context, const UNUSED(context); UNUSED(sub); } +int session_expiry__add_from_persistence(struct mosquitto *context, time_t expiry_time) +{ + UNUSED(context); + UNUSED(expiry_time); + return 0; +} diff --git a/www/pages/documentation.md b/www/pages/documentation.md index cc728dc8..a464b4a6 100644 --- a/www/pages/documentation.md +++ b/www/pages/documentation.md @@ -31,6 +31,15 @@ * [Using the snap package] - specific instructions on installing and configuring the Mosquitto snap package. * [Migrating from 1.x to 2.0] - details of changes needed to migrate to version 2.0. +# Third party + +These are some Mosquitto documentation hosted by third parties. + +* [Steve's internet guide] - a broad range of documentation and examples + covering Mosquitto and the Paho Python client, amongst others. +* [docs.cedalo.com] - includes documentation for both Mosquitto and Eclipse + Streamsheets + [mosquitto]:/man/mosquitto-8.html [mosquitto.conf]:/man/mosquitto-conf-5.html [mosquitto_passwd]:/man/mosquitto_passwd-1.html @@ -46,3 +55,6 @@ [Using the snap package]:/documentation/using-the-snap/ [Dynamic Security plugin]:/documentation/dynamic-security/ [Migrating from 1.x to 2.0]:/documentation/migrating-to-2-0/ + +[Steve's internet guide]: http://www.steves-internet-guide.com/ +[docs.cedalo.com]: https://docs.cedalo.com/ diff --git a/www/pages/documentation/dynamic-security.md b/www/pages/documentation/dynamic-security.md index 07f86830..7f16caa5 100644 --- a/www/pages/documentation/dynamic-security.md +++ b/www/pages/documentation/dynamic-security.md @@ -15,7 +15,7 @@ ## Introduction The Dynamic Security plugin is a Mosquitto plugin which provides role based -authentication and access control features that can updated whilst the broker +authentication and access control features that can be updated whilst the broker is running, using a special topic based API. It is supported since Mosquitto 2.0, and should be available in all @@ -464,9 +464,9 @@ admin username and any other options once and not have to add them to the command line every time. mosquitto_ctrl will try to load a configuration file from a default location. -For Windows this is at `%USER_PROFILE%\mosquitto_ctrl.conf`. For other systems, -it will try `$XDG_CONFIG_HOME/mosquitto_ctrl.conf` or -`$HOME/.config/mosquitto_ctrl.conf`. +For Windows this is at `%USER_PROFILE%\mosquitto_ctrl`. For other systems, +it will try `$XDG_CONFIG_HOME/mosquitto_ctrl` or +`$HOME/.config/mosquitto_ctrl`. You may override this behaviour by manually specifying an options file with `-o `. @@ -742,7 +742,7 @@ Where `acltype` is one of `publishClientSend`, `publishClientReceive`, For example: ``` -mosquitto_ctrl dynsec addRoleACL clientPublishSend client/topic allow 5 +mosquitto_ctrl dynsec addRoleACL publishClientSend client/topic allow 5 ``` To remove an ACL from a role using the topic filter as the key: @@ -752,7 +752,7 @@ mosquitto_ctrl dynsec removeRoleACL dynsec removeRoleACL clientPublishSend client/topic +mosquitto_ctrl dynsec removeRoleACL publishClientSend client/topic ``` To get information on a role: diff --git a/www/pages/index.html b/www/pages/index.html index 5842835c..00858e54 100644 --- a/www/pages/index.html +++ b/www/pages/index.html @@ -27,8 +27,8 @@ and mosquitto_sub command line MQTT clients.

Mosquitto is part of the Eclipse - Foundation, is an iot.eclipse.org - project and is sponsored by cedalo.com.

+ Foundation, and is an iot.eclipse.org + project. The development is driven by Cedalo.


@@ -37,43 +37,68 @@
-

Download

+

Download and Security

Mosquitto is highly portable and available for a wide range of platforms. Go to the dedicated download page to find the source or binaries for your platform.

-

Read the Change Log to find out +

Read the Change Log to find out about recent releases.

-
-
-

Test

-

You can have your own instance of Mosquitto running in - minutes, but to make testing even easier, the Mosquitto Project - runs a test server at test.mosquitto.org where - you can test your clients in a variety of ways: plain MQTT, - MQTT over TLS, MQTT over TLS (with client certificate), - MQTT over WebSockets and MQTT over WebSockets with TLS.

-
- -
-

Community

- - -
- -
-

Security

Use the security page to find out how to report vulnerabilities or responses to past security issues.

+ +
+

Test

+

You can have your own instance of Mosquitto running in + minutes, but to make testing even easier, the Mosquitto Project + runs a test server at + test.mosquitto.org where + you can test your clients in a variety of ways: plain MQTT, + MQTT over TLS, MQTT over TLS (with + client certificate), + MQTT over WebSockets and MQTT over WebSockets with TLS.

+
+
+ +
+
+

Community

+ +
+ +
+

Support

+

Support is always available from the community channels on a + best effort basis. If you require commercial support, + Cedalo can offer support for hosted + or on-premise instances, consulting on the use of Mosquitto, + and custom development to your needs.

+
+
+ +
+
+

Related Projects

+

Paho provides MQTT + client library implementations in a wide variety of + languages.

+

Streamsheets is an + easy to use web based real time spreadsheet interface that can + be used to process incoming data from a variety of sources, + such as MQTT, OPC-UA, and REST. Developers and non-developers + can use Streamsheets to control processes and build dashboards, + for example. Mosquitto is a core component of Streamsheets.

+
+
+
diff --git a/www/pages/security.md b/www/pages/security.md index 657e49ce..2f124cca 100644 --- a/www/pages/security.md +++ b/www/pages/security.md @@ -19,6 +19,9 @@ follow the steps on [Eclipse Security] page to report it. Listed with most recent first. Further information on security related issues can be found in the [security category]. +* August 2022: Deleting the anonymous group in the dynamic security plugin + could lead to a crash. Affecting versions **2.0.0** to **2.0.14** inclusive, + fixed in **2.0.15**. * August 2021: [CVE-2021-34434] Affecting versions **2.0.0** to **2.0.11** inclusive, fixed in **2.0.12**. * April 2021: [CVE-2021-28166] Affecting versions **2.0.0** to **2.0.9** diff --git a/www/posts/2022/08/version-2-0-15-released.md b/www/posts/2022/08/version-2-0-15-released.md new file mode 100644 index 00000000..2b59abce --- /dev/null +++ b/www/posts/2022/08/version-2-0-15-released.md @@ -0,0 +1,101 @@ + + +Versions 2.0.15 of Mosquitto has been released. This is a security +and bugfix release. + +# Security +- Deleting the group configured as the anonymous group in the Dynamic Security + plugin, would leave a dangling pointer that could lead to a single crash. + This is considered a minor issue - only administrative users should have + access to dynsec, the impact on availability is one-off, and there is no + associated loss of data. It is now forbidden to delete the group configured + as the anonymous group. + +# Broker +- Fix memory leak when a plugin modifies the topic of a message in + `MOSQ_EVT_MESSAGE`. +- Fix bridge `restart_timeout` not being honoured. +- Fix potential memory leaks if a plugin modifies the message in the + `MOSQ_EVT_MESSAGE` event. +- Fix unused flags in CONNECT command being forced to be 0, which is not + required for MQTT v3.1. Closes [#2522]. +- Improve documentation of `persistent_client_expiration` option. + Closes [#2404]. +- Add clients to session expiry check list when restarting and reloading from + persistence. Closes [#2546]. +- Fix bridges not sending failure notification messages to the local broker if + the remote bridge connection fails. Closes [#2467]. Closes [#1488]. +- Fix some PUBLISH messages not being counted in $SYS stats. Closes [#2448]. +- Fix incorrect return code being sent in DISCONNECT when a client session is + taken over. Closes [#2607]. +- Fix confusing "out of memory" error when a client is kicked in the dynamic + security plugin. Closes [#2525]. +- Fix confusing error message when dynamic security config file was a + directory. Closes [#2520]. +- Fix bridge queued messages not being persisted when local_cleansession is + set to false and cleansession is set to true. Closes [#2604]. +- Dynamic security: Fix modifyClient and modifyGroup commands to not modify + the client/group if a new group/client being added is not valid. + Closes [#2598]. +- Dynamic security: Fix the plugin being able to be loaded twice. Currently + only a single plugin can interact with a unique $CONTROL topic. Using + multiple instances of the plugin would produce duplicate entries in the + config file. Closes [#2601]. Closes [#2470]. +- Fix case where expired messages were causing queued messages not to be + delivered. Closes [#2609]. +- Fix websockets not passing on the X-Forwarded-For header. + +# Client library +- Fix threads library detection on Windows under cmake. Bumps the minimum + cmake version to 3.1, which is still ancient. +- Fix use of `MOSQ_OPT_TLS_ENGINE` being unable to be used due to the openssl + ctx not being initialised until starting to connect. Closes [#2537]. +- Fix incorrect use of SSL_connect. Closes [#2594]. +- Don't set SIGPIPE to ignore, use MSG_NOSIGNAL instead. Closes [#2564]. +- Add documentation of struct mosquitto_message to header. Closes [#2561]. +- Fix documentation omission around mosquitto_reinitialise. Closes [#2489]. +- Fix use of MOSQ_OPT_SSL_CTX when used in conjunction with + MOSQ_OPT_SSL_CTX_DEFAULTS. Closes [#2463]. +- Fix failure to close thread in some situations. Closes [#2545]. + +# Clients +- Fix mosquitto_pub incorrectly reusing topic aliases when reconnecting. + Closes [#2494]. + +# Apps +- Fix `-o` not working in `mosquitto_ctrl`, and typo in related documentation. + Closes [#2471]. + + +[#1488]: https://github.com/eclipse/mosquitto/issues/1488 +[#2404]: https://github.com/eclipse/mosquitto/issues/2404 +[#2448]: https://github.com/eclipse/mosquitto/issues/2448 +[#2463]: https://github.com/eclipse/mosquitto/issues/2463 +[#2467]: https://github.com/eclipse/mosquitto/issues/2467 +[#2470]: https://github.com/eclipse/mosquitto/issues/2470 +[#2471]: https://github.com/eclipse/mosquitto/issues/2471 +[#2489]: https://github.com/eclipse/mosquitto/issues/2489 +[#2494]: https://github.com/eclipse/mosquitto/issues/2494 +[#2520]: https://github.com/eclipse/mosquitto/issues/2520 +[#2522]: https://github.com/eclipse/mosquitto/issues/2522 +[#2525]: https://github.com/eclipse/mosquitto/issues/2525 +[#2537]: https://github.com/eclipse/mosquitto/issues/2537 +[#2545]: https://github.com/eclipse/mosquitto/issues/2545 +[#2546]: https://github.com/eclipse/mosquitto/issues/2546 +[#2561]: https://github.com/eclipse/mosquitto/issues/2561 +[#2564]: https://github.com/eclipse/mosquitto/issues/2564 +[#2594]: https://github.com/eclipse/mosquitto/issues/2594 +[#2598]: https://github.com/eclipse/mosquitto/issues/2598 +[#2601]: https://github.com/eclipse/mosquitto/issues/2601 +[#2604]: https://github.com/eclipse/mosquitto/issues/2604 +[#2607]: https://github.com/eclipse/mosquitto/issues/2607 +[#2609]: https://github.com/eclipse/mosquitto/issues/2609