summaryrefslogtreecommitdiffstats
path: root/src/aclk
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-07-24 09:54:23 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-07-24 09:54:44 +0000
commit836b47cb7e99a977c5a23b059ca1d0b5065d310e (patch)
tree1604da8f482d02effa033c94a84be42bc0c848c3 /src/aclk
parentReleasing debian version 1.44.3-2. (diff)
downloadnetdata-836b47cb7e99a977c5a23b059ca1d0b5065d310e.tar.xz
netdata-836b47cb7e99a977c5a23b059ca1d0b5065d310e.zip
Merging upstream version 1.46.3.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/aclk')
-rw-r--r--src/aclk/README.md133
-rw-r--r--src/aclk/aclk.c1353
-rw-r--r--src/aclk/aclk.h90
-rw-r--r--src/aclk/aclk_alarm_api.c44
-rw-r--r--src/aclk/aclk_alarm_api.h14
-rw-r--r--src/aclk/aclk_capas.c59
-rw-r--r--src/aclk/aclk_capas.h14
-rw-r--r--src/aclk/aclk_contexts_api.c41
-rw-r--r--src/aclk/aclk_contexts_api.h14
-rw-r--r--src/aclk/aclk_otp.c883
-rw-r--r--src/aclk/aclk_otp.h18
-rw-r--r--src/aclk/aclk_proxy.c177
-rw-r--r--src/aclk/aclk_proxy.h21
-rw-r--r--src/aclk/aclk_query.c373
-rw-r--r--src/aclk/aclk_query.h38
-rw-r--r--src/aclk/aclk_query_queue.c124
-rw-r--r--src/aclk/aclk_query_queue.h87
-rw-r--r--src/aclk/aclk_rrdhost_state.h11
-rw-r--r--src/aclk/aclk_rx_msgs.c571
-rw-r--r--src/aclk/aclk_rx_msgs.h17
-rw-r--r--src/aclk/aclk_stats.c483
-rw-r--r--src/aclk/aclk_stats.h77
-rw-r--r--src/aclk/aclk_tx_msgs.c276
-rw-r--r--src/aclk/aclk_tx_msgs.h20
-rw-r--r--src/aclk/aclk_util.c484
-rw-r--r--src/aclk/aclk_util.h121
-rw-r--r--src/aclk/helpers/mqtt_wss_pal.h19
-rw-r--r--src/aclk/helpers/ringbuffer_pal.h11
-rw-r--r--src/aclk/https_client.c866
-rw-r--r--src/aclk/https_client.h136
-rw-r--r--src/aclk/mqtt_websockets/.github/workflows/run-tests.yaml14
-rw-r--r--src/aclk/mqtt_websockets/.gitignore10
-rw-r--r--src/aclk/mqtt_websockets/README.md7
-rw-r--r--src/aclk/mqtt_websockets/c-rbuf/cringbuffer.c203
-rw-r--r--src/aclk/mqtt_websockets/c-rbuf/cringbuffer.h47
-rw-r--r--src/aclk/mqtt_websockets/c-rbuf/cringbuffer_internal.h37
-rw-r--r--src/aclk/mqtt_websockets/c-rbuf/ringbuffer_test.c485
-rw-r--r--src/aclk/mqtt_websockets/c_rhash/c_rhash.c264
-rw-r--r--src/aclk/mqtt_websockets/c_rhash/c_rhash.h61
-rw-r--r--src/aclk/mqtt_websockets/c_rhash/c_rhash_internal.h19
-rw-r--r--src/aclk/mqtt_websockets/c_rhash/tests.c273
-rw-r--r--src/aclk/mqtt_websockets/common_internal.h27
-rw-r--r--src/aclk/mqtt_websockets/common_public.c9
-rw-r--r--src/aclk/mqtt_websockets/common_public.h33
-rw-r--r--src/aclk/mqtt_websockets/endian_compat.h31
-rw-r--r--src/aclk/mqtt_websockets/mqtt_constants.h103
-rw-r--r--src/aclk/mqtt_websockets/mqtt_ng.c2237
-rw-r--r--src/aclk/mqtt_websockets/mqtt_ng.h99
-rw-r--r--src/aclk/mqtt_websockets/mqtt_wss_client.c1136
-rw-r--r--src/aclk/mqtt_websockets/mqtt_wss_client.h162
-rw-r--r--src/aclk/mqtt_websockets/mqtt_wss_log.c130
-rw-r--r--src/aclk/mqtt_websockets/mqtt_wss_log.h39
-rw-r--r--src/aclk/mqtt_websockets/test.c90
-rw-r--r--src/aclk/mqtt_websockets/ws_client.c744
-rw-r--r--src/aclk/mqtt_websockets/ws_client.h120
-rw-r--r--src/aclk/schema-wrappers/agent_cmds.cc38
-rw-r--r--src/aclk/schema-wrappers/agent_cmds.h27
-rw-r--r--src/aclk/schema-wrappers/alarm_config.cc140
-rw-r--r--src/aclk/schema-wrappers/alarm_config.h71
-rw-r--r--src/aclk/schema-wrappers/alarm_stream.cc221
-rw-r--r--src/aclk/schema-wrappers/alarm_stream.h128
-rw-r--r--src/aclk/schema-wrappers/capability.cc11
-rw-r--r--src/aclk/schema-wrappers/capability.h24
-rw-r--r--src/aclk/schema-wrappers/connection.cc72
-rw-r--r--src/aclk/schema-wrappers/connection.h47
-rw-r--r--src/aclk/schema-wrappers/context.cc125
-rw-r--r--src/aclk/schema-wrappers/context.h53
-rw-r--r--src/aclk/schema-wrappers/context_stream.cc42
-rw-r--r--src/aclk/schema-wrappers/context_stream.h36
-rw-r--r--src/aclk/schema-wrappers/node_connection.cc46
-rw-r--r--src/aclk/schema-wrappers/node_connection.h32
-rw-r--r--src/aclk/schema-wrappers/node_creation.cc39
-rw-r--r--src/aclk/schema-wrappers/node_creation.h31
-rw-r--r--src/aclk/schema-wrappers/node_info.cc136
-rw-r--r--src/aclk/schema-wrappers/node_info.h79
-rw-r--r--src/aclk/schema-wrappers/proto_2_json.cc88
-rw-r--r--src/aclk/schema-wrappers/proto_2_json.h18
-rw-r--r--src/aclk/schema-wrappers/schema_wrapper_utils.cc22
-rw-r--r--src/aclk/schema-wrappers/schema_wrapper_utils.h24
-rw-r--r--src/aclk/schema-wrappers/schema_wrappers.h19
80 files changed, 14524 insertions, 0 deletions
diff --git a/src/aclk/README.md b/src/aclk/README.md
new file mode 100644
index 000000000..0a260868c
--- /dev/null
+++ b/src/aclk/README.md
@@ -0,0 +1,133 @@
+# Agent-Cloud link (ACLK)
+
+The Agent-Cloud link (ACLK) is the mechanism responsible for securely connecting a Netdata Agent to your web browser
+through Netdata Cloud. The ACLK establishes an outgoing secure WebSocket (WSS) connection to Netdata Cloud on port
+`443`. The ACLK is encrypted, safe, and _is only established if you connect your node_.
+
+The Cloud App lives at app.netdata.cloud which currently resolves to the following list of IPs:
+
+- 54.198.178.11
+- 44.207.131.212
+- 44.196.50.41
+
+> ### Caution
+>
+>This list of IPs can change without notice, we strongly advise you to whitelist following domains `app.netdata.cloud`, `mqtt.netdata.cloud`, if this is not an option in your case always verify the current domain resolution (e.g via the `host` command).
+
+For a guide to connecting a node using the ACLK, plus additional troubleshooting and reference information, read our [connect to Cloud
+documentation](/src/claim/README.md).
+
+## Data privacy
+
+[Data privacy](https://netdata.cloud/privacy/) is very important to us. We firmly believe that your data belongs to
+you. This is why **we don't store any metric data in Netdata Cloud**.
+
+All the data that you see in the web browser when using Netdata Cloud, is actually streamed directly from the Netdata Agent to the Netdata Cloud dashboard. The data passes through our systems, but it isn't stored.
+
+However, to be able to offer the stunning visualizations and advanced functionality of Netdata Cloud, it does store a limited number of _metadata_. Read more about our [security and privacy design](/docs/security-and-privacy-design/README.md).
+
+## Enable and configure the ACLK
+
+The ACLK is enabled by default, with its settings automatically configured and stored in the Agent's memory. No file is
+created at `/var/lib/netdata/cloud.d/cloud.conf` until you either connect a node or create it yourself. The default
+configuration uses two settings:
+
+```conf
+[global]
+ enabled = yes
+ cloud base url = https://app.netdata.cloud
+```
+
+If your Agent needs to use a proxy to access the internet, you must [set up a proxy for
+connecting to cloud](/src/claim/README.md#connect-through-a-proxy).
+
+You can configure following keys in the `netdata.conf` section `[cloud]`:
+```
+[cloud]
+ statistics = yes
+ query thread count = 2
+```
+
+- `statistics` enables/disables ACLK related statistics and their charts. You can disable this to save some space in the database and slightly reduce memory usage of Netdata Agent.
+- `query thread count` specifies the number of threads to process cloud queries. Increasing this setting is useful for nodes with many children (streaming), which can expect to handle more queries (and/or more complicated queries).
+
+## Disable the ACLK
+
+You have two options if you prefer to disable the ACLK and not use Netdata Cloud.
+
+### Disable at installation
+
+You can pass the `--disable-cloud` parameter to the Agent installation when using a kickstart script
+([kickstart.sh](/packaging/installer/methods/kickstart.md), or a [manual installation from
+Git](/packaging/installer/methods/manual.md).
+
+When you pass this parameter, the installer does not download or compile any extra libraries. Once running, the Agent
+kills the thread responsible for the ACLK and connecting behavior, and behaves as though the ACLK, and thus Netdata Cloud,
+does not exist.
+
+### Disable at runtime
+
+You can change a runtime setting in your `cloud.conf` file to disable the ACLK. This setting only stops the Agent from
+attempting any connection via the ACLK, but does not prevent the installer from downloading and compiling the ACLK's
+dependencies.
+
+The file typically exists at `/var/lib/netdata/cloud.d/cloud.conf`, but can change if you set a prefix during
+installation. To disable the ACLK, open that file and change the `enabled` setting to `no`:
+
+```conf
+[global]
+ enabled = no
+```
+
+If the file at `/var/lib/netdata/cloud.d/cloud.conf` doesn't exist, you need to create it.
+
+Copy and paste the first two lines from below, which will change your prompt to `cat`.
+
+```bash
+cd /var/lib/netdata/cloud.d
+cat > cloud.conf << EOF
+```
+
+Copy and paste in lines 3-6, and after the final `EOF`, hit **Enter**. The final line must contain only `EOF`. Hit **Enter** again to return to your normal prompt with the newly-created file.
+
+To get your normal prompt back, the final line
+must contain only `EOF`.
+
+```bash
+[global]
+ enabled = no
+ cloud base url = https://app.netdata.cloud
+EOF
+```
+
+You also need to change the file's permissions. Use `grep "run as user" /etc/netdata/netdata.conf` to figure out which
+user your Agent runs as (typically `netdata`), and replace `netdata:netdata` as shown below if necessary:
+
+```bash
+sudo chmod 0770 cloud.conf
+sudo chown netdata:netdata cloud.conf
+```
+
+Restart your Agent to disable the ACLK.
+
+### Re-enable the ACLK
+
+If you first disable the ACLK and any Cloud functionality and then decide you would like to use Cloud, you must either
+[reinstall Netdata](/packaging/installer/REINSTALL.md) with Cloud enabled or change the runtime setting in your
+`cloud.conf` file.
+
+If you passed `--disable-cloud` to `netdata-installer.sh` during installation, you must
+[reinstall](/packaging/installer/REINSTALL.md) your Agent. Use the same method as before, but pass `--require-cloud` to
+the installer. When installation finishes you can [connect your node](/src/claim/README.md#how-to-connect-a-node).
+
+If you changed the runtime setting in your `var/lib/netdata/cloud.d/cloud.conf` file, edit the file again and change
+`enabled` to `yes`:
+
+```conf
+[global]
+ enabled = yes
+```
+
+Restart your Agent and [connect your node](/src/claim/README.md#how-to-connect-a-node).
+
+
diff --git a/src/aclk/aclk.c b/src/aclk/aclk.c
new file mode 100644
index 000000000..991745491
--- /dev/null
+++ b/src/aclk/aclk.c
@@ -0,0 +1,1353 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk.h"
+
+#ifdef ENABLE_ACLK
+#include "aclk_stats.h"
+#include "mqtt_websockets/mqtt_wss_client.h"
+#include "aclk_otp.h"
+#include "aclk_tx_msgs.h"
+#include "aclk_query.h"
+#include "aclk_query_queue.h"
+#include "aclk_util.h"
+#include "aclk_rx_msgs.h"
+#include "https_client.h"
+#include "schema-wrappers/schema_wrappers.h"
+#include "aclk_capas.h"
+
+#include "aclk_proxy.h"
+
+#ifdef ACLK_LOG_CONVERSATION_DIR
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#endif
+
+#define ACLK_STABLE_TIMEOUT 3 // Minimum delay to mark AGENT as stable
+
+#endif /* ENABLE_ACLK */
+
+int aclk_pubacks_per_conn = 0; // How many PubAcks we got since MQTT conn est.
+int aclk_rcvd_cloud_msgs = 0;
+int aclk_connection_counter = 0;
+int disconnect_req = 0;
+
+int aclk_connected = 0;
+int aclk_ctx_based = 0;
+int aclk_disable_runtime = 0;
+int aclk_stats_enabled;
+int aclk_kill_link = 0;
+
+usec_t aclk_session_us = 0;
+time_t aclk_session_sec = 0;
+
+time_t last_conn_time_mqtt = 0;
+time_t last_conn_time_appl = 0;
+time_t last_disconnect_time = 0;
+time_t next_connection_attempt = 0;
+float last_backoff_value = 0;
+
+time_t aclk_block_until = 0;
+
+#ifdef ENABLE_ACLK
+mqtt_wss_client mqttwss_client;
+
+netdata_mutex_t aclk_shared_state_mutex = NETDATA_MUTEX_INITIALIZER;
+#define ACLK_SHARED_STATE_LOCK netdata_mutex_lock(&aclk_shared_state_mutex)
+#define ACLK_SHARED_STATE_UNLOCK netdata_mutex_unlock(&aclk_shared_state_mutex)
+
+struct aclk_shared_state aclk_shared_state = {
+ .mqtt_shutdown_msg_id = -1,
+ .mqtt_shutdown_msg_rcvd = 0
+};
+
+#ifdef MQTT_WSS_DEBUG
+#include <openssl/ssl.h>
+#define DEFAULT_SSKEYLOGFILE_NAME "SSLKEYLOGFILE"
+const char *ssl_log_filename = NULL;
+FILE *ssl_log_file = NULL;
+static void aclk_ssl_keylog_cb(const SSL *ssl, const char *line)
+{
+ (void)ssl;
+ if (!ssl_log_file)
+ ssl_log_file = fopen(ssl_log_filename, "a");
+ if (!ssl_log_file) {
+ netdata_log_error("Couldn't open ssl_log file (%s) for append.", ssl_log_filename);
+ return;
+ }
+ fputs(line, ssl_log_file);
+ putc('\n', ssl_log_file);
+ fflush(ssl_log_file);
+}
+#endif
+
+#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_300
+OSSL_DECODER_CTX *aclk_dctx = NULL;
+EVP_PKEY *aclk_private_key = NULL;
+#else
+static RSA *aclk_private_key = NULL;
+#endif
+static int load_private_key()
+{
+ if (aclk_private_key != NULL) {
+#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_300
+ EVP_PKEY_free(aclk_private_key);
+ if (aclk_dctx)
+ OSSL_DECODER_CTX_free(aclk_dctx);
+
+ aclk_dctx = NULL;
+#else
+ RSA_free(aclk_private_key);
+#endif
+ }
+ aclk_private_key = NULL;
+ char filename[FILENAME_MAX + 1];
+ snprintfz(filename, FILENAME_MAX, "%s/cloud.d/private.pem", netdata_configured_varlib_dir);
+
+ long bytes_read;
+ char *private_key = read_by_filename(filename, &bytes_read);
+ if (!private_key) {
+ netdata_log_error("Claimed agent cannot establish ACLK - unable to load private key '%s' failed.", filename);
+ return 1;
+ }
+ netdata_log_debug(D_ACLK, "Claimed agent loaded private key len=%ld bytes", bytes_read);
+
+ BIO *key_bio = BIO_new_mem_buf(private_key, -1);
+ if (key_bio==NULL) {
+ netdata_log_error("Claimed agent cannot establish ACLK - failed to create BIO for key");
+ goto biofailed;
+ }
+
+#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_300
+ aclk_dctx = OSSL_DECODER_CTX_new_for_pkey(&aclk_private_key, "PEM", NULL,
+ "RSA",
+ OSSL_KEYMGMT_SELECT_PRIVATE_KEY,
+ NULL, NULL);
+
+ if (!aclk_dctx) {
+ netdata_log_error("Loading private key (from claiming) failed - no OpenSSL Decoders found");
+ goto biofailed;
+ }
+
+ // this is necesseary to avoid RSA key with wrong size
+ if (!OSSL_DECODER_from_bio(aclk_dctx, key_bio)) {
+ netdata_log_error("Decoding private key (from claiming) failed - invalid format.");
+ goto biofailed;
+ }
+#else
+ aclk_private_key = PEM_read_bio_RSAPrivateKey(key_bio, NULL, NULL, NULL);
+#endif
+ BIO_free(key_bio);
+ if (aclk_private_key!=NULL)
+ {
+ freez(private_key);
+ return 0;
+ }
+ char err[512];
+ ERR_error_string_n(ERR_get_error(), err, sizeof(err));
+ netdata_log_error("Claimed agent cannot establish ACLK - cannot create private key: %s", err);
+
+biofailed:
+ freez(private_key);
+ return 1;
+}
+
+static int wait_till_cloud_enabled()
+{
+ nd_log(NDLS_DAEMON, NDLP_INFO,
+ "Waiting for Cloud to be enabled");
+
+ while (!netdata_cloud_enabled) {
+ sleep_usec(USEC_PER_SEC * 1);
+ if (!service_running(SERVICE_ACLK))
+ return 1;
+ }
+ return 0;
+}
+
+/**
+ * Will block until agent is claimed. Returns only if agent claimed
+ * or if agent needs to shutdown.
+ *
+ * @return `0` if agent has been claimed,
+ * `1` if interrupted due to agent shutting down
+ */
+static int wait_till_agent_claimed(void)
+{
+ //TODO prevent malloc and freez
+ char *agent_id = get_agent_claimid();
+ while (likely(!agent_id)) {
+ sleep_usec(USEC_PER_SEC * 1);
+ if (!service_running(SERVICE_ACLK))
+ return 1;
+ agent_id = get_agent_claimid();
+ }
+ freez(agent_id);
+ return 0;
+}
+
+/**
+ * Checks everything is ready for connection
+ * agent claimed, cloud url set and private key available
+ *
+ * @param aclk_hostname points to location where string pointer to hostname will be set
+ * @param aclk_port port to int where port will be saved
+ *
+ * @return If non 0 returned irrecoverable error happened (or netdata_exit) and ACLK should be terminated
+ */
+static int wait_till_agent_claim_ready()
+{
+ url_t url;
+ while (service_running(SERVICE_ACLK)) {
+ if (wait_till_agent_claimed())
+ return 1;
+
+ // The NULL return means the value was never initialised, but this value has been initialized in post_conf_load.
+ // We trap the impossible NULL here to keep the linter happy without using a fatal() in the code.
+ char *cloud_base_url = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "cloud base url", NULL);
+ if (cloud_base_url == NULL) {
+ netdata_log_error("Do not move the cloud base url out of post_conf_load!!");
+ return 1;
+ }
+
+ // We just check configuration is valid here
+ // TODO make it without malloc/free
+ memset(&url, 0, sizeof(url_t));
+ if (url_parse(cloud_base_url, &url)) {
+ netdata_log_error("Agent is claimed but the URL in configuration key \"cloud base url\" is invalid, please fix");
+ url_t_destroy(&url);
+ sleep(5);
+ continue;
+ }
+ url_t_destroy(&url);
+
+ if (!load_private_key())
+ return 0;
+
+ sleep(5);
+ }
+
+ return 1;
+}
+
+void aclk_mqtt_wss_log_cb(mqtt_wss_log_type_t log_type, const char* str)
+{
+ switch(log_type) {
+ case MQTT_WSS_LOG_ERROR:
+ case MQTT_WSS_LOG_FATAL:
+ nd_log(NDLS_DAEMON, NDLP_ERR, "%s", str);
+ return;
+
+ case MQTT_WSS_LOG_WARN:
+ nd_log(NDLS_DAEMON, NDLP_WARNING, "%s", str);
+ return;
+
+ case MQTT_WSS_LOG_INFO:
+ nd_log(NDLS_DAEMON, NDLP_INFO, "%s", str);
+ return;
+
+ case MQTT_WSS_LOG_DEBUG:
+ return;
+
+ default:
+ nd_log(NDLS_DAEMON, NDLP_ERR, "Unknown log type from mqtt_wss");
+ }
+}
+
+static void msg_callback(const char *topic, const void *msg, size_t msglen, int qos)
+{
+ UNUSED(qos);
+ aclk_rcvd_cloud_msgs++;
+
+ netdata_log_debug(D_ACLK, "Got Message From Broker Topic \"%s\" QOS %d", topic, qos);
+
+ if (aclk_shared_state.mqtt_shutdown_msg_id > 0) {
+ netdata_log_error("Link is shutting down. Ignoring incoming message.");
+ return;
+ }
+
+ const char *msgtype = strrchr(topic, '/');
+ if (unlikely(!msgtype)) {
+ error_report("Cannot get message type from topic. Ignoring message from topic \"%s\"", topic);
+ return;
+ }
+ msgtype++;
+ if (unlikely(!*msgtype)) {
+ error_report("Message type empty. Ignoring message from topic \"%s\"", topic);
+ return;
+ }
+
+#ifdef ACLK_LOG_CONVERSATION_DIR
+#define FN_MAX_LEN 512
+ char filename[FN_MAX_LEN];
+ int logfd;
+ snprintf(filename, FN_MAX_LEN, ACLK_LOG_CONVERSATION_DIR "/%010d-rx-%s.bin", ACLK_GET_CONV_LOG_NEXT(), msgtype);
+ logfd = open(filename, O_CREAT | O_TRUNC | O_WRONLY, S_IRUSR | S_IWUSR );
+ if(logfd < 0)
+ netdata_log_error("Error opening ACLK Conversation logfile \"%s\" for RX message.", filename);
+ write(logfd, msg, msglen);
+ close(logfd);
+#endif
+
+ aclk_handle_new_cloud_msg(msgtype, msg, msglen, topic);
+}
+
+static void puback_callback(uint16_t packet_id)
+{
+ if (++aclk_pubacks_per_conn == ACLK_PUBACKS_CONN_STABLE) {
+ last_conn_time_appl = now_realtime_sec();
+ aclk_tbeb_reset();
+ }
+
+#ifdef NETDATA_INTERNAL_CHECKS
+ aclk_stats_msg_puback(packet_id);
+#endif
+
+ if (aclk_shared_state.mqtt_shutdown_msg_id == (int)packet_id) {
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Shutdown message has been acknowledged by the cloud. Exiting gracefully");
+
+ aclk_shared_state.mqtt_shutdown_msg_rcvd = 1;
+ }
+}
+
+static int read_query_thread_count()
+{
+ int threads = MIN(get_netdata_cpus()/2, 6);
+ threads = MAX(threads, 2);
+ threads = config_get_number(CONFIG_SECTION_CLOUD, "query thread count", threads);
+ if(threads < 1) {
+ netdata_log_error("You need at least one query thread. Overriding configured setting of \"%d\"", threads);
+ threads = 1;
+ config_set_number(CONFIG_SECTION_CLOUD, "query thread count", threads);
+ }
+ return threads;
+}
+
+void aclk_graceful_disconnect(mqtt_wss_client client);
+
+/* Keeps connection alive and handles all network communications.
+ * Returns on error or when netdata is shutting down.
+ * @param client instance of mqtt_wss_client
+ * @returns 0 - Netdata Exits
+ * >0 - Error happened. Reconnect and start over.
+ */
+static int handle_connection(mqtt_wss_client client)
+{
+ time_t last_periodic_query_wakeup = now_monotonic_sec();
+ while (service_running(SERVICE_ACLK)) {
+ // timeout 1000 to check at least once a second
+ // for netdata_exit
+ if (mqtt_wss_service(client, 1000) < 0){
+ error_report("Connection Error or Dropped");
+ return 1;
+ }
+
+ if (disconnect_req || aclk_kill_link) {
+ nd_log(NDLS_DAEMON, NDLP_NOTICE,
+ "Going to restart connection due to disconnect_req=%s (cloud req), aclk_kill_link=%s (reclaim)",
+ disconnect_req ? "true" : "false",
+ aclk_kill_link ? "true" : "false");
+
+ disconnect_req = 0;
+ aclk_kill_link = 0;
+ aclk_graceful_disconnect(client);
+ aclk_queue_unlock();
+ aclk_shared_state.mqtt_shutdown_msg_id = -1;
+ aclk_shared_state.mqtt_shutdown_msg_rcvd = 0;
+ return 1;
+ }
+
+ // mqtt_wss_service will return faster than in one second
+ // if there is enough work to do
+ time_t now = now_monotonic_sec();
+ if (last_periodic_query_wakeup < now) {
+ // wake up at least one Query Thread at least
+ // once per second
+ last_periodic_query_wakeup = now;
+ QUERY_THREAD_WAKEUP;
+ }
+ }
+ return 0;
+}
+
+static inline void mqtt_connected_actions(mqtt_wss_client client)
+{
+ char *topic = (char*)aclk_get_topic(ACLK_TOPICID_COMMAND);
+
+ if (!topic)
+ netdata_log_error("Unable to fetch topic for COMMAND (to subscribe)");
+ else
+ mqtt_wss_subscribe(client, topic, 1);
+
+ topic = (char*)aclk_get_topic(ACLK_TOPICID_CMD_NG_V1);
+ if (!topic)
+ netdata_log_error("Unable to fetch topic for protobuf COMMAND (to subscribe)");
+ else
+ mqtt_wss_subscribe(client, topic, 1);
+
+ aclk_stats_upd_online(1);
+ aclk_connected = 1;
+ aclk_pubacks_per_conn = 0;
+ aclk_rcvd_cloud_msgs = 0;
+ aclk_connection_counter++;
+
+ aclk_topic_cache_iter_t iter = ACLK_TOPIC_CACHE_ITER_T_INITIALIZER;
+ while ((topic = (char*)aclk_topic_cache_iterate(&iter)) != NULL)
+ mqtt_wss_set_topic_alias(client, topic);
+
+ aclk_send_agent_connection_update(client, 1);
+}
+
+void aclk_graceful_disconnect(mqtt_wss_client client)
+{
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Preparing to gracefully shutdown ACLK connection");
+
+ aclk_queue_lock();
+ aclk_queue_flush();
+
+ aclk_shared_state.mqtt_shutdown_msg_id = aclk_send_agent_connection_update(client, 0);
+
+ time_t t = now_monotonic_sec();
+ while (!mqtt_wss_service(client, 100)) {
+ if (now_monotonic_sec() - t >= 2) {
+ netdata_log_error("Wasn't able to gracefully shutdown ACLK in time!");
+ break;
+ }
+ if (aclk_shared_state.mqtt_shutdown_msg_rcvd) {
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "MQTT App Layer `disconnect` message sent successfully");
+ break;
+ }
+ }
+
+ nd_log(NDLS_DAEMON, NDLP_WARNING, "ACLK link is down");
+ nd_log(NDLS_ACCESS, NDLP_WARNING, "ACLK DISCONNECTED");
+
+ aclk_stats_upd_online(0);
+ last_disconnect_time = now_realtime_sec();
+ aclk_connected = 0;
+
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Attempting to gracefully shutdown the MQTT/WSS connection");
+
+ mqtt_wss_disconnect(client, 1000);
+}
+
+static unsigned long aclk_reconnect_delay() {
+ unsigned long recon_delay;
+ time_t now;
+
+ if (aclk_disable_runtime) {
+ aclk_tbeb_reset();
+ return 60 * MSEC_PER_SEC;
+ }
+
+ now = now_monotonic_sec();
+ if (aclk_block_until) {
+ if (now < aclk_block_until) {
+ recon_delay = aclk_block_until - now;
+ recon_delay *= MSEC_PER_SEC;
+ aclk_block_until = 0;
+ aclk_tbeb_reset();
+ return recon_delay;
+ }
+ aclk_block_until = 0;
+ }
+
+ if (!aclk_env || !aclk_env->backoff.base)
+ return aclk_tbeb_delay(0, 2, 0, 1024);
+
+ return aclk_tbeb_delay(0, aclk_env->backoff.base, aclk_env->backoff.min_s, aclk_env->backoff.max_s);
+}
+
+/* Block till aclk_reconnect_delay is satisfied or netdata_exit is signalled
+ * @return 0 - Go ahead and connect (delay expired)
+ * 1 - netdata_exit
+ */
+#define NETDATA_EXIT_POLL_MS (MSEC_PER_SEC/4)
+static int aclk_block_till_recon_allowed() {
+ unsigned long recon_delay = aclk_reconnect_delay();
+
+ next_connection_attempt = now_realtime_sec() + (recon_delay / MSEC_PER_SEC);
+ last_backoff_value = (float)recon_delay / MSEC_PER_SEC;
+
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Wait before attempting to reconnect in %.3f seconds", recon_delay / (float)MSEC_PER_SEC);
+
+ // we want to wake up from time to time to check netdata_exit
+ while (recon_delay)
+ {
+ if (!service_running(SERVICE_ACLK))
+ return 1;
+ if (recon_delay > NETDATA_EXIT_POLL_MS) {
+ sleep_usec(NETDATA_EXIT_POLL_MS * USEC_PER_MS);
+ recon_delay -= NETDATA_EXIT_POLL_MS;
+ continue;
+ }
+ sleep_usec(recon_delay * USEC_PER_MS);
+ recon_delay = 0;
+ }
+ return !service_running(SERVICE_ACLK);
+}
+
+#ifndef ACLK_DISABLE_CHALLENGE
+/* Cloud returns transport list ordered with highest
+ * priority first. This function selects highest prio
+ * transport that we can actually use (support)
+ */
+static int aclk_get_transport_idx(aclk_env_t *env) {
+ for (size_t i = 0; i < env->transport_count; i++) {
+ // currently we support only MQTT 5
+ // therefore select first transport that matches
+ if (env->transports[i]->type == ACLK_TRP_MQTT_5) {
+ return i;
+ }
+ }
+ return -1;
+}
+#endif
+
+ACLK_STATUS aclk_status = ACLK_STATUS_NONE;
+
+const char *aclk_status_to_string(void) {
+ switch(aclk_status) {
+ case ACLK_STATUS_CONNECTED:
+ return "connected";
+
+ case ACLK_STATUS_NONE:
+ return "none";
+
+ case ACLK_STATUS_DISABLED:
+ return "disabled";
+
+ case ACLK_STATUS_NO_CLOUD_URL:
+ return "no_cloud_url";
+
+ case ACLK_STATUS_INVALID_CLOUD_URL:
+ return "invalid_cloud_url";
+
+ case ACLK_STATUS_NOT_CLAIMED:
+ return "not_claimed";
+
+ case ACLK_STATUS_ENV_ENDPOINT_UNREACHABLE:
+ return "env_endpoint_unreachable";
+
+ case ACLK_STATUS_ENV_RESPONSE_NOT_200:
+ return "env_response_not_200";
+
+ case ACLK_STATUS_ENV_RESPONSE_EMPTY:
+ return "env_response_empty";
+
+ case ACLK_STATUS_ENV_RESPONSE_NOT_JSON:
+ return "env_response_not_json";
+
+ case ACLK_STATUS_ENV_FAILED:
+ return "env_failed";
+
+ case ACLK_STATUS_BLOCKED:
+ return "blocked";
+
+ case ACLK_STATUS_NO_OLD_PROTOCOL:
+ return "no_old_protocol";
+
+ case ACLK_STATUS_NO_PROTOCOL_CAPABILITY:
+ return "no_protocol_capability";
+
+ case ACLK_STATUS_INVALID_ENV_AUTH_URL:
+ return "invalid_env_auth_url";
+
+ case ACLK_STATUS_INVALID_ENV_TRANSPORT_IDX:
+ return "invalid_env_transport_idx";
+
+ case ACLK_STATUS_INVALID_ENV_TRANSPORT_URL:
+ return "invalid_env_transport_url";
+
+ case ACLK_STATUS_INVALID_OTP:
+ return "invalid_otp";
+
+ case ACLK_STATUS_NO_LWT_TOPIC:
+ return "no_lwt_topic";
+
+ default:
+ return "unknown";
+ }
+}
+
+const char *aclk_cloud_base_url = NULL;
+
+/* Attempts to make a connection to MQTT broker over WSS
+ * @param client instance of mqtt_wss_client
+ * @return 0 - Successful Connection,
+ * <0 - Irrecoverable Error -> Kill ACLK,
+ * >0 - netdata_exit
+ */
+#define CLOUD_BASE_URL_READ_RETRY 30
+#ifdef ACLK_SSL_ALLOW_SELF_SIGNED
+#define ACLK_SSL_FLAGS MQTT_WSS_SSL_ALLOW_SELF_SIGNED
+#else
+#define ACLK_SSL_FLAGS MQTT_WSS_SSL_CERT_CHECK_FULL
+#endif
+static int aclk_attempt_to_connect(mqtt_wss_client client)
+{
+ int ret;
+
+ url_t base_url;
+
+#ifndef ACLK_DISABLE_CHALLENGE
+ url_t auth_url;
+ url_t mqtt_url;
+#endif
+
+ while (service_running(SERVICE_ACLK)) {
+ aclk_cloud_base_url = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "cloud base url", NULL);
+ if (aclk_cloud_base_url == NULL) {
+ error_report("Do not move the cloud base url out of post_conf_load!!");
+ aclk_status = ACLK_STATUS_NO_CLOUD_URL;
+ return -1;
+ }
+
+ if (aclk_block_till_recon_allowed()) {
+ aclk_status = ACLK_STATUS_BLOCKED;
+ return 1;
+ }
+
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Attempting connection now");
+
+ memset(&base_url, 0, sizeof(url_t));
+ if (url_parse(aclk_cloud_base_url, &base_url)) {
+ aclk_status = ACLK_STATUS_INVALID_CLOUD_URL;
+ error_report("ACLK base URL configuration key could not be parsed. Will retry in %d seconds.", CLOUD_BASE_URL_READ_RETRY);
+ sleep(CLOUD_BASE_URL_READ_RETRY);
+ url_t_destroy(&base_url);
+ continue;
+ }
+
+ struct mqtt_wss_proxy proxy_conf = { .host = NULL, .port = 0, .username = NULL, .password = NULL, .type = MQTT_WSS_DIRECT };
+ aclk_set_proxy((char**)&proxy_conf.host, &proxy_conf.port, (char**)&proxy_conf.username, (char**)&proxy_conf.password, &proxy_conf.type);
+
+ struct mqtt_connect_params mqtt_conn_params = {
+ .clientid = "anon",
+ .username = "anon",
+ .password = "anon",
+ .will_topic = "lwt",
+ .will_msg = NULL,
+ .will_flags = MQTT_WSS_PUB_QOS2,
+ .keep_alive = 60,
+ .drop_on_publish_fail = 1
+ };
+
+#ifndef ACLK_DISABLE_CHALLENGE
+ if (aclk_env) {
+ aclk_env_t_destroy(aclk_env);
+ freez(aclk_env);
+ }
+ aclk_env = callocz(1, sizeof(aclk_env_t));
+
+ ret = aclk_get_env(aclk_env, base_url.host, base_url.port);
+ url_t_destroy(&base_url);
+ if(ret) switch(ret) {
+ case 1:
+ aclk_status = ACLK_STATUS_NOT_CLAIMED;
+ error_report("Failed to Get ACLK environment (agent is not claimed)");
+ // delay handled by aclk_block_till_recon_allowed
+ continue;
+
+ case 2:
+ aclk_status = ACLK_STATUS_ENV_ENDPOINT_UNREACHABLE;
+ error_report("Failed to Get ACLK environment (cannot contact ENV endpoint)");
+ // delay handled by aclk_block_till_recon_allowed
+ continue;
+
+ case 3:
+ aclk_status = ACLK_STATUS_ENV_RESPONSE_NOT_200;
+ error_report("Failed to Get ACLK environment (ENV response code is not 200)");
+ // delay handled by aclk_block_till_recon_allowed
+ continue;
+
+ case 4:
+ aclk_status = ACLK_STATUS_ENV_RESPONSE_EMPTY;
+ error_report("Failed to Get ACLK environment (ENV response is empty)");
+ // delay handled by aclk_block_till_recon_allowed
+ continue;
+
+ case 5:
+ aclk_status = ACLK_STATUS_ENV_RESPONSE_NOT_JSON;
+ error_report("Failed to Get ACLK environment (ENV response is not JSON)");
+ // delay handled by aclk_block_till_recon_allowed
+ continue;
+
+ default:
+ aclk_status = ACLK_STATUS_ENV_FAILED;
+ error_report("Failed to Get ACLK environment (unknown error)");
+ // delay handled by aclk_block_till_recon_allowed
+ continue;
+ }
+
+ if (!service_running(SERVICE_ACLK)) {
+ aclk_status = ACLK_STATUS_DISABLED;
+ return 1;
+ }
+
+ if (aclk_env->encoding != ACLK_ENC_PROTO) {
+ aclk_status = ACLK_STATUS_NO_OLD_PROTOCOL;
+ error_report("This agent can only use the new cloud protocol but cloud requested old one.");
+ continue;
+ }
+
+ if (!aclk_env_has_capa("proto")) {
+ aclk_status = ACLK_STATUS_NO_PROTOCOL_CAPABILITY;
+ error_report("Can't use encoding=proto without at least \"proto\" capability.");
+ continue;
+ }
+
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "New ACLK protobuf protocol negotiated successfully (/env response).");
+
+ memset(&auth_url, 0, sizeof(url_t));
+ if (url_parse(aclk_env->auth_endpoint, &auth_url)) {
+ aclk_status = ACLK_STATUS_INVALID_ENV_AUTH_URL;
+ error_report("Parsing URL returned by env endpoint for authentication failed. \"%s\"", aclk_env->auth_endpoint);
+ url_t_destroy(&auth_url);
+ continue;
+ }
+
+ ret = aclk_get_mqtt_otp(aclk_private_key, (char **)&mqtt_conn_params.clientid, (char **)&mqtt_conn_params.username, (char **)&mqtt_conn_params.password, &auth_url);
+ url_t_destroy(&auth_url);
+ if (ret) {
+ aclk_status = ACLK_STATUS_INVALID_OTP;
+ error_report("Error passing Challenge/Response to get OTP");
+ continue;
+ }
+
+ // aclk_get_topic moved here as during OTP we
+ // generate the topic cache
+ mqtt_conn_params.will_topic = aclk_get_topic(ACLK_TOPICID_AGENT_CONN);
+
+ if (!mqtt_conn_params.will_topic) {
+ aclk_status = ACLK_STATUS_NO_LWT_TOPIC;
+ error_report("Couldn't get LWT topic. Will not send LWT.");
+ continue;
+ }
+
+ // Do the MQTT connection
+ ret = aclk_get_transport_idx(aclk_env);
+ if (ret < 0) {
+ aclk_status = ACLK_STATUS_INVALID_ENV_TRANSPORT_IDX;
+ error_report("Cloud /env endpoint didn't return any transport usable by this Agent.");
+ continue;
+ }
+
+ memset(&mqtt_url, 0, sizeof(url_t));
+ if (url_parse(aclk_env->transports[ret]->endpoint, &mqtt_url)){
+ aclk_status = ACLK_STATUS_INVALID_ENV_TRANSPORT_URL;
+ error_report("Failed to parse target URL for /env trp idx %d \"%s\"", ret, aclk_env->transports[ret]->endpoint);
+ url_t_destroy(&mqtt_url);
+ continue;
+ }
+#endif
+
+ aclk_session_newarch = now_realtime_usec();
+ aclk_session_sec = aclk_session_newarch / USEC_PER_SEC;
+ aclk_session_us = aclk_session_newarch % USEC_PER_SEC;
+
+ mqtt_conn_params.will_msg = aclk_generate_lwt(&mqtt_conn_params.will_msg_len);
+
+#ifdef ACLK_DISABLE_CHALLENGE
+ ret = mqtt_wss_connect(client, base_url.host, base_url.port, &mqtt_conn_params, ACLK_SSL_FLAGS, &proxy_conf);
+ url_t_destroy(&base_url);
+#else
+ ret = mqtt_wss_connect(client, mqtt_url.host, mqtt_url.port, &mqtt_conn_params, ACLK_SSL_FLAGS, &proxy_conf);
+ url_t_destroy(&mqtt_url);
+
+ freez((char*)mqtt_conn_params.clientid);
+ freez((char*)mqtt_conn_params.password);
+ freez((char*)mqtt_conn_params.username);
+#endif
+
+ freez((char*)mqtt_conn_params.will_msg);
+ freez((char*)proxy_conf.host);
+ freez((char*)proxy_conf.username);
+ freez((char*)proxy_conf.password);
+
+ if (!ret) {
+ last_conn_time_mqtt = now_realtime_sec();
+ nd_log(NDLS_DAEMON, NDLP_INFO, "ACLK connection successfully established");
+ aclk_status = ACLK_STATUS_CONNECTED;
+ nd_log(NDLS_ACCESS, NDLP_INFO, "ACLK CONNECTED");
+ mqtt_connected_actions(client);
+ return 0;
+ }
+
+ error_report("Connect failed");
+ }
+
+ aclk_status = ACLK_STATUS_DISABLED;
+ return 1;
+}
+
+/**
+ * Main agent cloud link thread
+ *
+ * This thread will simply call the main event loop that handles
+ * pending requests - both inbound and outbound
+ *
+ * @param ptr is a pointer to the netdata_static_thread structure.
+ *
+ * @return It always returns NULL
+ */
+void *aclk_main(void *ptr)
+{
+ struct netdata_static_thread *static_thread = (struct netdata_static_thread *)ptr;
+
+ struct aclk_stats_thread *stats_thread = NULL;
+
+ struct aclk_query_threads query_threads;
+ query_threads.thread_list = NULL;
+
+ ACLK_PROXY_TYPE proxy_type;
+ aclk_get_proxy(&proxy_type);
+ if (proxy_type == PROXY_TYPE_SOCKS5) {
+ netdata_log_error("SOCKS5 proxy is not supported by ACLK-NG yet.");
+ static_thread->enabled = NETDATA_MAIN_THREAD_EXITED;
+ return NULL;
+ }
+
+ unsigned int proto_hdl_cnt = aclk_init_rx_msg_handlers();
+
+#if defined( DISABLE_CLOUD ) || !defined( ENABLE_ACLK )
+ nd_log(NDLS_DAEMON, NDLP_INFO,
+ "Killing ACLK thread -> cloud functionality has been disabled");
+
+ static_thread->enabled = NETDATA_MAIN_THREAD_EXITED;
+ return NULL;
+#endif
+ query_threads.count = read_query_thread_count();
+
+ if (wait_till_cloud_enabled())
+ goto exit;
+
+ if (wait_till_agent_claim_ready())
+ goto exit;
+
+ if (!(mqttwss_client = mqtt_wss_new("mqtt_wss", aclk_mqtt_wss_log_cb, msg_callback, puback_callback))) {
+ netdata_log_error("Couldn't initialize MQTT_WSS network library");
+ goto exit;
+ }
+
+#ifdef MQTT_WSS_DEBUG
+ size_t default_ssl_log_filename_size = strlen(netdata_configured_log_dir) + strlen(DEFAULT_SSKEYLOGFILE_NAME) + 2;
+ char *default_ssl_log_filename = mallocz(default_ssl_log_filename_size);
+ snprintfz(default_ssl_log_filename, default_ssl_log_filename_size, "%s/%s", netdata_configured_log_dir, DEFAULT_SSKEYLOGFILE_NAME);
+ ssl_log_filename = config_get(CONFIG_SECTION_CLOUD, "aclk ssl keylog file", default_ssl_log_filename);
+ freez(default_ssl_log_filename);
+ if (ssl_log_filename) {
+ error_report("SSLKEYLOGFILE active (path:\"%s\")!", ssl_log_filename);
+ mqtt_wss_set_SSL_CTX_keylog_cb(mqttwss_client, aclk_ssl_keylog_cb);
+ }
+#endif
+
+ // Enable MQTT buffer growth if necessary
+ // e.g. old cloud architecture clients with huge nodes
+ // that send JSON payloads of 10 MB as single messages
+ mqtt_wss_set_max_buf_size(mqttwss_client, 25*1024*1024);
+
+ aclk_stats_enabled = config_get_boolean(CONFIG_SECTION_CLOUD, "statistics", global_statistics_enabled);
+ if (aclk_stats_enabled) {
+ stats_thread = callocz(1, sizeof(struct aclk_stats_thread));
+ stats_thread->query_thread_count = query_threads.count;
+ stats_thread->client = mqttwss_client;
+ aclk_stats_thread_prepare(query_threads.count, proto_hdl_cnt);
+ stats_thread->thread = nd_thread_create("ACLK_STATS", NETDATA_THREAD_OPTION_JOINABLE, aclk_stats_main_thread, stats_thread);
+ }
+
+ // Keep reconnecting and talking until our time has come
+ // and the Grim Reaper (netdata_exit) calls
+ do {
+ if (aclk_attempt_to_connect(mqttwss_client))
+ goto exit_full;
+
+ if (unlikely(!query_threads.thread_list))
+ aclk_query_threads_start(&query_threads, mqttwss_client);
+
+ if (handle_connection(mqttwss_client)) {
+ aclk_stats_upd_online(0);
+ last_disconnect_time = now_realtime_sec();
+ aclk_connected = 0;
+ nd_log(NDLS_ACCESS, NDLP_WARNING, "ACLK DISCONNECTED");
+ }
+ } while (service_running(SERVICE_ACLK));
+
+ aclk_graceful_disconnect(mqttwss_client);
+
+#ifdef MQTT_WSS_DEBUG
+ if (ssl_log_file)
+ fclose(ssl_log_file);
+#endif
+
+exit_full:
+// Tear Down
+ QUERY_THREAD_WAKEUP_ALL;
+
+ aclk_query_threads_cleanup(&query_threads);
+
+ if (aclk_stats_enabled) {
+ nd_thread_join(stats_thread->thread);
+ aclk_stats_thread_cleanup();
+ freez(stats_thread);
+ }
+ free_topic_cache();
+ mqtt_wss_destroy(mqttwss_client);
+exit:
+ if (aclk_env) {
+ aclk_env_t_destroy(aclk_env);
+ freez(aclk_env);
+ }
+ static_thread->enabled = NETDATA_MAIN_THREAD_EXITED;
+ return NULL;
+}
+
+void aclk_host_state_update(RRDHOST *host, int cmd, int queryable)
+{
+ nd_uuid_t node_id;
+ int ret = 0;
+
+ if (!aclk_connected)
+ return;
+
+ if (host->node_id && !uuid_is_null(*host->node_id)) {
+ uuid_copy(node_id, *host->node_id);
+ }
+ else {
+ ret = get_node_id(&host->host_uuid, &node_id);
+ if (ret > 0) {
+ // this means we were not able to check if node_id already present
+ netdata_log_error("Unable to check for node_id. Ignoring the host state update.");
+ return;
+ }
+ if (ret < 0) {
+ // node_id not found
+ aclk_query_t create_query;
+ create_query = aclk_query_new(REGISTER_NODE);
+ rrdhost_aclk_state_lock(localhost);
+ node_instance_creation_t node_instance_creation = {
+ .claim_id = localhost->aclk_state.claimed_id,
+ .hops = host->system_info->hops,
+ .hostname = rrdhost_hostname(host),
+ .machine_guid = host->machine_guid};
+ create_query->data.bin_payload.payload =
+ generate_node_instance_creation(&create_query->data.bin_payload.size, &node_instance_creation);
+ rrdhost_aclk_state_unlock(localhost);
+ create_query->data.bin_payload.topic = ACLK_TOPICID_CREATE_NODE;
+ create_query->data.bin_payload.msg_name = "CreateNodeInstance";
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Registering host=%s, hops=%u", host->machine_guid, host->system_info->hops);
+
+ aclk_queue_query(create_query);
+ return;
+ }
+ }
+
+ aclk_query_t query = aclk_query_new(NODE_STATE_UPDATE);
+ node_instance_connection_t node_state_update = {
+ .hops = host->system_info->hops,
+ .live = cmd,
+ .queryable = queryable,
+ .session_id = aclk_session_newarch
+ };
+ node_state_update.node_id = mallocz(UUID_STR_LEN);
+ uuid_unparse_lower(node_id, (char*)node_state_update.node_id);
+
+ node_state_update.capabilities = aclk_get_agent_capas();
+
+ rrdhost_aclk_state_lock(localhost);
+ node_state_update.claim_id = localhost->aclk_state.claimed_id;
+ query->data.bin_payload.payload = generate_node_instance_connection(&query->data.bin_payload.size, &node_state_update);
+ rrdhost_aclk_state_unlock(localhost);
+
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Queuing status update for node=%s, live=%d, hops=%u, queryable=%d",
+ (char*)node_state_update.node_id, cmd, host->system_info->hops, queryable);
+ freez((void*)node_state_update.node_id);
+ query->data.bin_payload.msg_name = "UpdateNodeInstanceConnection";
+ query->data.bin_payload.topic = ACLK_TOPICID_NODE_CONN;
+ aclk_queue_query(query);
+}
+
+void aclk_send_node_instances()
+{
+ struct node_instance_list *list_head = get_node_list();
+ struct node_instance_list *list = list_head;
+ if (unlikely(!list)) {
+ error_report("Failure to get_node_list from DB!");
+ return;
+ }
+ while (!uuid_is_null(list->host_id)) {
+ if (!uuid_is_null(list->node_id)) {
+ aclk_query_t query = aclk_query_new(NODE_STATE_UPDATE);
+ node_instance_connection_t node_state_update = {
+ .live = list->live,
+ .hops = list->hops,
+ .queryable = 1,
+ .session_id = aclk_session_newarch
+ };
+ node_state_update.node_id = mallocz(UUID_STR_LEN);
+ uuid_unparse_lower(list->node_id, (char*)node_state_update.node_id);
+
+ char host_id[UUID_STR_LEN];
+ uuid_unparse_lower(list->host_id, host_id);
+
+ RRDHOST *host = rrdhost_find_by_guid(host_id);
+ if (unlikely(!host)) {
+ freez((void*)node_state_update.node_id);
+ freez(query);
+ continue;
+ }
+ node_state_update.capabilities = aclk_get_node_instance_capas(host);
+
+ rrdhost_aclk_state_lock(localhost);
+ node_state_update.claim_id = localhost->aclk_state.claimed_id;
+ query->data.bin_payload.payload = generate_node_instance_connection(&query->data.bin_payload.size, &node_state_update);
+ rrdhost_aclk_state_unlock(localhost);
+
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Queuing status update for node=%s, live=%d, hops=%d, queryable=1",
+ (char*)node_state_update.node_id, list->live, list->hops);
+
+ freez((void*)node_state_update.capabilities);
+ freez((void*)node_state_update.node_id);
+ query->data.bin_payload.msg_name = "UpdateNodeInstanceConnection";
+ query->data.bin_payload.topic = ACLK_TOPICID_NODE_CONN;
+ aclk_queue_query(query);
+ } else {
+ aclk_query_t create_query;
+ create_query = aclk_query_new(REGISTER_NODE);
+ node_instance_creation_t node_instance_creation = {
+ .hops = list->hops,
+ .hostname = list->hostname,
+ };
+ node_instance_creation.machine_guid = mallocz(UUID_STR_LEN);
+ uuid_unparse_lower(list->host_id, (char*)node_instance_creation.machine_guid);
+ create_query->data.bin_payload.topic = ACLK_TOPICID_CREATE_NODE;
+ create_query->data.bin_payload.msg_name = "CreateNodeInstance";
+ rrdhost_aclk_state_lock(localhost);
+ node_instance_creation.claim_id = localhost->aclk_state.claimed_id,
+ create_query->data.bin_payload.payload = generate_node_instance_creation(&create_query->data.bin_payload.size, &node_instance_creation);
+ rrdhost_aclk_state_unlock(localhost);
+
+ nd_log(NDLS_DAEMON, NDLP_DEBUG,
+ "Queuing registration for host=%s, hops=%d",
+ (char*)node_instance_creation.machine_guid, list->hops);
+
+ freez((void *)node_instance_creation.machine_guid);
+ aclk_queue_query(create_query);
+ }
+ freez(list->hostname);
+
+ list++;
+ }
+ freez(list_head);
+}
+
+void aclk_send_bin_msg(char *msg, size_t msg_len, enum aclk_topics subtopic, const char *msgname)
+{
+ aclk_send_bin_message_subtopic_pid(mqttwss_client, msg, msg_len, subtopic, msgname);
+}
+
+static void fill_alert_status_for_host(BUFFER *wb, RRDHOST *host)
+{
+ struct proto_alert_status status;
+ memset(&status, 0, sizeof(status));
+ if (get_proto_alert_status(host, &status)) {
+ buffer_strcat(wb, "\nFailed to get alert streaming status for this host");
+ return;
+ }
+ buffer_sprintf(wb,
+ "\n\t\tUpdates: %d"
+ "\n\t\tPending Min Seq ID: %"PRIu64
+ "\n\t\tPending Max Seq ID: %"PRIu64
+ "\n\t\tLast Submitted Seq ID: %"PRIu64,
+ status.alert_updates,
+ status.pending_min_sequence_id,
+ status.pending_max_sequence_id,
+ status.last_submitted_sequence_id
+ );
+}
+#endif /* ENABLE_ACLK */
+
+char *aclk_state(void)
+{
+#ifndef ENABLE_ACLK
+ return strdupz("ACLK Available: No");
+#else
+ BUFFER *wb = buffer_create(1024, &netdata_buffers_statistics.buffers_aclk);
+ struct tm *tmptr, tmbuf;
+ char *ret;
+
+ buffer_strcat(wb,
+ "ACLK Available: Yes\n"
+ "ACLK Version: 2\n"
+ "Protocols Supported: Protobuf\n"
+ );
+ buffer_sprintf(wb, "Protocol Used: Protobuf\nMQTT Version: %d\nClaimed: ", 5);
+
+ char *agent_id = get_agent_claimid();
+ if (agent_id == NULL)
+ buffer_strcat(wb, "No\n");
+ else {
+ char *cloud_base_url = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "cloud base url", NULL);
+ buffer_sprintf(wb, "Yes\nClaimed Id: %s\nCloud URL: %s\n", agent_id, cloud_base_url ? cloud_base_url : "null");
+ freez(agent_id);
+ }
+
+ buffer_sprintf(wb, "Online: %s\nReconnect count: %d\nBanned By Cloud: %s\n", aclk_connected ? "Yes" : "No", aclk_connection_counter > 0 ? (aclk_connection_counter - 1) : 0, aclk_disable_runtime ? "Yes" : "No");
+ if (last_conn_time_mqtt && (tmptr = localtime_r(&last_conn_time_mqtt, &tmbuf)) ) {
+ char timebuf[26];
+ strftime(timebuf, 26, "%Y-%m-%d %H:%M:%S", tmptr);
+ buffer_sprintf(wb, "Last Connection Time: %s\n", timebuf);
+ }
+ if (last_conn_time_appl && (tmptr = localtime_r(&last_conn_time_appl, &tmbuf)) ) {
+ char timebuf[26];
+ strftime(timebuf, 26, "%Y-%m-%d %H:%M:%S", tmptr);
+ buffer_sprintf(wb, "Last Connection Time + %d PUBACKs received: %s\n", ACLK_PUBACKS_CONN_STABLE, timebuf);
+ }
+ if (last_disconnect_time && (tmptr = localtime_r(&last_disconnect_time, &tmbuf)) ) {
+ char timebuf[26];
+ strftime(timebuf, 26, "%Y-%m-%d %H:%M:%S", tmptr);
+ buffer_sprintf(wb, "Last Disconnect Time: %s\n", timebuf);
+ }
+ if (!aclk_connected && next_connection_attempt && (tmptr = localtime_r(&next_connection_attempt, &tmbuf)) ) {
+ char timebuf[26];
+ strftime(timebuf, 26, "%Y-%m-%d %H:%M:%S", tmptr);
+ buffer_sprintf(wb, "Next Connection Attempt At: %s\nLast Backoff: %.3f", timebuf, last_backoff_value);
+ }
+
+ if (aclk_connected) {
+ buffer_sprintf(wb, "Received Cloud MQTT Messages: %d\nMQTT Messages Confirmed by Remote Broker (PUBACKs): %d", aclk_rcvd_cloud_msgs, aclk_pubacks_per_conn);
+
+ RRDHOST *host;
+ rrd_rdlock();
+ rrdhost_foreach_read(host) {
+ buffer_sprintf(wb, "\n\n> Node Instance for mGUID: \"%s\" hostname \"%s\"\n", host->machine_guid, rrdhost_hostname(host));
+
+ buffer_strcat(wb, "\tClaimed ID: ");
+ rrdhost_aclk_state_lock(host);
+ if (host->aclk_state.claimed_id)
+ buffer_strcat(wb, host->aclk_state.claimed_id);
+ else
+ buffer_strcat(wb, "null");
+ rrdhost_aclk_state_unlock(host);
+
+
+ if (host->node_id == NULL || uuid_is_null(*host->node_id)) {
+ buffer_strcat(wb, "\n\tNode ID: null\n");
+ } else {
+ char node_id[GUID_LEN + 1];
+ uuid_unparse_lower(*host->node_id, node_id);
+ buffer_sprintf(wb, "\n\tNode ID: %s\n", node_id);
+ }
+
+ buffer_sprintf(wb, "\tStreaming Hops: %d\n\tRelationship: %s", host->system_info->hops, host == localhost ? "self" : "child");
+
+ if (host != localhost)
+ buffer_sprintf(wb, "\n\tStreaming Connection Live: %s", host->receiver ? "true" : "false");
+
+ buffer_strcat(wb, "\n\tAlert Streaming Status:");
+ fill_alert_status_for_host(wb, host);
+ }
+ rrd_rdunlock();
+ }
+
+ ret = strdupz(buffer_tostring(wb));
+ buffer_free(wb);
+ return ret;
+#endif /* ENABLE_ACLK */
+}
+
+#ifdef ENABLE_ACLK
+static void fill_alert_status_for_host_json(json_object *obj, RRDHOST *host)
+{
+ struct proto_alert_status status;
+ memset(&status, 0, sizeof(status));
+ if (get_proto_alert_status(host, &status))
+ return;
+
+ json_object *tmp = json_object_new_int(status.alert_updates);
+ json_object_object_add(obj, "updates", tmp);
+
+ tmp = json_object_new_int(status.pending_min_sequence_id);
+ json_object_object_add(obj, "pending-min-seq-id", tmp);
+
+ tmp = json_object_new_int(status.pending_max_sequence_id);
+ json_object_object_add(obj, "pending-max-seq-id", tmp);
+
+ tmp = json_object_new_int(status.last_submitted_sequence_id);
+ json_object_object_add(obj, "last-submitted-seq-id", tmp);
+}
+
+static json_object *timestamp_to_json(const time_t *t)
+{
+ struct tm *tmptr, tmbuf;
+ if (*t && (tmptr = gmtime_r(t, &tmbuf)) ) {
+ char timebuf[26];
+ strftime(timebuf, 26, "%Y-%m-%d %H:%M:%S", tmptr);
+ return json_object_new_string(timebuf);
+ }
+ return NULL;
+}
+#endif /* ENABLE_ACLK */
+
+char *aclk_state_json(void)
+{
+#ifndef ENABLE_ACLK
+ return strdupz("{\"aclk-available\":false}");
+#else
+ json_object *tmp, *grp, *msg = json_object_new_object();
+
+ tmp = json_object_new_boolean(1);
+ json_object_object_add(msg, "aclk-available", tmp);
+
+ tmp = json_object_new_int(2);
+ json_object_object_add(msg, "aclk-version", tmp);
+
+ grp = json_object_new_array();
+ tmp = json_object_new_string("Protobuf");
+ json_object_array_add(grp, tmp);
+ json_object_object_add(msg, "protocols-supported", grp);
+
+ char *agent_id = get_agent_claimid();
+ tmp = json_object_new_boolean(agent_id != NULL);
+ json_object_object_add(msg, "agent-claimed", tmp);
+
+ if (agent_id) {
+ tmp = json_object_new_string(agent_id);
+ freez(agent_id);
+ } else
+ tmp = NULL;
+ json_object_object_add(msg, "claimed-id", tmp);
+
+ char *cloud_base_url = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, "cloud base url", NULL);
+ tmp = cloud_base_url ? json_object_new_string(cloud_base_url) : NULL;
+ json_object_object_add(msg, "cloud-url", tmp);
+
+ tmp = json_object_new_boolean(aclk_connected);
+ json_object_object_add(msg, "online", tmp);
+
+ tmp = json_object_new_string("Protobuf");
+ json_object_object_add(msg, "used-cloud-protocol", tmp);
+
+ tmp = json_object_new_int(5);
+ json_object_object_add(msg, "mqtt-version", tmp);
+
+ tmp = json_object_new_int(aclk_rcvd_cloud_msgs);
+ json_object_object_add(msg, "received-app-layer-msgs", tmp);
+
+ tmp = json_object_new_int(aclk_pubacks_per_conn);
+ json_object_object_add(msg, "received-mqtt-pubacks", tmp);
+
+ tmp = json_object_new_int(aclk_connection_counter > 0 ? (aclk_connection_counter - 1) : 0);
+ json_object_object_add(msg, "reconnect-count", tmp);
+
+ json_object_object_add(msg, "last-connect-time-utc", timestamp_to_json(&last_conn_time_mqtt));
+ json_object_object_add(msg, "last-connect-time-puback-utc", timestamp_to_json(&last_conn_time_appl));
+ json_object_object_add(msg, "last-disconnect-time-utc", timestamp_to_json(&last_disconnect_time));
+ json_object_object_add(msg, "next-connection-attempt-utc", !aclk_connected ? timestamp_to_json(&next_connection_attempt) : NULL);
+ tmp = NULL;
+ if (!aclk_connected && last_backoff_value)
+ tmp = json_object_new_double(last_backoff_value);
+ json_object_object_add(msg, "last-backoff-value", tmp);
+
+ tmp = json_object_new_boolean(aclk_disable_runtime);
+ json_object_object_add(msg, "banned-by-cloud", tmp);
+
+ grp = json_object_new_array();
+
+ RRDHOST *host;
+ rrd_rdlock();
+ rrdhost_foreach_read(host) {
+ json_object *nodeinstance = json_object_new_object();
+
+ tmp = json_object_new_string(rrdhost_hostname(host));
+ json_object_object_add(nodeinstance, "hostname", tmp);
+
+ tmp = json_object_new_string(host->machine_guid);
+ json_object_object_add(nodeinstance, "mguid", tmp);
+
+ rrdhost_aclk_state_lock(host);
+ if (host->aclk_state.claimed_id) {
+ tmp = json_object_new_string(host->aclk_state.claimed_id);
+ json_object_object_add(nodeinstance, "claimed_id", tmp);
+ } else
+ json_object_object_add(nodeinstance, "claimed_id", NULL);
+ rrdhost_aclk_state_unlock(host);
+
+ if (host->node_id == NULL || uuid_is_null(*host->node_id)) {
+ json_object_object_add(nodeinstance, "node-id", NULL);
+ } else {
+ char node_id[GUID_LEN + 1];
+ uuid_unparse_lower(*host->node_id, node_id);
+ tmp = json_object_new_string(node_id);
+ json_object_object_add(nodeinstance, "node-id", tmp);
+ }
+
+ tmp = json_object_new_int(host->system_info->hops);
+ json_object_object_add(nodeinstance, "streaming-hops", tmp);
+
+ tmp = json_object_new_string(host == localhost ? "self" : "child");
+ json_object_object_add(nodeinstance, "relationship", tmp);
+
+ tmp = json_object_new_boolean((host->receiver || host == localhost));
+ json_object_object_add(nodeinstance, "streaming-online", tmp);
+
+ tmp = json_object_new_object();
+ fill_alert_status_for_host_json(tmp, host);
+ json_object_object_add(nodeinstance, "alert-sync-status", tmp);
+
+ json_object_array_add(grp, nodeinstance);
+ }
+ rrd_rdunlock();
+ json_object_object_add(msg, "node-instances", grp);
+
+ char *str = strdupz(json_object_to_json_string_ext(msg, JSON_C_TO_STRING_PLAIN));
+ json_object_put(msg);
+ return str;
+#endif /* ENABLE_ACLK */
+}
+
+void add_aclk_host_labels(void) {
+ RRDLABELS *labels = localhost->rrdlabels;
+
+#ifdef ENABLE_ACLK
+ rrdlabels_add(labels, "_aclk_available", "true", RRDLABEL_SRC_AUTO|RRDLABEL_SRC_ACLK);
+ ACLK_PROXY_TYPE aclk_proxy;
+ char *proxy_str;
+ aclk_get_proxy(&aclk_proxy);
+
+ switch(aclk_proxy) {
+ case PROXY_TYPE_SOCKS5:
+ proxy_str = "SOCKS5";
+ break;
+ case PROXY_TYPE_HTTP:
+ proxy_str = "HTTP";
+ break;
+ default:
+ proxy_str = "none";
+ break;
+ }
+
+ rrdlabels_add(labels, "_mqtt_version", "5", RRDLABEL_SRC_AUTO);
+ rrdlabels_add(labels, "_aclk_proxy", proxy_str, RRDLABEL_SRC_AUTO);
+ rrdlabels_add(labels, "_aclk_ng_new_cloud_protocol", "true", RRDLABEL_SRC_AUTO|RRDLABEL_SRC_ACLK);
+#else
+ rrdlabels_add(labels, "_aclk_available", "false", RRDLABEL_SRC_AUTO|RRDLABEL_SRC_ACLK);
+#endif
+}
+
+void aclk_queue_node_info(RRDHOST *host, bool immediate)
+{
+ struct aclk_sync_cfg_t *wc = host->aclk_config;
+ if (likely(wc))
+ wc->node_info_send_time = (host == localhost || immediate) ? 1 : now_realtime_sec();
+}
diff --git a/src/aclk/aclk.h b/src/aclk/aclk.h
new file mode 100644
index 000000000..72d1a2e11
--- /dev/null
+++ b/src/aclk/aclk.h
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+#ifndef ACLK_H
+#define ACLK_H
+
+#include "daemon/common.h"
+
+#ifdef ENABLE_ACLK
+#include "aclk_util.h"
+#include "aclk_rrdhost_state.h"
+
+// How many MQTT PUBACKs we need to get to consider connection
+// stable for the purposes of TBEB (truncated binary exponential backoff)
+#define ACLK_PUBACKS_CONN_STABLE 3
+#endif /* ENABLE_ACLK */
+
+typedef enum __attribute__((packed)) {
+ ACLK_STATUS_CONNECTED = 0,
+ ACLK_STATUS_NONE,
+ ACLK_STATUS_DISABLED,
+ ACLK_STATUS_NO_CLOUD_URL,
+ ACLK_STATUS_INVALID_CLOUD_URL,
+ ACLK_STATUS_NOT_CLAIMED,
+ ACLK_STATUS_ENV_ENDPOINT_UNREACHABLE,
+ ACLK_STATUS_ENV_RESPONSE_NOT_200,
+ ACLK_STATUS_ENV_RESPONSE_EMPTY,
+ ACLK_STATUS_ENV_RESPONSE_NOT_JSON,
+ ACLK_STATUS_ENV_FAILED,
+ ACLK_STATUS_BLOCKED,
+ ACLK_STATUS_NO_OLD_PROTOCOL,
+ ACLK_STATUS_NO_PROTOCOL_CAPABILITY,
+ ACLK_STATUS_INVALID_ENV_AUTH_URL,
+ ACLK_STATUS_INVALID_ENV_TRANSPORT_IDX,
+ ACLK_STATUS_INVALID_ENV_TRANSPORT_URL,
+ ACLK_STATUS_INVALID_OTP,
+ ACLK_STATUS_NO_LWT_TOPIC,
+} ACLK_STATUS;
+
+extern ACLK_STATUS aclk_status;
+extern const char *aclk_cloud_base_url;
+const char *aclk_status_to_string(void);
+
+extern int aclk_connected;
+extern int aclk_ctx_based;
+extern int aclk_disable_runtime;
+extern int aclk_stats_enabled;
+extern int aclk_kill_link;
+
+extern time_t last_conn_time_mqtt;
+extern time_t last_conn_time_appl;
+extern time_t last_disconnect_time;
+extern time_t next_connection_attempt;
+extern float last_backoff_value;
+
+extern usec_t aclk_session_us;
+extern time_t aclk_session_sec;
+
+extern time_t aclk_block_until;
+
+extern int aclk_connection_counter;
+extern int disconnect_req;
+
+#ifdef ENABLE_ACLK
+void *aclk_main(void *ptr);
+
+extern netdata_mutex_t aclk_shared_state_mutex;
+#define ACLK_SHARED_STATE_LOCK netdata_mutex_lock(&aclk_shared_state_mutex)
+#define ACLK_SHARED_STATE_UNLOCK netdata_mutex_unlock(&aclk_shared_state_mutex)
+
+extern struct aclk_shared_state {
+ // To wait for `disconnect` message PUBACK
+ // when shutting down
+ // at the same time if > 0 we know link is
+ // shutting down
+ int mqtt_shutdown_msg_id;
+ int mqtt_shutdown_msg_rcvd;
+} aclk_shared_state;
+
+void aclk_host_state_update(RRDHOST *host, int cmd, int queryable);
+void aclk_send_node_instances(void);
+
+void aclk_send_bin_msg(char *msg, size_t msg_len, enum aclk_topics subtopic, const char *msgname);
+
+#endif /* ENABLE_ACLK */
+
+char *aclk_state(void);
+char *aclk_state_json(void);
+void add_aclk_host_labels(void);
+void aclk_queue_node_info(RRDHOST *host, bool immediate);
+
+#endif /* ACLK_H */
diff --git a/src/aclk/aclk_alarm_api.c b/src/aclk/aclk_alarm_api.c
new file mode 100644
index 000000000..664671f70
--- /dev/null
+++ b/src/aclk/aclk_alarm_api.c
@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_alarm_api.h"
+
+#include "aclk_query_queue.h"
+
+#include "aclk_util.h"
+
+#include "aclk.h"
+
+void aclk_send_provide_alarm_checkpoint(struct alarm_checkpoint *checkpoint)
+{
+ aclk_query_t query = aclk_query_new(ALARM_PROVIDE_CHECKPOINT);
+ query->data.bin_payload.payload = generate_alarm_checkpoint(&query->data.bin_payload.size, checkpoint);
+ query->data.bin_payload.topic = ACLK_TOPICID_ALARM_CHECKPOINT;
+ query->data.bin_payload.msg_name = "AlarmCheckpoint";
+ QUEUE_IF_PAYLOAD_PRESENT(query);
+}
+
+void aclk_send_alarm_log_entry(struct alarm_log_entry *log_entry)
+{
+ size_t payload_size;
+ char *payload = generate_alarm_log_entry(&payload_size, log_entry);
+
+ aclk_send_bin_msg(payload, payload_size, ACLK_TOPICID_ALARM_LOG, "AlarmLogEntry");
+}
+
+void aclk_send_provide_alarm_cfg(struct provide_alarm_configuration *cfg)
+{
+ aclk_query_t query = aclk_query_new(ALARM_PROVIDE_CFG);
+ query->data.bin_payload.payload = generate_provide_alarm_configuration(&query->data.bin_payload.size, cfg);
+ query->data.bin_payload.topic = ACLK_TOPICID_ALARM_CONFIG;
+ query->data.bin_payload.msg_name = "ProvideAlarmConfiguration";
+ QUEUE_IF_PAYLOAD_PRESENT(query);
+}
+
+void aclk_send_alarm_snapshot(alarm_snapshot_proto_ptr_t snapshot)
+{
+ aclk_query_t query = aclk_query_new(ALARM_SNAPSHOT);
+ query->data.bin_payload.payload = generate_alarm_snapshot_bin(&query->data.bin_payload.size, snapshot);
+ query->data.bin_payload.topic = ACLK_TOPICID_ALARM_SNAPSHOT;
+ query->data.bin_payload.msg_name = "AlarmSnapshot";
+ QUEUE_IF_PAYLOAD_PRESENT(query);
+}
diff --git a/src/aclk/aclk_alarm_api.h b/src/aclk/aclk_alarm_api.h
new file mode 100644
index 000000000..4d9d9447a
--- /dev/null
+++ b/src/aclk/aclk_alarm_api.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_ALARM_API_H
+#define ACLK_ALARM_API_H
+
+#include "../daemon/common.h"
+#include "schema-wrappers/schema_wrappers.h"
+
+void aclk_send_provide_alarm_checkpoint(struct alarm_checkpoint *checkpoint);
+void aclk_send_alarm_log_entry(struct alarm_log_entry *log_entry);
+void aclk_send_provide_alarm_cfg(struct provide_alarm_configuration *cfg);
+void aclk_send_alarm_snapshot(alarm_snapshot_proto_ptr_t snapshot);
+
+#endif /* ACLK_ALARM_API_H */
diff --git a/src/aclk/aclk_capas.c b/src/aclk/aclk_capas.c
new file mode 100644
index 000000000..00102ad4a
--- /dev/null
+++ b/src/aclk/aclk_capas.c
@@ -0,0 +1,59 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_capas.h"
+
+#include "ml/ml.h"
+
+#define HTTP_API_V2_VERSION 6
+
+const struct capability *aclk_get_agent_capas()
+{
+ static struct capability agent_capabilities[] = {
+ { .name = "json", .version = 2, .enabled = 0 },
+ { .name = "proto", .version = 1, .enabled = 1 },
+ { .name = "ml", .version = 0, .enabled = 0 }, // index 2, below
+ { .name = "mc", .version = 0, .enabled = 0 }, // index 3, below
+ { .name = "ctx", .version = 1, .enabled = 1 },
+ { .name = "funcs", .version = 1, .enabled = 1 },
+ { .name = "http_api_v2", .version = HTTP_API_V2_VERSION, .enabled = 1 },
+ { .name = "health", .version = 1, .enabled = 0 }, // index 7, below
+ { .name = "req_cancel", .version = 1, .enabled = 1 },
+ { .name = "dyncfg", .version = 2, .enabled = 1 },
+ { .name = NULL, .version = 0, .enabled = 0 }
+ };
+ agent_capabilities[2].version = ml_capable() ? 1 : 0;
+ agent_capabilities[2].enabled = ml_enabled(localhost);
+
+ agent_capabilities[3].version = enable_metric_correlations ? metric_correlations_version : 0;
+ agent_capabilities[3].enabled = enable_metric_correlations;
+
+ agent_capabilities[7].enabled = localhost->health.health_enabled;
+
+ return agent_capabilities;
+}
+
+struct capability *aclk_get_node_instance_capas(RRDHOST *host)
+{
+ bool functions = (host == localhost || (host->receiver && stream_has_capability(host->receiver, STREAM_CAP_FUNCTIONS)));
+ bool dyncfg = (host == localhost || dyncfg_available_for_rrdhost(host));
+
+ struct capability ni_caps[] = {
+ { .name = "proto", .version = 1, .enabled = 1 },
+ { .name = "ml", .version = ml_capable(), .enabled = ml_enabled(host) },
+ { .name = "mc",
+ .version = enable_metric_correlations ? metric_correlations_version : 0,
+ .enabled = enable_metric_correlations },
+ { .name = "ctx", .version = 1, .enabled = 1 },
+ { .name = "funcs", .version = functions ? 1 : 0, .enabled = functions ? 1 : 0 },
+ { .name = "http_api_v2", .version = HTTP_API_V2_VERSION, .enabled = 1 },
+ { .name = "health", .version = 1, .enabled = host->health.health_enabled },
+ { .name = "req_cancel", .version = 1, .enabled = 1 },
+ { .name = "dyncfg", .version = 2, .enabled = dyncfg },
+ { .name = NULL, .version = 0, .enabled = 0 }
+ };
+
+ struct capability *ret = mallocz(sizeof(ni_caps));
+ memcpy(ret, ni_caps, sizeof(ni_caps));
+
+ return ret;
+}
diff --git a/src/aclk/aclk_capas.h b/src/aclk/aclk_capas.h
new file mode 100644
index 000000000..c39a197b8
--- /dev/null
+++ b/src/aclk/aclk_capas.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_CAPAS_H
+#define ACLK_CAPAS_H
+
+#include "daemon/common.h"
+#include "libnetdata/libnetdata.h"
+
+#include "schema-wrappers/capability.h"
+
+const struct capability *aclk_get_agent_capas();
+struct capability *aclk_get_node_instance_capas(RRDHOST *host);
+
+#endif /* ACLK_CAPAS_H */
diff --git a/src/aclk/aclk_contexts_api.c b/src/aclk/aclk_contexts_api.c
new file mode 100644
index 000000000..f3344935e
--- /dev/null
+++ b/src/aclk/aclk_contexts_api.c
@@ -0,0 +1,41 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_query_queue.h"
+
+#include "aclk_contexts_api.h"
+
+void aclk_send_contexts_snapshot(contexts_snapshot_t data)
+{
+ aclk_query_t query = aclk_query_new(PROTO_BIN_MESSAGE);
+ query->data.bin_payload.topic = ACLK_TOPICID_CTXS_SNAPSHOT;
+ query->data.bin_payload.payload = contexts_snapshot_2bin(data, &query->data.bin_payload.size);
+ query->data.bin_payload.msg_name = "ContextsSnapshot";
+ QUEUE_IF_PAYLOAD_PRESENT(query);
+}
+
+void aclk_send_contexts_updated(contexts_updated_t data)
+{
+ aclk_query_t query = aclk_query_new(PROTO_BIN_MESSAGE);
+ query->data.bin_payload.topic = ACLK_TOPICID_CTXS_UPDATED;
+ query->data.bin_payload.payload = contexts_updated_2bin(data, &query->data.bin_payload.size);
+ query->data.bin_payload.msg_name = "ContextsUpdated";
+ QUEUE_IF_PAYLOAD_PRESENT(query);
+}
+
+void aclk_update_node_collectors(struct update_node_collectors *collectors)
+{
+ aclk_query_t query = aclk_query_new(UPDATE_NODE_COLLECTORS);
+ query->data.bin_payload.topic = ACLK_TOPICID_NODE_COLLECTORS;
+ query->data.bin_payload.payload = generate_update_node_collectors_message(&query->data.bin_payload.size, collectors);
+ query->data.bin_payload.msg_name = "UpdateNodeCollectors";
+ QUEUE_IF_PAYLOAD_PRESENT(query);
+}
+
+void aclk_update_node_info(struct update_node_info *info)
+{
+ aclk_query_t query = aclk_query_new(UPDATE_NODE_INFO);
+ query->data.bin_payload.topic = ACLK_TOPICID_NODE_INFO;
+ query->data.bin_payload.payload = generate_update_node_info_message(&query->data.bin_payload.size, info);
+ query->data.bin_payload.msg_name = "UpdateNodeInfo";
+ QUEUE_IF_PAYLOAD_PRESENT(query);
+}
diff --git a/src/aclk/aclk_contexts_api.h b/src/aclk/aclk_contexts_api.h
new file mode 100644
index 000000000..f0b5ec77e
--- /dev/null
+++ b/src/aclk/aclk_contexts_api.h
@@ -0,0 +1,14 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+#ifndef ACLK_CONTEXTS_API_H
+#define ACLK_CONTEXTS_API_H
+
+#include "schema-wrappers/schema_wrappers.h"
+
+
+void aclk_send_contexts_snapshot(contexts_snapshot_t data);
+void aclk_send_contexts_updated(contexts_updated_t data);
+void aclk_update_node_collectors(struct update_node_collectors *collectors);
+void aclk_update_node_info(struct update_node_info *info);
+
+#endif /* ACLK_CONTEXTS_API_H */
+
diff --git a/src/aclk/aclk_otp.c b/src/aclk/aclk_otp.c
new file mode 100644
index 000000000..c9c75dd38
--- /dev/null
+++ b/src/aclk/aclk_otp.c
@@ -0,0 +1,883 @@
+
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_otp.h"
+#include "aclk_util.h"
+#include "aclk.h"
+
+#include "daemon/common.h"
+
+#include "mqtt_websockets/c-rbuf/cringbuffer.h"
+
+static int aclk_https_request(https_req_t *request, https_req_response_t *response) {
+ int rc;
+ // wrapper for ACLK only which loads ACLK specific proxy settings
+ // then only calls https_request
+ struct mqtt_wss_proxy proxy_conf = { .host = NULL, .port = 0, .username = NULL, .password = NULL, .type = MQTT_WSS_DIRECT };
+ aclk_set_proxy((char**)&proxy_conf.host, &proxy_conf.port, (char**)&proxy_conf.username, (char**)&proxy_conf.password, &proxy_conf.type);
+
+ if (proxy_conf.type == MQTT_WSS_PROXY_HTTP) {
+ request->proxy_host = (char*)proxy_conf.host; // TODO make it const as well
+ request->proxy_port = proxy_conf.port;
+ request->proxy_username = proxy_conf.username;
+ request->proxy_password = proxy_conf.password;
+ }
+
+ rc = https_request(request, response);
+ freez((char*)proxy_conf.host);
+ freez((char*)proxy_conf.username);
+ freez((char*)proxy_conf.password);
+ return rc;
+}
+
+struct auth_data {
+ char *client_id;
+ char *username;
+ char *passwd;
+};
+
+#define PARSE_ENV_JSON_CHK_TYPE(it, type, name) \
+ if (json_object_get_type(json_object_iter_peek_value(it)) != type) { \
+ netdata_log_error("value of key \"%s\" should be %s", name, #type); \
+ goto exit; \
+ }
+
+#define JSON_KEY_CLIENTID "clientID"
+#define JSON_KEY_USER "username"
+#define JSON_KEY_PASS "password"
+#define JSON_KEY_TOPICS "topics"
+
+static int parse_passwd_response(const char *json_str, struct auth_data *auth) {
+ int rc = 1;
+ json_object *json;
+ struct json_object_iterator it;
+ struct json_object_iterator itEnd;
+
+ json = json_tokener_parse(json_str);
+ if (!json) {
+ netdata_log_error("JSON-C failed to parse the payload of http response of /env endpoint");
+ return 1;
+ }
+
+ it = json_object_iter_begin(json);
+ itEnd = json_object_iter_end(json);
+
+ while (!json_object_iter_equal(&it, &itEnd)) {
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_CLIENTID)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_CLIENTID)
+
+ auth->client_id = strdupz(json_object_get_string(json_object_iter_peek_value(&it)));
+ json_object_iter_next(&it);
+ continue;
+ }
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_USER)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_USER)
+
+ auth->username = strdupz(json_object_get_string(json_object_iter_peek_value(&it)));
+ json_object_iter_next(&it);
+ continue;
+ }
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_PASS)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_PASS)
+
+ auth->passwd = strdupz(json_object_get_string(json_object_iter_peek_value(&it)));
+ json_object_iter_next(&it);
+ continue;
+ }
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_TOPICS)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_array, JSON_KEY_TOPICS)
+
+ if (aclk_generate_topic_cache(json_object_iter_peek_value(&it))) {
+ netdata_log_error("Failed to generate topic cache!");
+ goto exit;
+ }
+ json_object_iter_next(&it);
+ continue;
+ }
+ netdata_log_error("Unknown key \"%s\" in passwd response payload. Ignoring", json_object_iter_peek_name(&it));
+ json_object_iter_next(&it);
+ }
+
+ if (!auth->client_id) {
+ netdata_log_error(JSON_KEY_CLIENTID " is compulsory key in /password response");
+ goto exit;
+ }
+ if (!auth->passwd) {
+ netdata_log_error(JSON_KEY_PASS " is compulsory in /password response");
+ goto exit;
+ }
+ if (!auth->username) {
+ netdata_log_error(JSON_KEY_USER " is compulsory in /password response");
+ goto exit;
+ }
+
+ rc = 0;
+exit:
+ json_object_put(json);
+ return rc;
+}
+
+#define JSON_KEY_ERTRY "errorNonRetryable"
+#define JSON_KEY_EDELAY "errorRetryDelaySeconds"
+#define JSON_KEY_EEC "errorCode"
+#define JSON_KEY_EMSGKEY "errorMsgKey"
+#define JSON_KEY_EMSG "errorMessage"
+#if JSON_C_MINOR_VERSION >= 13
+static const char *get_json_str_by_path(json_object *json, const char *path) {
+ json_object *ptr;
+ if (json_pointer_get(json, path, &ptr)) {
+ netdata_log_error("Missing compulsory key \"%s\" in error response", path);
+ return NULL;
+ }
+ if (json_object_get_type(ptr) != json_type_string) {
+ netdata_log_error("Value of Key \"%s\" in error response should be string", path);
+ return NULL;
+ }
+ return json_object_get_string(ptr);
+}
+
+static int aclk_parse_otp_error(const char *json_str) {
+ int rc = 1;
+ json_object *json, *ptr;
+ const char *ec;
+ const char *ek;
+ const char *emsg;
+ int block_retry = -1, backoff = -1;
+
+
+ json = json_tokener_parse(json_str);
+ if (!json) {
+ netdata_log_error("JSON-C failed to parse the payload of http response of /env endpoint");
+ return 1;
+ }
+
+ if ((ec = get_json_str_by_path(json, "/" JSON_KEY_EEC)) == NULL)
+ goto exit;
+
+ if ((ek = get_json_str_by_path(json, "/" JSON_KEY_EMSGKEY)) == NULL)
+ goto exit;
+
+ if ((emsg = get_json_str_by_path(json, "/" JSON_KEY_EMSG)) == NULL)
+ goto exit;
+
+ // optional field
+ if (!json_pointer_get(json, "/" JSON_KEY_ERTRY, &ptr)) {
+ if (json_object_get_type(ptr) != json_type_boolean) {
+ netdata_log_error("Error response Key " "/" JSON_KEY_ERTRY " should be of boolean type");
+ goto exit;
+ }
+ block_retry = json_object_get_boolean(ptr);
+ }
+
+ // optional field
+ if (!json_pointer_get(json, "/" JSON_KEY_EDELAY, &ptr)) {
+ if (json_object_get_type(ptr) != json_type_int) {
+ netdata_log_error("Error response Key " "/" JSON_KEY_EDELAY " should be of integer type");
+ goto exit;
+ }
+ backoff = json_object_get_int(ptr);
+ }
+
+ if (block_retry > 0)
+ aclk_disable_runtime = 1;
+
+ if (backoff > 0)
+ aclk_block_until = now_monotonic_sec() + backoff;
+
+ netdata_log_error("Cloud returned EC=\"%s\", Msg-Key:\"%s\", Msg:\"%s\", BlockRetry:%s, Backoff:%ds (-1 unset by cloud)", ec, ek, emsg, block_retry > 0 ? "true" : "false", backoff);
+ rc = 0;
+exit:
+ json_object_put(json);
+ return rc;
+}
+#else
+static int aclk_parse_otp_error(const char *json_str) {
+ int rc = 1;
+ int block_retry = -1, backoff = -1;
+
+ const char *ec = NULL;
+ const char *ek = NULL;
+ const char *emsg = NULL;
+
+ json_object *json;
+ struct json_object_iterator it;
+ struct json_object_iterator itEnd;
+
+ json = json_tokener_parse(json_str);
+ if (!json) {
+ netdata_log_error("JSON-C failed to parse the payload of http response of /env endpoint");
+ return 1;
+ }
+
+ it = json_object_iter_begin(json);
+ itEnd = json_object_iter_end(json);
+
+ while (!json_object_iter_equal(&it, &itEnd)) {
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_EMSG)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_EMSG)
+
+ emsg = json_object_get_string(json_object_iter_peek_value(&it));
+ json_object_iter_next(&it);
+ continue;
+ }
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_EMSGKEY)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_EMSGKEY)
+
+ ek = json_object_get_string(json_object_iter_peek_value(&it));
+ json_object_iter_next(&it);
+ continue;
+ }
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_EEC)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_EEC)
+
+ ec = strdupz(json_object_get_string(json_object_iter_peek_value(&it)));
+ json_object_iter_next(&it);
+ continue;
+ }
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_EDELAY)) {
+ if (json_object_get_type(json_object_iter_peek_value(&it)) != json_type_int) {
+ netdata_log_error("value of key " JSON_KEY_EDELAY " should be integer");
+ goto exit;
+ }
+
+ backoff = json_object_get_int(json_object_iter_peek_value(&it));
+ json_object_iter_next(&it);
+ continue;
+ }
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_ERTRY)) {
+ if (json_object_get_type(json_object_iter_peek_value(&it)) != json_type_boolean) {
+ netdata_log_error("value of key " JSON_KEY_ERTRY " should be integer");
+ goto exit;
+ }
+
+ block_retry = json_object_get_boolean(json_object_iter_peek_value(&it));
+ json_object_iter_next(&it);
+ continue;
+ }
+ netdata_log_error("Unknown key \"%s\" in error response payload. Ignoring", json_object_iter_peek_name(&it));
+ json_object_iter_next(&it);
+ }
+
+ if (block_retry > 0)
+ aclk_disable_runtime = 1;
+
+ if (backoff > 0)
+ aclk_block_until = now_monotonic_sec() + backoff;
+
+ netdata_log_error("Cloud returned EC=\"%s\", Msg-Key:\"%s\", Msg:\"%s\", BlockRetry:%s, Backoff:%ds (-1 unset by cloud)", ec, ek, emsg, block_retry > 0 ? "true" : "false", backoff);
+ rc = 0;
+exit:
+ json_object_put(json);
+ return rc;
+}
+#endif
+
+#if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_110
+static EVP_ENCODE_CTX *EVP_ENCODE_CTX_new(void)
+{
+ EVP_ENCODE_CTX *ctx = OPENSSL_malloc(sizeof(*ctx));
+
+ if (ctx != NULL) {
+ memset(ctx, 0, sizeof(*ctx));
+ }
+ return ctx;
+}
+static void EVP_ENCODE_CTX_free(EVP_ENCODE_CTX *ctx)
+{
+ OPENSSL_free(ctx);
+ return;
+}
+#endif
+
+#define CHALLENGE_LEN 256
+#define CHALLENGE_LEN_BASE64 344
+inline static int base64_decode_helper(unsigned char *out, int *outl, const unsigned char *in, int in_len)
+{
+ unsigned char remaining_data[CHALLENGE_LEN];
+ EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
+ EVP_DecodeInit(ctx);
+ EVP_DecodeUpdate(ctx, out, outl, in, in_len);
+ int remainder = 0;
+ EVP_DecodeFinal(ctx, remaining_data, &remainder);
+ EVP_ENCODE_CTX_free(ctx);
+ if (remainder) {
+ netdata_log_error("Unexpected data at EVP_DecodeFinal");
+ return 1;
+ }
+ return 0;
+}
+
+#define OTP_URL_PREFIX "/api/v1/auth/node/"
+int aclk_get_otp_challenge(url_t *target, const char *agent_id, unsigned char **challenge, int *challenge_bytes)
+{
+ int rc = 1;
+ https_req_t req = HTTPS_REQ_T_INITIALIZER;
+ https_req_response_t resp = HTTPS_REQ_RESPONSE_T_INITIALIZER;
+
+ BUFFER *url = buffer_create(strlen(OTP_URL_PREFIX) + UUID_STR_LEN + 20, &netdata_buffers_statistics.buffers_aclk);
+
+ req.host = target->host;
+ req.port = target->port;
+ buffer_sprintf(url, "%s/node/%s/challenge", target->path, agent_id);
+ req.url = (char *)buffer_tostring(url);
+
+ if (aclk_https_request(&req, &resp)) {
+ netdata_log_error("ACLK_OTP Challenge failed");
+ buffer_free(url);
+ return 1;
+ }
+ if (resp.http_code != 200) {
+ netdata_log_error("ACLK_OTP Challenge HTTP code not 200 OK (got %d)", resp.http_code);
+ buffer_free(url);
+ if (resp.payload_size)
+ aclk_parse_otp_error(resp.payload);
+ goto cleanup_resp;
+ }
+ buffer_free(url);
+
+ netdata_log_info("ACLK_OTP Got Challenge from Cloud");
+
+ json_object *json = json_tokener_parse(resp.payload);
+ if (!json) {
+ netdata_log_error("Couldn't parse HTTP GET challenge payload");
+ goto cleanup_resp;
+ }
+ json_object *challenge_json;
+ if (!json_object_object_get_ex(json, "challenge", &challenge_json)) {
+ netdata_log_error("No key named \"challenge\" in the returned JSON");
+ goto cleanup_json;
+ }
+ if (!json_object_is_type(challenge_json, json_type_string)) {
+ netdata_log_error("\"challenge\" is not a string JSON type");
+ goto cleanup_json;
+ }
+ const char *challenge_base64;
+ if (!(challenge_base64 = json_object_get_string(challenge_json))) {
+ netdata_log_error("Failed to extract challenge from JSON object");
+ goto cleanup_json;
+ }
+ if (strlen(challenge_base64) != CHALLENGE_LEN_BASE64) {
+ netdata_log_error("Received Challenge has unexpected length of %zu (expected %d)", strlen(challenge_base64), CHALLENGE_LEN_BASE64);
+ goto cleanup_json;
+ }
+
+ *challenge = mallocz((CHALLENGE_LEN_BASE64 / 4) * 3);
+ base64_decode_helper(*challenge, challenge_bytes, (const unsigned char*)challenge_base64, strlen(challenge_base64));
+ if (*challenge_bytes != CHALLENGE_LEN) {
+ netdata_log_error("Unexpected challenge length of %d instead of %d", *challenge_bytes, CHALLENGE_LEN);
+ freez(*challenge);
+ *challenge = NULL;
+ goto cleanup_json;
+ }
+ rc = 0;
+
+cleanup_json:
+ json_object_put(json);
+cleanup_resp:
+ https_req_response_free(&resp);
+ return rc;
+}
+
+int aclk_send_otp_response(const char *agent_id, const unsigned char *response, int response_bytes, url_t *target, struct auth_data *mqtt_auth)
+{
+ int len;
+ int rc = 1;
+ https_req_t req = HTTPS_REQ_T_INITIALIZER;
+ https_req_response_t resp = HTTPS_REQ_RESPONSE_T_INITIALIZER;
+
+ req.host = target->host;
+ req.port = target->port;
+ req.request_type = HTTP_REQ_POST;
+
+ unsigned char base64[CHALLENGE_LEN_BASE64 + 1];
+ memset(base64, 0, CHALLENGE_LEN_BASE64 + 1);
+
+ base64_encode_helper(base64, &len, response, response_bytes);
+
+ BUFFER *url = buffer_create(strlen(OTP_URL_PREFIX) + UUID_STR_LEN + 20, &netdata_buffers_statistics.buffers_aclk);
+ BUFFER *resp_json = buffer_create(strlen(OTP_URL_PREFIX) + UUID_STR_LEN + 20, &netdata_buffers_statistics.buffers_aclk);
+
+ buffer_sprintf(url, "%s/node/%s/password", target->path, agent_id);
+ buffer_sprintf(resp_json, "{\"response\":\"%s\"}", base64);
+
+ req.url = (char *)buffer_tostring(url);
+ req.payload = (char *)buffer_tostring(resp_json);
+ req.payload_size = strlen(req.payload);
+
+ if (aclk_https_request(&req, &resp)) {
+ netdata_log_error("ACLK_OTP Password error trying to post result to password");
+ goto cleanup_buffers;
+ }
+ if (resp.http_code != 201) {
+ netdata_log_error("ACLK_OTP Password HTTP code not 201 Created (got %d)", resp.http_code);
+ if (resp.payload_size)
+ aclk_parse_otp_error(resp.payload);
+ goto cleanup_response;
+ }
+ if (resp.payload_size == 0 || resp.payload == NULL) {
+ netdata_log_error("ACLK_OTP Password response payload is empty despite returning 201 Created!");
+ goto cleanup_response;
+ }
+ netdata_log_info("ACLK_OTP Got Password from Cloud");
+
+ if (parse_passwd_response(resp.payload, mqtt_auth)){
+ netdata_log_error("Error parsing response of password endpoint");
+ goto cleanup_response;
+ }
+
+ rc = 0;
+
+cleanup_response:
+ https_req_response_free(&resp);
+cleanup_buffers:
+ buffer_free(resp_json);
+ buffer_free(url);
+ return rc;
+}
+
+#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_300
+static int private_decrypt(EVP_PKEY *p_key, unsigned char * enc_data, int data_len, unsigned char **decrypted)
+#else
+static int private_decrypt(RSA *p_key, unsigned char * enc_data, int data_len, unsigned char **decrypted)
+#endif
+{
+ int result;
+#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_300
+ size_t outlen = EVP_PKEY_size(p_key);
+ EVP_PKEY_CTX *ctx = EVP_PKEY_CTX_new(p_key, NULL);
+ if (!ctx)
+ return 1;
+
+ if (EVP_PKEY_decrypt_init(ctx) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ return 1;
+ }
+
+ if (EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING) <= 0) {
+ EVP_PKEY_CTX_free(ctx);
+ return 1;
+ }
+
+ *decrypted = mallocz(outlen);
+
+ if (EVP_PKEY_decrypt(ctx, *decrypted, &outlen, enc_data, data_len) == 1)
+ result = (int) outlen;
+ else
+ result = -1;
+
+ EVP_PKEY_CTX_free(ctx);
+#else
+ *decrypted = mallocz(RSA_size(p_key));
+ result = RSA_private_decrypt(data_len, enc_data, *decrypted, p_key, RSA_PKCS1_OAEP_PADDING);
+#endif
+ if (result == -1)
+ {
+ char err[512];
+ ERR_error_string_n(ERR_get_error(), err, sizeof(err));
+ netdata_log_error("Decryption of the challenge failed: %s", err);
+ }
+ return result;
+}
+
+#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_300
+int aclk_get_mqtt_otp(EVP_PKEY *p_key, char **mqtt_id, char **mqtt_usr, char **mqtt_pass, url_t *target)
+#else
+int aclk_get_mqtt_otp(RSA *p_key, char **mqtt_id, char **mqtt_usr, char **mqtt_pass, url_t *target)
+#endif
+{
+ unsigned char *challenge = NULL;
+ int challenge_bytes;
+
+ char *agent_id = get_agent_claimid();
+ if (agent_id == NULL) {
+ netdata_log_error("Agent was not claimed - cannot perform challenge/response");
+ return 1;
+ }
+
+ // Get Challenge
+ if (aclk_get_otp_challenge(target, agent_id, &challenge, &challenge_bytes)) {
+ netdata_log_error("Error getting challenge");
+ freez(agent_id);
+ return 1;
+ }
+
+ // Decrypt Challenge / Get response
+ unsigned char *response_plaintext = NULL;
+ int response_plaintext_bytes = private_decrypt(p_key, challenge, challenge_bytes, &response_plaintext);
+ if (response_plaintext_bytes < 0) {
+ netdata_log_error("Couldn't decrypt the challenge received");
+ freez(response_plaintext);
+ freez(challenge);
+ freez(agent_id);
+ return 1;
+ }
+ freez(challenge);
+
+ // Encode and Send Challenge
+ struct auth_data data = { .client_id = NULL, .passwd = NULL, .username = NULL };
+ if (aclk_send_otp_response(agent_id, response_plaintext, response_plaintext_bytes, target, &data)) {
+ netdata_log_error("Error getting response");
+ freez(response_plaintext);
+ freez(agent_id);
+ return 1;
+ }
+
+ *mqtt_pass = data.passwd;
+ *mqtt_usr = data.username;
+ *mqtt_id = data.client_id;
+
+ freez(response_plaintext);
+ freez(agent_id);
+ return 0;
+}
+
+#define JSON_KEY_ENC "encoding"
+#define JSON_KEY_AUTH_ENDPOINT "authEndpoint"
+#define JSON_KEY_TRP "transports"
+#define JSON_KEY_TRP_TYPE "type"
+#define JSON_KEY_TRP_ENDPOINT "endpoint"
+#define JSON_KEY_BACKOFF "backoff"
+#define JSON_KEY_BACKOFF_BASE "base"
+#define JSON_KEY_BACKOFF_MAX "maxSeconds"
+#define JSON_KEY_BACKOFF_MIN "minSeconds"
+#define JSON_KEY_CAPS "capabilities"
+
+static int parse_json_env_transport(json_object *json, aclk_transport_desc_t *trp) {
+ struct json_object_iterator it;
+ struct json_object_iterator itEnd;
+
+ it = json_object_iter_begin(json);
+ itEnd = json_object_iter_end(json);
+
+ while (!json_object_iter_equal(&it, &itEnd)) {
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_TRP_TYPE)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_TRP_TYPE)
+ if (trp->type != ACLK_TRP_UNKNOWN) {
+ netdata_log_error(JSON_KEY_TRP_TYPE " set already");
+ goto exit;
+ }
+ trp->type = aclk_transport_type_t_from_str(json_object_get_string(json_object_iter_peek_value(&it)));
+ if (trp->type == ACLK_TRP_UNKNOWN) {
+ netdata_log_error(JSON_KEY_TRP_TYPE " unknown type \"%s\"", json_object_get_string(json_object_iter_peek_value(&it)));
+ goto exit;
+ }
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_TRP_ENDPOINT)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_TRP_ENDPOINT)
+ if (trp->endpoint) {
+ netdata_log_error(JSON_KEY_TRP_ENDPOINT " set already");
+ goto exit;
+ }
+ trp->endpoint = strdupz(json_object_get_string(json_object_iter_peek_value(&it)));
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ netdata_log_error("unknown JSON key in dictionary (\"%s\")", json_object_iter_peek_name(&it));
+ json_object_iter_next(&it);
+ }
+
+ if (!trp->endpoint) {
+ netdata_log_error(JSON_KEY_TRP_ENDPOINT " is missing from JSON dictionary");
+ goto exit;
+ }
+
+ if (trp->type == ACLK_TRP_UNKNOWN) {
+ netdata_log_error("transport type not set");
+ goto exit;
+ }
+
+ return 0;
+
+exit:
+ aclk_transport_desc_t_destroy(trp);
+ return 1;
+}
+
+static int parse_json_env_transports(json_object *json_array, aclk_env_t *env) {
+ aclk_transport_desc_t *trp;
+ json_object *obj;
+
+ if (env->transports) {
+ netdata_log_error("transports have been set already");
+ return 1;
+ }
+
+ env->transport_count = json_object_array_length(json_array);
+
+ env->transports = callocz(env->transport_count , sizeof(aclk_transport_desc_t *));
+
+ for (size_t i = 0; i < env->transport_count; i++) {
+ trp = callocz(1, sizeof(aclk_transport_desc_t));
+ obj = json_object_array_get_idx(json_array, i);
+ if (parse_json_env_transport(obj, trp)) {
+ netdata_log_error("error parsing transport idx %d", (int)i);
+ freez(trp);
+ return 1;
+ }
+ env->transports[i] = trp;
+ }
+
+ return 0;
+}
+
+#define MATCHED_CORRECT 1
+#define MATCHED_ERROR -1
+#define NOT_MATCHED 0
+static int parse_json_backoff_int(struct json_object_iterator *it, int *out, const char* name, int min, int max) {
+ if (!strcmp(json_object_iter_peek_name(it), name)) {
+ if (json_object_get_type(json_object_iter_peek_value(it)) != json_type_int) {
+ netdata_log_error("Could not parse \"%s\". Not an integer as expected.", name);
+ return MATCHED_ERROR;
+ }
+
+ *out = json_object_get_int(json_object_iter_peek_value(it));
+
+ if (*out < min || *out > max) {
+ netdata_log_error("Value of \"%s\"=%d out of range (%d-%d).", name, *out, min, max);
+ return MATCHED_ERROR;
+ }
+
+ return MATCHED_CORRECT;
+ }
+ return NOT_MATCHED;
+}
+
+static int parse_json_backoff(json_object *json, aclk_backoff_t *backoff) {
+ struct json_object_iterator it;
+ struct json_object_iterator itEnd;
+ int ret;
+
+ it = json_object_iter_begin(json);
+ itEnd = json_object_iter_end(json);
+
+ while (!json_object_iter_equal(&it, &itEnd)) {
+ if ( (ret = parse_json_backoff_int(&it, &backoff->base, JSON_KEY_BACKOFF_BASE, 1, 10)) ) {
+ if (ret == MATCHED_ERROR) {
+ return 1;
+ }
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ if ( (ret = parse_json_backoff_int(&it, &backoff->max_s, JSON_KEY_BACKOFF_MAX, 500, INT_MAX)) ) {
+ if (ret == MATCHED_ERROR) {
+ return 1;
+ }
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ if ( (ret = parse_json_backoff_int(&it, &backoff->min_s, JSON_KEY_BACKOFF_MIN, 0, INT_MAX)) ) {
+ if (ret == MATCHED_ERROR) {
+ return 1;
+ }
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ netdata_log_error("unknown JSON key in dictionary (\"%s\")", json_object_iter_peek_name(&it));
+ json_object_iter_next(&it);
+ }
+
+ return 0;
+}
+
+static int parse_json_env_caps(json_object *json, aclk_env_t *env) {
+ json_object *obj;
+ const char *str;
+
+ if (env->capabilities) {
+ netdata_log_error("transports have been set already");
+ return 1;
+ }
+
+ env->capability_count = json_object_array_length(json);
+
+ // empty capabilities list is allowed
+ if (!env->capability_count)
+ return 0;
+
+ env->capabilities = callocz(env->capability_count , sizeof(char *));
+
+ for (size_t i = 0; i < env->capability_count; i++) {
+ obj = json_object_array_get_idx(json, i);
+ if (json_object_get_type(obj) != json_type_string) {
+ netdata_log_error("Capability at index %d not a string!", (int)i);
+ return 1;
+ }
+ str = json_object_get_string(obj);
+ if (!str) {
+ netdata_log_error("Error parsing capabilities");
+ return 1;
+ }
+ env->capabilities[i] = strdupz(str);
+ }
+
+ return 0;
+}
+
+static int parse_json_env(const char *json_str, aclk_env_t *env) {
+ json_object *json;
+ struct json_object_iterator it;
+ struct json_object_iterator itEnd;
+
+ json = json_tokener_parse(json_str);
+ if (!json) {
+ netdata_log_error("JSON-C failed to parse the payload of http response of /env endpoint");
+ return 1;
+ }
+
+ it = json_object_iter_begin(json);
+ itEnd = json_object_iter_end(json);
+
+ while (!json_object_iter_equal(&it, &itEnd)) {
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_AUTH_ENDPOINT)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_AUTH_ENDPOINT)
+ if (env->auth_endpoint) {
+ netdata_log_error("authEndpoint set already");
+ goto exit;
+ }
+ env->auth_endpoint = strdupz(json_object_get_string(json_object_iter_peek_value(&it)));
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_ENC)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_string, JSON_KEY_ENC)
+ if (env->encoding != ACLK_ENC_UNKNOWN) {
+ netdata_log_error(JSON_KEY_ENC " set already");
+ goto exit;
+ }
+ env->encoding = aclk_encoding_type_t_from_str(json_object_get_string(json_object_iter_peek_value(&it)));
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_TRP)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_array, JSON_KEY_TRP)
+
+ json_object *now = json_object_iter_peek_value(&it);
+ parse_json_env_transports(now, env);
+
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_BACKOFF)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_object, JSON_KEY_BACKOFF)
+
+ if (parse_json_backoff(json_object_iter_peek_value(&it), &env->backoff)) {
+ env->backoff.base = 0;
+ netdata_log_error("Error parsing Backoff parameters in env");
+ goto exit;
+ }
+
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_KEY_CAPS)) {
+ PARSE_ENV_JSON_CHK_TYPE(&it, json_type_array, JSON_KEY_CAPS)
+
+ if (parse_json_env_caps(json_object_iter_peek_value(&it), env)) {
+ netdata_log_error("Error parsing capabilities list");
+ goto exit;
+ }
+
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ netdata_log_error("unknown JSON key in dictionary (\"%s\")", json_object_iter_peek_name(&it));
+ json_object_iter_next(&it);
+ }
+
+ // Check all compulsory keys have been set
+ if (env->transport_count < 1) {
+ netdata_log_error("env has to return at least one transport");
+ goto exit;
+ }
+ if (!env->auth_endpoint) {
+ netdata_log_error(JSON_KEY_AUTH_ENDPOINT " is compulsory");
+ goto exit;
+ }
+ if (env->encoding == ACLK_ENC_UNKNOWN) {
+ netdata_log_error(JSON_KEY_ENC " is compulsory");
+ goto exit;
+ }
+ if (!env->backoff.base) {
+ netdata_log_error(JSON_KEY_BACKOFF " is compulsory");
+ goto exit;
+ }
+
+ json_object_put(json);
+ return 0;
+
+exit:
+ aclk_env_t_destroy(env);
+ json_object_put(json);
+ return 1;
+}
+
+int aclk_get_env(aclk_env_t *env, const char* aclk_hostname, int aclk_port) {
+ BUFFER *buf = buffer_create(1024, &netdata_buffers_statistics.buffers_aclk);
+
+ https_req_t req = HTTPS_REQ_T_INITIALIZER;
+ https_req_response_t resp = HTTPS_REQ_RESPONSE_T_INITIALIZER;
+
+ req.request_type = HTTP_REQ_GET;
+
+ char *agent_id = get_agent_claimid();
+ if (agent_id == NULL)
+ {
+ netdata_log_error("Agent was not claimed - cannot perform challenge/response");
+ buffer_free(buf);
+ return 1;
+ }
+
+ buffer_sprintf(buf, "/api/v1/env?v=%s&cap=proto,ctx&claim_id=%s", &(NETDATA_VERSION[1]) /* skip 'v' at beginning */, agent_id);
+
+ freez(agent_id);
+
+ req.host = (char*)aclk_hostname;
+ req.port = aclk_port;
+ req.url = buf->buffer;
+ if (aclk_https_request(&req, &resp)) {
+ netdata_log_error("Error trying to contact env endpoint");
+ https_req_response_free(&resp);
+ buffer_free(buf);
+ return 2;
+ }
+ if (resp.http_code != 200) {
+ netdata_log_error("The HTTP code not 200 OK (Got %d)", resp.http_code);
+ if (resp.payload_size)
+ aclk_parse_otp_error(resp.payload);
+ https_req_response_free(&resp);
+ buffer_free(buf);
+ return 3;
+ }
+
+ if (!resp.payload || !resp.payload_size) {
+ netdata_log_error("Unexpected empty payload as response to /env call");
+ https_req_response_free(&resp);
+ buffer_free(buf);
+ return 4;
+ }
+
+ if (parse_json_env(resp.payload, env)) {
+ netdata_log_error("error parsing /env message");
+ https_req_response_free(&resp);
+ buffer_free(buf);
+ return 5;
+ }
+
+ netdata_log_info("Getting Cloud /env successful");
+
+ https_req_response_free(&resp);
+ buffer_free(buf);
+ return 0;
+}
diff --git a/src/aclk/aclk_otp.h b/src/aclk/aclk_otp.h
new file mode 100644
index 000000000..2d660e5a4
--- /dev/null
+++ b/src/aclk/aclk_otp.h
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_OTP_H
+#define ACLK_OTP_H
+
+#include "daemon/common.h"
+
+#include "https_client.h"
+#include "aclk_util.h"
+
+#if OPENSSL_VERSION_NUMBER >= OPENSSL_VERSION_300
+int aclk_get_mqtt_otp(EVP_PKEY *p_key, char **mqtt_id, char **mqtt_usr, char **mqtt_pass, url_t *target);
+#else
+int aclk_get_mqtt_otp(RSA *p_key, char **mqtt_id, char **mqtt_usr, char **mqtt_pass, url_t *target);
+#endif
+int aclk_get_env(aclk_env_t *env, const char *aclk_hostname, int aclk_port);
+
+#endif /* ACLK_OTP_H */
diff --git a/src/aclk/aclk_proxy.c b/src/aclk/aclk_proxy.c
new file mode 100644
index 000000000..8d0e2d657
--- /dev/null
+++ b/src/aclk/aclk_proxy.c
@@ -0,0 +1,177 @@
+#include "aclk_proxy.h"
+
+#include "daemon/common.h"
+
+#define ACLK_PROXY_ENV "env"
+#define ACLK_PROXY_CONFIG_VAR "proxy"
+
+struct {
+ ACLK_PROXY_TYPE type;
+ const char *url_str;
+} supported_proxy_types[] = {
+ { .type = PROXY_TYPE_SOCKS5, .url_str = "socks5" ACLK_PROXY_PROTO_ADDR_SEPARATOR },
+ { .type = PROXY_TYPE_SOCKS5, .url_str = "socks5h" ACLK_PROXY_PROTO_ADDR_SEPARATOR },
+ { .type = PROXY_TYPE_HTTP, .url_str = "http" ACLK_PROXY_PROTO_ADDR_SEPARATOR },
+ { .type = PROXY_TYPE_UNKNOWN, .url_str = NULL },
+};
+
+static inline ACLK_PROXY_TYPE aclk_find_proxy(const char *string)
+{
+ int i = 0;
+ while (supported_proxy_types[i].url_str) {
+ if (!strncmp(supported_proxy_types[i].url_str, string, strlen(supported_proxy_types[i].url_str)))
+ return supported_proxy_types[i].type;
+ i++;
+ }
+ return PROXY_TYPE_UNKNOWN;
+}
+
+ACLK_PROXY_TYPE aclk_verify_proxy(const char *string)
+{
+ if (!string)
+ return PROXY_TYPE_UNKNOWN;
+
+ while (*string == 0x20)
+ string++;
+
+ if (!*string)
+ return PROXY_TYPE_UNKNOWN;
+
+ return aclk_find_proxy(string);
+}
+
+// helper function to censor user&password
+// for logging purposes
+void safe_log_proxy_censor(char *proxy)
+{
+ size_t length = strlen(proxy);
+ char *auth = proxy + length - 1;
+ char *cur;
+
+ while ((auth >= proxy) && (*auth != '@'))
+ auth--;
+
+ //if not found or @ is first char do nothing
+ if (auth <= proxy)
+ return;
+
+ cur = strstr(proxy, ACLK_PROXY_PROTO_ADDR_SEPARATOR);
+ if (!cur)
+ cur = proxy;
+ else
+ cur += strlen(ACLK_PROXY_PROTO_ADDR_SEPARATOR);
+
+ while (cur < auth) {
+ *cur = 'X';
+ cur++;
+ }
+}
+
+static inline void safe_log_proxy_error(char *str, const char *proxy)
+{
+ char *log = strdupz(proxy);
+ safe_log_proxy_censor(log);
+ netdata_log_error("%s Provided Value:\"%s\"", str, log);
+ freez(log);
+}
+
+static inline int check_socks_enviroment(const char **proxy)
+{
+ char *tmp = getenv("socks_proxy");
+
+ if (!tmp)
+ return 1;
+
+ if (aclk_verify_proxy(tmp) == PROXY_TYPE_SOCKS5) {
+ *proxy = tmp;
+ return 0;
+ }
+
+ safe_log_proxy_error(
+ "Environment var \"socks_proxy\" defined but of unknown format. Supported syntax: \"socks5[h]://[user:pass@]host:ip\".",
+ tmp);
+ return 1;
+}
+
+static inline int check_http_enviroment(const char **proxy)
+{
+ char *tmp = getenv("http_proxy");
+
+ if (!tmp)
+ return 1;
+
+ if (aclk_verify_proxy(tmp) == PROXY_TYPE_HTTP) {
+ *proxy = tmp;
+ return 0;
+ }
+
+ safe_log_proxy_error(
+ "Environment var \"http_proxy\" defined but of unknown format. Supported syntax: \"http[s]://[user:pass@]host:ip\".",
+ tmp);
+ return 1;
+}
+
+const char *aclk_lws_wss_get_proxy_setting(ACLK_PROXY_TYPE *type)
+{
+ const char *proxy = appconfig_get(&cloud_config, CONFIG_SECTION_GLOBAL, ACLK_PROXY_CONFIG_VAR, ACLK_PROXY_ENV);
+
+ // backward compatibility: "proxy" was in "netdata.conf"
+ if (config_exists(CONFIG_SECTION_CLOUD, ACLK_PROXY_CONFIG_VAR))
+ proxy = config_get(CONFIG_SECTION_CLOUD, ACLK_PROXY_CONFIG_VAR, ACLK_PROXY_ENV);
+
+ *type = PROXY_DISABLED;
+
+ if (strcmp(proxy, "none") == 0)
+ return proxy;
+
+ if (strcmp(proxy, ACLK_PROXY_ENV) == 0) {
+ if (check_socks_enviroment(&proxy) == 0) {
+#ifdef LWS_WITH_SOCKS5
+ *type = PROXY_TYPE_SOCKS5;
+ return proxy;
+#else
+ safe_log_proxy_error("socks_proxy environment variable set to use SOCKS5 proxy "
+ "but Libwebsockets used doesn't have SOCKS5 support built in. "
+ "Ignoring and checking for other options.",
+ proxy);
+#endif
+ }
+ if (check_http_enviroment(&proxy) == 0)
+ *type = PROXY_TYPE_HTTP;
+ return proxy;
+ }
+
+ *type = aclk_verify_proxy(proxy);
+#ifndef LWS_WITH_SOCKS5
+ if (*type == PROXY_TYPE_SOCKS5) {
+ safe_log_proxy_error(
+ "Config var \"" ACLK_PROXY_CONFIG_VAR
+ "\" set to use SOCKS5 proxy but Libwebsockets used is built without support for SOCKS proxy. ACLK will be disabled.",
+ proxy);
+ }
+#endif
+ if (*type == PROXY_TYPE_UNKNOWN) {
+ *type = PROXY_DISABLED;
+ safe_log_proxy_error(
+ "Config var \"" ACLK_PROXY_CONFIG_VAR
+ "\" defined but of unknown format. Supported syntax: \"socks5[h]://[user:pass@]host:ip\".",
+ proxy);
+ }
+
+ return proxy;
+}
+
+// helper function to read settings only once (static)
+// as claiming, challenge/response and ACLK
+// read the same thing, no need to parse again
+const char *aclk_get_proxy(ACLK_PROXY_TYPE *type)
+{
+ static const char *proxy = NULL;
+ static ACLK_PROXY_TYPE proxy_type = PROXY_NOT_SET;
+
+ if (proxy_type == PROXY_NOT_SET)
+ proxy = aclk_lws_wss_get_proxy_setting(&proxy_type);
+
+ *type = proxy_type;
+ return proxy;
+}
diff --git a/src/aclk/aclk_proxy.h b/src/aclk/aclk_proxy.h
new file mode 100644
index 000000000..6877b526b
--- /dev/null
+++ b/src/aclk/aclk_proxy.h
@@ -0,0 +1,21 @@
+#ifndef ACLK_PROXY_H
+#define ACLK_PROXY_H
+
+#include <config.h>
+
+#define ACLK_PROXY_PROTO_ADDR_SEPARATOR "://"
+
+typedef enum aclk_proxy_type {
+ PROXY_TYPE_UNKNOWN = 0,
+ PROXY_TYPE_SOCKS5,
+ PROXY_TYPE_HTTP,
+ PROXY_DISABLED,
+ PROXY_NOT_SET,
+} ACLK_PROXY_TYPE;
+
+ACLK_PROXY_TYPE aclk_verify_proxy(const char *string);
+const char *aclk_lws_wss_get_proxy_setting(ACLK_PROXY_TYPE *type);
+void safe_log_proxy_censor(char *proxy);
+const char *aclk_get_proxy(ACLK_PROXY_TYPE *type);
+
+#endif /* ACLK_PROXY_H */
diff --git a/src/aclk/aclk_query.c b/src/aclk/aclk_query.c
new file mode 100644
index 000000000..08bc2acf3
--- /dev/null
+++ b/src/aclk/aclk_query.c
@@ -0,0 +1,373 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_query.h"
+#include "aclk_stats.h"
+#include "aclk_tx_msgs.h"
+#include "../../web/server/web_client_cache.h"
+
+#define WEB_HDR_ACCEPT_ENC "Accept-Encoding:"
+
+pthread_cond_t query_cond_wait = PTHREAD_COND_INITIALIZER;
+pthread_mutex_t query_lock_wait = PTHREAD_MUTEX_INITIALIZER;
+#define QUERY_THREAD_LOCK pthread_mutex_lock(&query_lock_wait)
+#define QUERY_THREAD_UNLOCK pthread_mutex_unlock(&query_lock_wait)
+
+struct pending_req_list {
+ const char *msg_id;
+ uint32_t hash;
+
+ int canceled;
+
+ struct pending_req_list *next;
+};
+
+static struct pending_req_list *pending_req_list_head = NULL;
+static pthread_mutex_t pending_req_list_lock = PTHREAD_MUTEX_INITIALIZER;
+
+static struct pending_req_list *pending_req_list_add(const char *msg_id)
+{
+ struct pending_req_list *new = callocz(1, sizeof(struct pending_req_list));
+ new->msg_id = msg_id;
+ new->hash = simple_hash(msg_id);
+
+ pthread_mutex_lock(&pending_req_list_lock);
+ new->next = pending_req_list_head;
+ pending_req_list_head = new;
+ pthread_mutex_unlock(&pending_req_list_lock);
+ return new;
+}
+
+void pending_req_list_rm(const char *msg_id)
+{
+ uint32_t hash = simple_hash(msg_id);
+ struct pending_req_list *prev = NULL;
+
+ pthread_mutex_lock(&pending_req_list_lock);
+ struct pending_req_list *curr = pending_req_list_head;
+
+ while (curr) {
+ if (curr->hash == hash && strcmp(curr->msg_id, msg_id) == 0) {
+ if (prev)
+ prev->next = curr->next;
+ else
+ pending_req_list_head = curr->next;
+
+ freez(curr);
+ break;
+ }
+
+ prev = curr;
+ curr = curr->next;
+ }
+ pthread_mutex_unlock(&pending_req_list_lock);
+}
+
+int mark_pending_req_cancelled(const char *msg_id)
+{
+ uint32_t hash = simple_hash(msg_id);
+
+ pthread_mutex_lock(&pending_req_list_lock);
+ struct pending_req_list *curr = pending_req_list_head;
+
+ while (curr) {
+ if (curr->hash == hash && strcmp(curr->msg_id, msg_id) == 0) {
+ curr->canceled = 1;
+ pthread_mutex_unlock(&pending_req_list_lock);
+ return 0;
+ }
+
+ curr = curr->next;
+ }
+ pthread_mutex_unlock(&pending_req_list_lock);
+ return 1;
+}
+
+static bool aclk_web_client_interrupt_cb(struct web_client *w __maybe_unused, void *data)
+{
+ struct pending_req_list *req = (struct pending_req_list *)data;
+ return req->canceled;
+}
+
+static int http_api_v2(struct aclk_query_thread *query_thr, aclk_query_t query) {
+ ND_LOG_STACK lgs[] = {
+ ND_LOG_FIELD_TXT(NDF_SRC_TRANSPORT, "aclk"),
+ ND_LOG_FIELD_END(),
+ };
+ ND_LOG_STACK_PUSH(lgs);
+
+ int retval = 0;
+ BUFFER *local_buffer = NULL;
+ size_t size = 0;
+ size_t sent = 0;
+ usec_t dt_ut = 0;
+
+ int z_ret;
+ BUFFER *z_buffer = buffer_create(NETDATA_WEB_RESPONSE_INITIAL_SIZE, &netdata_buffers_statistics.buffers_aclk);
+
+ struct web_client *w = web_client_get_from_cache();
+ web_client_set_conn_cloud(w);
+ w->port_acl = HTTP_ACL_ACLK | HTTP_ACL_ALL_FEATURES;
+ w->acl = w->port_acl;
+ web_client_set_permissions(w, HTTP_ACCESS_MAP_OLD_MEMBER, HTTP_USER_ROLE_MEMBER, WEB_CLIENT_FLAG_AUTH_CLOUD);
+
+ w->mode = HTTP_REQUEST_MODE_GET;
+ w->timings.tv_in = query->created_tv;
+
+ w->interrupt.callback = aclk_web_client_interrupt_cb;
+ w->interrupt.callback_data = pending_req_list_add(query->msg_id);
+
+ buffer_flush(w->response.data);
+ buffer_strcat(w->response.data, query->data.http_api_v2.payload);
+
+ HTTP_VALIDATION validation = http_request_validate(w);
+ if(validation != HTTP_VALIDATION_OK) {
+ nd_log(NDLS_ACCESS, NDLP_ERR, "ACLK received request is not valid, code %d", validation);
+ retval = 1;
+ w->response.code = HTTP_RESP_BAD_REQUEST;
+ w->response.code = (short)aclk_http_msg_v2(query_thr->client, query->callback_topic, query->msg_id,
+ dt_ut, query->created, w->response.code,
+ NULL, 0);
+ goto cleanup;
+ }
+
+ web_client_timeout_checkpoint_set(w, query->timeout);
+ if(web_client_timeout_checkpoint_and_check(w, &dt_ut)) {
+ nd_log(NDLS_ACCESS, NDLP_ERR,
+ "QUERY CANCELED: QUEUE TIME EXCEEDED %llu ms (LIMIT %d ms)",
+ dt_ut / USEC_PER_MS, query->timeout);
+ retval = 1;
+ w->response.code = HTTP_RESP_SERVICE_UNAVAILABLE;
+ aclk_http_msg_v2_err(query_thr->client, query->callback_topic, query->msg_id, w->response.code, CLOUD_EC_SND_TIMEOUT, CLOUD_EMSG_SND_TIMEOUT, NULL, 0);
+ goto cleanup;
+ }
+
+ char *path = (char *)buffer_tostring(w->url_path_decoded);
+
+ if (aclk_stats_enabled) {
+ char *url_path_endpoint = strrchr(path, '/');
+ ACLK_STATS_LOCK;
+ int stat_idx = aclk_cloud_req_http_type_to_idx(url_path_endpoint ? url_path_endpoint + 1 : "other");
+ aclk_metrics_per_sample.cloud_req_http_by_type[stat_idx]++;
+ ACLK_STATS_UNLOCK;
+ }
+
+ w->response.code = (short)web_client_api_request_with_node_selection(localhost, w, path);
+ web_client_timeout_checkpoint_response_ready(w, &dt_ut);
+
+ if (aclk_stats_enabled) {
+ ACLK_STATS_LOCK;
+ aclk_metrics_per_sample.cloud_q_process_total += dt_ut;
+ aclk_metrics_per_sample.cloud_q_process_count++;
+ if (aclk_metrics_per_sample.cloud_q_process_max < dt_ut)
+ aclk_metrics_per_sample.cloud_q_process_max = dt_ut;
+ ACLK_STATS_UNLOCK;
+ }
+
+ size = w->response.data->len;
+ sent = size;
+
+ if (w->response.data->len && w->response.zinitialized) {
+ w->response.zstream.next_in = (Bytef *)w->response.data->buffer;
+ w->response.zstream.avail_in = w->response.data->len;
+
+ do {
+ w->response.zstream.avail_out = NETDATA_WEB_RESPONSE_ZLIB_CHUNK_SIZE;
+ w->response.zstream.next_out = w->response.zbuffer;
+ z_ret = deflate(&w->response.zstream, Z_FINISH);
+ if(z_ret < 0) {
+ if(w->response.zstream.msg)
+ netdata_log_error("Error compressing body. ZLIB error: \"%s\"", w->response.zstream.msg);
+ else
+ netdata_log_error("Unknown error during zlib compression.");
+ retval = 1;
+ w->response.code = 500;
+ aclk_http_msg_v2_err(query_thr->client, query->callback_topic, query->msg_id, w->response.code, CLOUD_EC_ZLIB_ERROR, CLOUD_EMSG_ZLIB_ERROR, NULL, 0);
+ goto cleanup;
+ }
+ int bytes_to_cpy = NETDATA_WEB_RESPONSE_ZLIB_CHUNK_SIZE - w->response.zstream.avail_out;
+ buffer_need_bytes(z_buffer, bytes_to_cpy);
+ memcpy(&z_buffer->buffer[z_buffer->len], w->response.zbuffer, bytes_to_cpy);
+ z_buffer->len += bytes_to_cpy;
+ } while(z_ret != Z_STREAM_END);
+
+ // so that web_client_build_http_header
+ // puts correct content length into header
+ buffer_free(w->response.data);
+ w->response.data = z_buffer;
+ z_buffer = NULL;
+ }
+
+ web_client_build_http_header(w);
+ local_buffer = buffer_create(NETDATA_WEB_RESPONSE_INITIAL_SIZE, &netdata_buffers_statistics.buffers_aclk);
+ local_buffer->content_type = CT_APPLICATION_JSON;
+
+ buffer_strcat(local_buffer, w->response.header_output->buffer);
+
+ if (w->response.data->len) {
+ if (w->response.zinitialized) {
+ buffer_need_bytes(local_buffer, w->response.data->len);
+ memcpy(&local_buffer->buffer[local_buffer->len], w->response.data->buffer, w->response.data->len);
+ local_buffer->len += w->response.data->len;
+ sent = sent - size + w->response.data->len;
+ } else {
+ buffer_strcat(local_buffer, w->response.data->buffer);
+ }
+ }
+
+ // send msg.
+ w->response.code = (short)aclk_http_msg_v2(query_thr->client, query->callback_topic, query->msg_id,
+ dt_ut, query->created, w->response.code,
+ local_buffer->buffer, local_buffer->len);
+
+cleanup:
+ web_client_log_completed_request(w, false);
+ web_client_release_to_cache(w);
+
+ pending_req_list_rm(query->msg_id);
+
+ buffer_free(z_buffer);
+ buffer_free(local_buffer);
+ return retval;
+}
+
+static int send_bin_msg(struct aclk_query_thread *query_thr, aclk_query_t query)
+{
+ // this will be simplified when legacy support is removed
+ aclk_send_bin_message_subtopic_pid(query_thr->client, query->data.bin_payload.payload, query->data.bin_payload.size, query->data.bin_payload.topic, query->data.bin_payload.msg_name);
+ return 0;
+}
+
+const char *aclk_query_get_name(aclk_query_type_t qt, int unknown_ok)
+{
+ switch (qt) {
+ case HTTP_API_V2: return "http_api_request_v2";
+ case REGISTER_NODE: return "register_node";
+ case NODE_STATE_UPDATE: return "node_state_update";
+ case CHART_DIMS_UPDATE: return "chart_and_dim_update";
+ case CHART_CONFIG_UPDATED: return "chart_config_updated";
+ case CHART_RESET: return "reset_chart_messages";
+ case RETENTION_UPDATED: return "update_retention_info";
+ case UPDATE_NODE_INFO: return "update_node_info";
+ case ALARM_PROVIDE_CHECKPOINT: return "alarm_checkpoint";
+ case ALARM_PROVIDE_CFG: return "provide_alarm_config";
+ case ALARM_SNAPSHOT: return "alarm_snapshot";
+ case UPDATE_NODE_COLLECTORS: return "update_node_collectors";
+ case PROTO_BIN_MESSAGE: return "generic_binary_proto_message";
+ default:
+ if (!unknown_ok)
+ error_report("Unknown query type used %d", (int) qt);
+ return "unknown";
+ }
+}
+
+static void aclk_query_process_msg(struct aclk_query_thread *query_thr, aclk_query_t query)
+{
+ if (query->type == UNKNOWN || query->type >= ACLK_QUERY_TYPE_COUNT) {
+ error_report("Unknown query in query queue. %u", query->type);
+ aclk_query_free(query);
+ return;
+ }
+
+ worker_is_busy(query->type);
+ if (query->type == HTTP_API_V2) {
+ netdata_log_debug(D_ACLK, "Processing Queued Message of type: \"http_api_request_v2\"");
+ http_api_v2(query_thr, query);
+ } else {
+ netdata_log_debug(D_ACLK, "Processing Queued Message of type: \"%s\"", query->data.bin_payload.msg_name);
+ send_bin_msg(query_thr, query);
+ }
+
+ if (aclk_stats_enabled) {
+ ACLK_STATS_LOCK;
+ aclk_metrics_per_sample.queries_dispatched++;
+ aclk_queries_per_thread[query_thr->idx]++;
+ aclk_metrics_per_sample.queries_per_type[query->type]++;
+ ACLK_STATS_UNLOCK;
+ }
+
+ aclk_query_free(query);
+
+ worker_is_idle();
+}
+
+/* Processes messages from queue. Compete for work with other threads
+ */
+int aclk_query_process_msgs(struct aclk_query_thread *query_thr)
+{
+ aclk_query_t query;
+ while ((query = aclk_queue_pop()))
+ aclk_query_process_msg(query_thr, query);
+
+ return 0;
+}
+
+static void worker_aclk_register(void) {
+ worker_register("ACLKQUERY");
+ for (int i = 1; i < ACLK_QUERY_TYPE_COUNT; i++) {
+ worker_register_job_name(i, aclk_query_get_name(i, 0));
+ }
+}
+
+static void aclk_query_request_cancel(void *data)
+{
+ pthread_cond_broadcast((pthread_cond_t *) data);
+}
+
+/**
+ * Main query processing thread
+ */
+void *aclk_query_main_thread(void *ptr)
+{
+ worker_aclk_register();
+
+ struct aclk_query_thread *query_thr = ptr;
+
+ service_register(SERVICE_THREAD_TYPE_NETDATA, aclk_query_request_cancel, NULL, &query_cond_wait, false);
+
+ while (service_running(SERVICE_ACLK | ABILITY_DATA_QUERIES)) {
+ aclk_query_process_msgs(query_thr);
+
+ worker_is_idle();
+ QUERY_THREAD_LOCK;
+ if (unlikely(pthread_cond_wait(&query_cond_wait, &query_lock_wait)))
+ sleep_usec(USEC_PER_SEC * 1);
+ QUERY_THREAD_UNLOCK;
+ }
+
+ worker_unregister();
+ return NULL;
+}
+
+#define TASK_LEN_MAX 22
+void aclk_query_threads_start(struct aclk_query_threads *query_threads, mqtt_wss_client client)
+{
+ netdata_log_info("Starting %d query threads.", query_threads->count);
+
+ char thread_name[TASK_LEN_MAX];
+ query_threads->thread_list = callocz(query_threads->count, sizeof(struct aclk_query_thread));
+ for (int i = 0; i < query_threads->count; i++) {
+ query_threads->thread_list[i].idx = i; //thread needs to know its index for statistics
+ query_threads->thread_list[i].client = client;
+
+ if(unlikely(snprintfz(thread_name, TASK_LEN_MAX, "ACLK_QRY[%d]", i) < 0))
+ netdata_log_error("snprintf encoding error");
+
+ query_threads->thread_list[i].thread = nd_thread_create(
+ thread_name,
+ NETDATA_THREAD_OPTION_JOINABLE,
+ aclk_query_main_thread,
+ &query_threads->thread_list[i]);
+ }
+}
+
+void aclk_query_threads_cleanup(struct aclk_query_threads *query_threads)
+{
+ if (query_threads && query_threads->thread_list) {
+ for (int i = 0; i < query_threads->count; i++) {
+ nd_thread_join(query_threads->thread_list[i].thread);
+ }
+ freez(query_threads->thread_list);
+ }
+ aclk_queue_lock();
+ aclk_queue_flush();
+}
diff --git a/src/aclk/aclk_query.h b/src/aclk/aclk_query.h
new file mode 100644
index 000000000..900583237
--- /dev/null
+++ b/src/aclk/aclk_query.h
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef NETDATA_ACLK_QUERY_H
+#define NETDATA_ACLK_QUERY_H
+
+#include "libnetdata/libnetdata.h"
+
+#include "mqtt_websockets/mqtt_wss_client.h"
+
+#include "aclk_query_queue.h"
+
+extern pthread_cond_t query_cond_wait;
+extern pthread_mutex_t query_lock_wait;
+#define QUERY_THREAD_WAKEUP pthread_cond_signal(&query_cond_wait)
+#define QUERY_THREAD_WAKEUP_ALL pthread_cond_broadcast(&query_cond_wait)
+
+// TODO
+//extern volatile int aclk_connected;
+
+struct aclk_query_thread {
+ ND_THREAD *thread;
+ int idx;
+ mqtt_wss_client client;
+};
+
+struct aclk_query_threads {
+ struct aclk_query_thread *thread_list;
+ int count;
+};
+
+void aclk_query_threads_start(struct aclk_query_threads *query_threads, mqtt_wss_client client);
+void aclk_query_threads_cleanup(struct aclk_query_threads *query_threads);
+
+const char *aclk_query_get_name(aclk_query_type_t qt, int unknown_ok);
+
+int mark_pending_req_cancelled(const char *msg_id);
+
+#endif //NETDATA_AGENT_CLOUD_LINK_H
diff --git a/src/aclk/aclk_query_queue.c b/src/aclk/aclk_query_queue.c
new file mode 100644
index 000000000..3edadc002
--- /dev/null
+++ b/src/aclk/aclk_query_queue.c
@@ -0,0 +1,124 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_query_queue.h"
+#include "aclk_query.h"
+#include "aclk_stats.h"
+
+static netdata_mutex_t aclk_query_queue_mutex = NETDATA_MUTEX_INITIALIZER;
+#define ACLK_QUEUE_LOCK netdata_mutex_lock(&aclk_query_queue_mutex)
+#define ACLK_QUEUE_UNLOCK netdata_mutex_unlock(&aclk_query_queue_mutex)
+
+static struct aclk_query_queue {
+ aclk_query_t head;
+ int block_push;
+} aclk_query_queue = {
+ .head = NULL,
+ .block_push = 0
+};
+
+static inline int _aclk_queue_query(aclk_query_t query)
+{
+ now_monotonic_high_precision_timeval(&query->created_tv);
+ query->created = now_realtime_usec();
+
+ ACLK_QUEUE_LOCK;
+ if (aclk_query_queue.block_push) {
+ ACLK_QUEUE_UNLOCK;
+ if(service_running(SERVICE_ACLK | ABILITY_DATA_QUERIES))
+ netdata_log_error("Query Queue is blocked from accepting new requests. This is normally the case when ACLK prepares to shutdown.");
+ aclk_query_free(query);
+ return 1;
+ }
+ DOUBLE_LINKED_LIST_APPEND_ITEM_UNSAFE(aclk_query_queue.head, query, prev, next);
+ ACLK_QUEUE_UNLOCK;
+ return 0;
+
+}
+
+int aclk_queue_query(aclk_query_t query)
+{
+ int ret = _aclk_queue_query(query);
+ if (!ret) {
+ QUERY_THREAD_WAKEUP;
+ if (aclk_stats_enabled) {
+ ACLK_STATS_LOCK;
+ aclk_metrics_per_sample.queries_queued++;
+ ACLK_STATS_UNLOCK;
+ }
+ }
+ return ret;
+}
+
+aclk_query_t aclk_queue_pop(void)
+{
+ aclk_query_t ret;
+
+ ACLK_QUEUE_LOCK;
+ if (aclk_query_queue.block_push) {
+ ACLK_QUEUE_UNLOCK;
+ if(service_running(SERVICE_ACLK | ABILITY_DATA_QUERIES))
+ netdata_log_error("POP Query Queue is blocked from accepting new requests. This is normally the case when ACLK prepares to shutdown.");
+ return NULL;
+ }
+
+ ret = aclk_query_queue.head;
+ if (!ret) {
+ ACLK_QUEUE_UNLOCK;
+ return ret;
+ }
+
+ DOUBLE_LINKED_LIST_REMOVE_ITEM_UNSAFE(aclk_query_queue.head, ret, prev, next);
+ ACLK_QUEUE_UNLOCK;
+
+ ret->next = NULL;
+ return ret;
+}
+
+void aclk_queue_flush(void)
+{
+ aclk_query_t query = aclk_queue_pop();
+ while (query) {
+ aclk_query_free(query);
+ query = aclk_queue_pop();
+ }
+}
+
+aclk_query_t aclk_query_new(aclk_query_type_t type)
+{
+ aclk_query_t query = callocz(1, sizeof(struct aclk_query));
+ query->type = type;
+ return query;
+}
+
+void aclk_query_free(aclk_query_t query)
+{
+ switch (query->type) {
+ case HTTP_API_V2:
+ freez(query->data.http_api_v2.payload);
+ if (query->data.http_api_v2.query != query->dedup_id)
+ freez(query->data.http_api_v2.query);
+ break;
+
+ default:
+ break;
+ }
+
+ freez(query->dedup_id);
+ freez(query->callback_topic);
+ freez(query->msg_id);
+ freez(query);
+}
+
+void aclk_queue_lock(void)
+{
+ ACLK_QUEUE_LOCK;
+ aclk_query_queue.block_push = 1;
+ ACLK_QUEUE_UNLOCK;
+}
+
+void aclk_queue_unlock(void)
+{
+ ACLK_QUEUE_LOCK;
+ aclk_query_queue.block_push = 0;
+ ACLK_QUEUE_UNLOCK;
+}
diff --git a/src/aclk/aclk_query_queue.h b/src/aclk/aclk_query_queue.h
new file mode 100644
index 000000000..4a4a36a3f
--- /dev/null
+++ b/src/aclk/aclk_query_queue.h
@@ -0,0 +1,87 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef NETDATA_ACLK_QUERY_QUEUE_H
+#define NETDATA_ACLK_QUERY_QUEUE_H
+
+#include "libnetdata/libnetdata.h"
+#include "daemon/common.h"
+#include "schema-wrappers/schema_wrappers.h"
+
+#include "aclk_util.h"
+
+typedef enum {
+ UNKNOWN = 0,
+ HTTP_API_V2,
+ REGISTER_NODE,
+ NODE_STATE_UPDATE,
+ CHART_DIMS_UPDATE,
+ CHART_CONFIG_UPDATED,
+ CHART_RESET,
+ RETENTION_UPDATED,
+ UPDATE_NODE_INFO,
+ ALARM_PROVIDE_CHECKPOINT,
+ ALARM_PROVIDE_CFG,
+ ALARM_SNAPSHOT,
+ UPDATE_NODE_COLLECTORS,
+ PROTO_BIN_MESSAGE,
+ ACLK_QUERY_TYPE_COUNT // always keep this as last
+} aclk_query_type_t;
+
+struct aclk_query_http_api_v2 {
+ char *payload;
+ char *query;
+};
+
+struct aclk_bin_payload {
+ char *payload;
+ size_t size;
+ enum aclk_topics topic;
+ const char *msg_name;
+};
+
+typedef struct aclk_query *aclk_query_t;
+struct aclk_query {
+ aclk_query_type_t type;
+
+ // dedup_id is used to deduplicate queries in the list
+ // if type and dedup_id is the same message is deduplicated
+ // set dedup_id to NULL to never deduplicate the message
+ // set dedup_id to constant (e.g. empty string "") to make
+ // message of this type ever exist only once in the list
+ char *dedup_id;
+ char *callback_topic;
+ char *msg_id;
+
+ struct timeval created_tv;
+ usec_t created;
+ int timeout;
+ aclk_query_t prev, next;
+
+ // TODO maybe remove?
+ int version;
+ union {
+ struct aclk_query_http_api_v2 http_api_v2;
+ struct aclk_bin_payload bin_payload;
+ } data;
+};
+
+aclk_query_t aclk_query_new(aclk_query_type_t type);
+void aclk_query_free(aclk_query_t query);
+
+int aclk_queue_query(aclk_query_t query);
+aclk_query_t aclk_queue_pop(void);
+void aclk_queue_flush(void);
+
+void aclk_queue_lock(void);
+void aclk_queue_unlock(void);
+
+#define QUEUE_IF_PAYLOAD_PRESENT(query) do { \
+ if (likely(query->data.bin_payload.payload)) { \
+ aclk_queue_query(query); \
+ } else { \
+ nd_log(NDLS_DAEMON, NDLP_ERR, "Failed to generate payload"); \
+ aclk_query_free(query); \
+ } \
+} while(0)
+
+#endif /* NETDATA_ACLK_QUERY_QUEUE_H */
diff --git a/src/aclk/aclk_rrdhost_state.h b/src/aclk/aclk_rrdhost_state.h
new file mode 100644
index 000000000..5c8a2ddc9
--- /dev/null
+++ b/src/aclk/aclk_rrdhost_state.h
@@ -0,0 +1,11 @@
+#ifndef ACLK_RRDHOST_STATE_H
+#define ACLK_RRDHOST_STATE_H
+
+#include "libnetdata/libnetdata.h"
+
+typedef struct aclk_rrdhost_state {
+ char *claimed_id; // Claimed ID if host has one otherwise NULL
+ char *prev_claimed_id; // Claimed ID if changed (reclaimed) during runtime
+} aclk_rrdhost_state;
+
+#endif /* ACLK_RRDHOST_STATE_H */
diff --git a/src/aclk/aclk_rx_msgs.c b/src/aclk/aclk_rx_msgs.c
new file mode 100644
index 000000000..60e421928
--- /dev/null
+++ b/src/aclk/aclk_rx_msgs.c
@@ -0,0 +1,571 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_rx_msgs.h"
+
+#include "aclk_stats.h"
+#include "aclk_query_queue.h"
+#include "aclk.h"
+#include "aclk_capas.h"
+#include "aclk_query.h"
+
+#include "schema-wrappers/proto_2_json.h"
+
+#define ACLK_V2_PAYLOAD_SEPARATOR "\x0D\x0A\x0D\x0A"
+
+#define ACLK_V_COMPRESSION 2
+
+struct aclk_request {
+ char *type_id;
+ char *msg_id;
+ char *callback_topic;
+ char *payload;
+ int version;
+ int timeout;
+ int min_version;
+ int max_version;
+};
+
+static int cloud_to_agent_parse(JSON_ENTRY *e)
+{
+ struct aclk_request *data = e->callback_data;
+
+ switch (e->type) {
+ case JSON_OBJECT:
+ case JSON_ARRAY:
+ break;
+ case JSON_STRING:
+ if (!strcmp(e->name, "msg-id")) {
+ data->msg_id = strdupz(e->data.string);
+ break;
+ }
+ if (!strcmp(e->name, "type")) {
+ data->type_id = strdupz(e->data.string);
+ break;
+ }
+ if (!strcmp(e->name, "callback-topic")) {
+ data->callback_topic = strdupz(e->data.string);
+ break;
+ }
+ if (!strcmp(e->name, "payload")) {
+ if (likely(e->data.string)) {
+ size_t len = strlen(e->data.string);
+ data->payload = mallocz(len+1);
+ if (!url_decode_r(data->payload, e->data.string, len + 1))
+ strcpy(data->payload, e->data.string);
+ }
+ break;
+ }
+ break;
+ case JSON_NUMBER:
+ if (!strcmp(e->name, "version")) {
+ data->version = (int)e->data.number;
+ break;
+ }
+ if (!strcmp(e->name, "timeout")) {
+ data->timeout = (int)e->data.number;
+ break;
+ }
+ if (!strcmp(e->name, "min-version")) {
+ data->min_version = (int)e->data.number;
+ break;
+ }
+ if (!strcmp(e->name, "max-version")) {
+ data->max_version = (int)e->data.number;
+ break;
+ }
+
+ break;
+
+ case JSON_BOOLEAN:
+ break;
+
+ case JSON_NULL:
+ break;
+ }
+ return 0;
+}
+
+static inline int aclk_extract_v2_data(char *payload, char **data)
+{
+ char* ptr = strstr(payload, ACLK_V2_PAYLOAD_SEPARATOR);
+ if(!ptr)
+ return 1;
+ ptr += strlen(ACLK_V2_PAYLOAD_SEPARATOR);
+ *data = strdupz(ptr);
+ return 0;
+}
+
+static inline int aclk_v2_payload_get_query(const char *payload, char **query_url)
+{
+ const char *start, *end;
+
+ if(strncmp(payload, "GET /", 5) == 0 || strncmp(payload, "PUT /", 5) == 0)
+ start = payload + 4;
+ else if(strncmp(payload, "POST /", 6) == 0)
+ start = payload + 5;
+ else if(strncmp(payload, "DELETE /", 8) == 0)
+ start = payload + 7;
+ else {
+ errno = 0;
+ netdata_log_error("Only accepting requests that start with GET, POST, PUT, DELETE from CLOUD.");
+ return 1;
+ }
+
+ if(!(end = strstr(payload, HTTP_1_1 HTTP_ENDL))) {
+ errno = 0;
+ netdata_log_error("Doesn't look like HTTP GET request.");
+ return 1;
+ }
+
+ *query_url = mallocz((end - start) + 1);
+ strncpyz(*query_url, start, end - start);
+
+ return 0;
+}
+
+static int aclk_handle_cloud_http_request_v2(struct aclk_request *cloud_to_agent, char *raw_payload)
+{
+ aclk_query_t query;
+
+ errno = 0;
+ if (cloud_to_agent->version < ACLK_V_COMPRESSION) {
+ netdata_log_error(
+ "This handler cannot reply to request with version older than %d, received %d.",
+ ACLK_V_COMPRESSION,
+ cloud_to_agent->version);
+ return 1;
+ }
+
+ query = aclk_query_new(HTTP_API_V2);
+
+ if (unlikely(aclk_extract_v2_data(raw_payload, &query->data.http_api_v2.payload))) {
+ netdata_log_error("Error extracting payload expected after the JSON dictionary.");
+ goto error;
+ }
+
+ if (unlikely(aclk_v2_payload_get_query(query->data.http_api_v2.payload, &query->dedup_id))) {
+ netdata_log_error("Could not extract payload from query");
+ goto error;
+ }
+
+ if (unlikely(!cloud_to_agent->callback_topic)) {
+ netdata_log_error("Missing callback_topic");
+ goto error;
+ }
+
+ if (unlikely(!cloud_to_agent->msg_id)) {
+ netdata_log_error("Missing msg_id");
+ goto error;
+ }
+
+ // aclk_queue_query takes ownership of data pointer
+ query->callback_topic = cloud_to_agent->callback_topic;
+ query->timeout = cloud_to_agent->timeout;
+ // for clarity and code readability as when we process the request
+ // it would be strange to get URL from `dedup_id`
+ query->data.http_api_v2.query = query->dedup_id;
+ query->msg_id = cloud_to_agent->msg_id;
+ aclk_queue_query(query);
+ return 0;
+
+error:
+ aclk_query_free(query);
+ return 1;
+}
+
+int aclk_handle_cloud_cmd_message(char *payload)
+{
+ struct aclk_request cloud_to_agent;
+ memset(&cloud_to_agent, 0, sizeof(struct aclk_request));
+
+ if (unlikely(!payload)) {
+ error_report("ACLK incoming 'cmd' message is empty");
+ return 1;
+ }
+
+ netdata_log_debug(D_ACLK, "ACLK incoming 'cmd' message (%s)", payload);
+
+ int rc = json_parse(payload, &cloud_to_agent, cloud_to_agent_parse);
+
+ if (unlikely(rc != JSON_OK)) {
+ error_report("Malformed json request (%s)", payload);
+ goto err_cleanup;
+ }
+
+ if (!cloud_to_agent.type_id) {
+ error_report("Cloud message is missing compulsory key \"type\"");
+ goto err_cleanup;
+ }
+
+ // Originally we were expecting to have multiple types of 'cmd' message,
+ // but after the new protocol was designed we will ever only have 'http'
+ if (strcmp(cloud_to_agent.type_id, "http") != 0) {
+ error_report("Only 'http' cmd message is supported");
+ goto err_cleanup;
+ }
+
+ if (likely(!aclk_handle_cloud_http_request_v2(&cloud_to_agent, payload))) {
+ // aclk_handle_cloud_request takes ownership of the pointers
+ // (to avoid copying) in case of success
+ freez(cloud_to_agent.type_id);
+ return 0;
+ }
+
+err_cleanup:
+ if (cloud_to_agent.payload)
+ freez(cloud_to_agent.payload);
+ if (cloud_to_agent.type_id)
+ freez(cloud_to_agent.type_id);
+ if (cloud_to_agent.msg_id)
+ freez(cloud_to_agent.msg_id);
+ if (cloud_to_agent.callback_topic)
+ freez(cloud_to_agent.callback_topic);
+
+ return 1;
+}
+
+typedef uint32_t simple_hash_t;
+typedef int(*rx_msg_handler)(const char *msg, size_t msg_len);
+
+int handle_old_proto_cmd(const char *msg, size_t msg_len)
+{
+ // msg is binary payload in all other cases
+ // however in this message from old legacy cloud
+ // we have to convert it to C string
+ char *str = mallocz(msg_len+1);
+ memcpy(str, msg, msg_len);
+ str[msg_len] = 0;
+ if (aclk_handle_cloud_cmd_message(str)) {
+ freez(str);
+ return 1;
+ }
+ freez(str);
+ return 0;
+}
+
+int create_node_instance_result(const char *msg, size_t msg_len)
+{
+ node_instance_creation_result_t res = parse_create_node_instance_result(msg, msg_len);
+ if (!res.machine_guid || !res.node_id) {
+ error_report("Error parsing CreateNodeInstanceResult");
+ freez(res.machine_guid);
+ freez(res.node_id);
+ return 1;
+ }
+
+ netdata_log_debug(D_ACLK, "CreateNodeInstanceResult: guid:%s nodeid:%s", res.machine_guid, res.node_id);
+
+ nd_uuid_t host_id, node_id;
+ if (uuid_parse(res.machine_guid, host_id)) {
+ netdata_log_error("Error parsing machine_guid provided by CreateNodeInstanceResult");
+ freez(res.machine_guid);
+ freez(res.node_id);
+ return 1;
+ }
+ if (uuid_parse(res.node_id, node_id)) {
+ netdata_log_error("Error parsing node_id provided by CreateNodeInstanceResult");
+ freez(res.machine_guid);
+ freez(res.node_id);
+ return 1;
+ }
+ update_node_id(&host_id, &node_id);
+
+ aclk_query_t query = aclk_query_new(NODE_STATE_UPDATE);
+ node_instance_connection_t node_state_update = {
+ .hops = 1,
+ .live = 0,
+ .queryable = 1,
+ .session_id = aclk_session_newarch,
+ .node_id = res.node_id,
+ .capabilities = NULL
+ };
+
+ RRDHOST *host = rrdhost_find_by_guid(res.machine_guid);
+ if (likely(host)) {
+ if (host == localhost) {
+ node_state_update.live = 1;
+ node_state_update.hops = 0;
+ } else {
+ node_state_update.live = (!rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN));
+ node_state_update.hops = host->system_info->hops;
+ }
+ node_state_update.capabilities = aclk_get_node_instance_capas(host);
+ }
+
+ rrdhost_aclk_state_lock(localhost);
+ node_state_update.claim_id = localhost->aclk_state.claimed_id;
+ query->data.bin_payload.payload = generate_node_instance_connection(&query->data.bin_payload.size, &node_state_update);
+ rrdhost_aclk_state_unlock(localhost);
+
+ freez((void *)node_state_update.capabilities);
+
+ query->data.bin_payload.msg_name = "UpdateNodeInstanceConnection";
+ query->data.bin_payload.topic = ACLK_TOPICID_NODE_CONN;
+
+ aclk_queue_query(query);
+ freez(res.node_id);
+ freez(res.machine_guid);
+ return 0;
+}
+
+int send_node_instances(const char *msg, size_t msg_len)
+{
+ UNUSED(msg);
+ UNUSED(msg_len);
+ aclk_send_node_instances();
+ return 0;
+}
+
+int stream_charts_and_dimensions(const char *msg, size_t msg_len)
+{
+ UNUSED(msg);
+ UNUSED(msg_len);
+ error_report("Received obsolete StreamChartsAndDimensions msg");
+ return 0;
+}
+
+int charts_and_dimensions_ack(const char *msg, size_t msg_len)
+{
+ UNUSED(msg);
+ UNUSED(msg_len);
+ error_report("Received obsolete StreamChartsAndDimensionsAck msg");
+ return 0;
+}
+
+int update_chart_configs(const char *msg, size_t msg_len)
+{
+ UNUSED(msg);
+ UNUSED(msg_len);
+ error_report("Received obsolete UpdateChartConfigs msg");
+ return 0;
+}
+
+int start_alarm_streaming(const char *msg, size_t msg_len)
+{
+ struct start_alarm_streaming res = parse_start_alarm_streaming(msg, msg_len);
+ if (!res.node_id) {
+ netdata_log_error("Error parsing StartAlarmStreaming");
+ return 1;
+ }
+ aclk_start_alert_streaming(res.node_id, res.resets);
+ freez(res.node_id);
+ return 0;
+}
+
+int send_alarm_checkpoint(const char *msg, size_t msg_len)
+{
+ struct send_alarm_checkpoint sac = parse_send_alarm_checkpoint(msg, msg_len);
+ if (!sac.node_id || !sac.claim_id) {
+ netdata_log_error("Error parsing SendAlarmCheckpoint");
+ freez(sac.node_id);
+ freez(sac.claim_id);
+ return 1;
+ }
+ aclk_send_alarm_checkpoint(sac.node_id, sac.claim_id);
+ freez(sac.node_id);
+ freez(sac.claim_id);
+ return 0;
+}
+
+int send_alarm_configuration(const char *msg, size_t msg_len)
+{
+ char *config_hash = parse_send_alarm_configuration(msg, msg_len);
+ if (!config_hash || !*config_hash) {
+ netdata_log_error("Error parsing SendAlarmConfiguration");
+ freez(config_hash);
+ return 1;
+ }
+ aclk_send_alarm_configuration(config_hash);
+ freez(config_hash);
+ return 0;
+}
+
+int send_alarm_snapshot(const char *msg, size_t msg_len)
+{
+ struct send_alarm_snapshot *sas = parse_send_alarm_snapshot(msg, msg_len);
+ if (!sas->node_id || !sas->claim_id || !sas->snapshot_uuid) {
+ netdata_log_error("Error parsing SendAlarmSnapshot");
+ destroy_send_alarm_snapshot(sas);
+ return 1;
+ }
+ aclk_process_send_alarm_snapshot(sas->node_id, sas->claim_id, sas->snapshot_uuid);
+ destroy_send_alarm_snapshot(sas);
+ return 0;
+}
+
+int handle_disconnect_req(const char *msg, size_t msg_len)
+{
+ struct disconnect_cmd *cmd = parse_disconnect_cmd(msg, msg_len);
+ if (!cmd)
+ return 1;
+ if (cmd->permaban) {
+ netdata_log_error("Cloud Banned This Agent!");
+ aclk_disable_runtime = 1;
+ }
+ netdata_log_info("Cloud requested disconnect (EC=%u, \"%s\")", (unsigned int)cmd->error_code, cmd->error_description);
+ if (cmd->reconnect_after_s > 0) {
+ aclk_block_until = now_monotonic_sec() + cmd->reconnect_after_s;
+ netdata_log_info(
+ "Cloud asks not to reconnect for %u seconds. We shall honor that request",
+ (unsigned int)cmd->reconnect_after_s);
+ }
+ disconnect_req = 1;
+ freez(cmd->error_description);
+ freez(cmd);
+ return 0;
+}
+
+int contexts_checkpoint(const char *msg, size_t msg_len)
+{
+ aclk_ctx_based = 1;
+
+ struct ctxs_checkpoint *cmd = parse_ctxs_checkpoint(msg, msg_len);
+ if (!cmd)
+ return 1;
+
+ rrdcontext_hub_checkpoint_command(cmd);
+
+ freez(cmd->claim_id);
+ freez(cmd->node_id);
+ freez(cmd);
+ return 0;
+}
+
+int stop_streaming_contexts(const char *msg, size_t msg_len)
+{
+ if (!aclk_ctx_based) {
+ error_report("Received StopStreamingContexts message but context based communication was not enabled (Cloud violated the protocol). Ignoring message");
+ return 1;
+ }
+
+ struct stop_streaming_ctxs *cmd = parse_stop_streaming_ctxs(msg, msg_len);
+ if (!cmd)
+ return 1;
+
+ rrdcontext_hub_stop_streaming_command(cmd);
+
+ freez(cmd->claim_id);
+ freez(cmd->node_id);
+ freez(cmd);
+ return 0;
+}
+
+int cancel_pending_req(const char *msg, size_t msg_len)
+{
+ struct aclk_cancel_pending_req cmd = {.request_id = NULL, .trace_id = NULL};
+ if(parse_cancel_pending_req(msg, msg_len, &cmd)) {
+ error_report("Error parsing CancelPendingReq");
+ return 1;
+ }
+
+ nd_log(NDLS_ACCESS, NDLP_NOTICE, "ACLK CancelPendingRequest REQ: %s, cloud trace-id: %s", cmd.request_id, cmd.trace_id);
+
+ if (mark_pending_req_cancelled(cmd.request_id))
+ error_report("CancelPending Request for %s failed. No such pending request.", cmd.request_id);
+
+ free_cancel_pending_req(&cmd);
+ return 0;
+}
+
+typedef struct {
+ const char *name;
+ simple_hash_t name_hash;
+ rx_msg_handler fnc;
+} new_cloud_rx_msg_t;
+
+new_cloud_rx_msg_t rx_msgs[] = {
+ { .name = "cmd", .name_hash = 0, .fnc = handle_old_proto_cmd },
+ { .name = "CreateNodeInstanceResult", .name_hash = 0, .fnc = create_node_instance_result },
+ { .name = "SendNodeInstances", .name_hash = 0, .fnc = send_node_instances },
+ { .name = "StreamChartsAndDimensions", .name_hash = 0, .fnc = stream_charts_and_dimensions },
+ { .name = "ChartsAndDimensionsAck", .name_hash = 0, .fnc = charts_and_dimensions_ack },
+ { .name = "UpdateChartConfigs", .name_hash = 0, .fnc = update_chart_configs },
+ { .name = "StartAlarmStreaming", .name_hash = 0, .fnc = start_alarm_streaming },
+ { .name = "SendAlarmCheckpoint", .name_hash = 0, .fnc = send_alarm_checkpoint },
+ { .name = "SendAlarmConfiguration", .name_hash = 0, .fnc = send_alarm_configuration },
+ { .name = "SendAlarmSnapshot", .name_hash = 0, .fnc = send_alarm_snapshot },
+ { .name = "DisconnectReq", .name_hash = 0, .fnc = handle_disconnect_req },
+ { .name = "ContextsCheckpoint", .name_hash = 0, .fnc = contexts_checkpoint },
+ { .name = "StopStreamingContexts", .name_hash = 0, .fnc = stop_streaming_contexts },
+ { .name = "CancelPendingRequest", .name_hash = 0, .fnc = cancel_pending_req },
+ { .name = NULL, .name_hash = 0, .fnc = NULL },
+};
+
+new_cloud_rx_msg_t *find_rx_handler_by_hash(simple_hash_t hash)
+{
+ // we can afford to not compare strings after hash match
+ // because we check for collisions at initialization in
+ // aclk_init_rx_msg_handlers()
+ for (int i = 0; rx_msgs[i].fnc; i++) {
+ if (rx_msgs[i].name_hash == hash)
+ return &rx_msgs[i];
+ }
+ return NULL;
+}
+
+const char *rx_handler_get_name(size_t i)
+{
+ return rx_msgs[i].name;
+}
+
+unsigned int aclk_init_rx_msg_handlers(void)
+{
+ int i;
+ for (i = 0; rx_msgs[i].fnc; i++) {
+ simple_hash_t hash = simple_hash(rx_msgs[i].name);
+ new_cloud_rx_msg_t *hdl = find_rx_handler_by_hash(hash);
+ if (unlikely(hdl)) {
+ // the list of message names changes only by changing
+ // the source code, therefore fatal is appropriate
+ fatal("Hash collision. Choose better hash. Added '%s' clashes with existing '%s'", rx_msgs[i].name, hdl->name);
+ }
+ rx_msgs[i].name_hash = hash;
+ }
+ return i;
+}
+
+void aclk_handle_new_cloud_msg(const char *message_type, const char *msg, size_t msg_len, const char *topic __maybe_unused)
+{
+ if (aclk_stats_enabled) {
+ ACLK_STATS_LOCK;
+ aclk_metrics_per_sample.cloud_req_recvd++;
+ ACLK_STATS_UNLOCK;
+ }
+ new_cloud_rx_msg_t *msg_descriptor = find_rx_handler_by_hash(simple_hash(message_type));
+ netdata_log_debug(D_ACLK, "Got message named '%s' from cloud", message_type);
+ if (unlikely(!msg_descriptor)) {
+ netdata_log_error("Do not know how to handle message of type '%s'. Ignoring", message_type);
+ if (aclk_stats_enabled) {
+ ACLK_STATS_LOCK;
+ aclk_metrics_per_sample.cloud_req_err++;
+ ACLK_STATS_UNLOCK;
+ }
+ return;
+ }
+
+
+ if (aclklog_enabled) {
+ if (!strncmp(message_type, "cmd", strlen("cmd"))) {
+ log_aclk_message_bin(msg, msg_len, 0, topic, msg_descriptor->name);
+ } else {
+ char *json = protomsg_to_json(msg, msg_len, msg_descriptor->name);
+ log_aclk_message_bin(json, strlen(json), 0, topic, msg_descriptor->name);
+ freez(json);
+ }
+ }
+
+ if (aclk_stats_enabled) {
+ ACLK_STATS_LOCK;
+ aclk_proto_rx_msgs_sample[msg_descriptor-rx_msgs]++;
+ ACLK_STATS_UNLOCK;
+ }
+ if (msg_descriptor->fnc(msg, msg_len)) {
+ netdata_log_error("Error processing message of type '%s'", message_type);
+ if (aclk_stats_enabled) {
+ ACLK_STATS_LOCK;
+ aclk_metrics_per_sample.cloud_req_err++;
+ ACLK_STATS_UNLOCK;
+ }
+ return;
+ }
+}
diff --git a/src/aclk/aclk_rx_msgs.h b/src/aclk/aclk_rx_msgs.h
new file mode 100644
index 000000000..61921faec
--- /dev/null
+++ b/src/aclk/aclk_rx_msgs.h
@@ -0,0 +1,17 @@
+
+
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_RX_MSGS_H
+#define ACLK_RX_MSGS_H
+
+#include "daemon/common.h"
+#include "libnetdata/libnetdata.h"
+
+int aclk_handle_cloud_cmd_message(char *payload);
+
+const char *rx_handler_get_name(size_t i);
+unsigned int aclk_init_rx_msg_handlers(void);
+void aclk_handle_new_cloud_msg(const char *message_type, const char *msg, size_t msg_len, const char *topic);
+
+#endif /* ACLK_RX_MSGS_H */
diff --git a/src/aclk/aclk_stats.c b/src/aclk/aclk_stats.c
new file mode 100644
index 000000000..47a48c366
--- /dev/null
+++ b/src/aclk/aclk_stats.c
@@ -0,0 +1,483 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef MQTT_WSS_CPUSTATS
+#define MQTT_WSS_CPUSTATS
+#endif
+
+#include "aclk_stats.h"
+
+#include "aclk_query.h"
+
+netdata_mutex_t aclk_stats_mutex = NETDATA_MUTEX_INITIALIZER;
+
+struct {
+ int query_thread_count;
+ unsigned int proto_hdl_cnt;
+ uint32_t *aclk_proto_rx_msgs_sample;
+ RRDDIM **rx_msg_dims;
+} aclk_stats_cfg; // there is only 1 stats thread at a time
+
+// data ACLK stats need per query thread
+struct aclk_qt_data {
+ RRDDIM *dim;
+} *aclk_qt_data = NULL;
+
+uint32_t *aclk_queries_per_thread = NULL;
+uint32_t *aclk_queries_per_thread_sample = NULL;
+uint32_t *aclk_proto_rx_msgs_sample = NULL;
+
+struct aclk_metrics aclk_metrics = {
+ .online = 0,
+};
+
+struct aclk_metrics_per_sample aclk_metrics_per_sample;
+
+static void aclk_stats_collect(struct aclk_metrics_per_sample *per_sample, struct aclk_metrics *permanent)
+{
+ static RRDSET *st_aclkstats = NULL;
+ static RRDDIM *rd_online_status = NULL;
+
+ if (unlikely(!st_aclkstats)) {
+ st_aclkstats = rrdset_create_localhost(
+ "netdata", "aclk_status", NULL, "aclk", NULL, "ACLK/Cloud connection status",
+ "connected", "netdata", "stats", 200000, localhost->rrd_update_every, RRDSET_TYPE_LINE);
+
+ rd_online_status = rrddim_add(st_aclkstats, "online", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ rrddim_set_by_pointer(st_aclkstats, rd_online_status, per_sample->offline_during_sample ? 0 : permanent->online);
+
+ rrdset_done(st_aclkstats);
+}
+
+static void aclk_stats_query_queue(struct aclk_metrics_per_sample *per_sample)
+{
+ static RRDSET *st_query_thread = NULL;
+ static RRDDIM *rd_queued = NULL;
+ static RRDDIM *rd_dispatched = NULL;
+
+ if (unlikely(!st_query_thread)) {
+ st_query_thread = rrdset_create_localhost(
+ "netdata", "aclk_query_per_second", NULL, "aclk", NULL, "ACLK Queries per second", "queries/s",
+ "netdata", "stats", 200001, localhost->rrd_update_every, RRDSET_TYPE_AREA);
+
+ rd_queued = rrddim_add(st_query_thread, "added", NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ rd_dispatched = rrddim_add(st_query_thread, "dispatched", NULL, -1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ rrddim_set_by_pointer(st_query_thread, rd_queued, per_sample->queries_queued);
+ rrddim_set_by_pointer(st_query_thread, rd_dispatched, per_sample->queries_dispatched);
+
+ rrdset_done(st_query_thread);
+}
+
+#ifdef NETDATA_INTERNAL_CHECKS
+static void aclk_stats_latency(struct aclk_metrics_per_sample *per_sample)
+{
+ static RRDSET *st = NULL;
+ static RRDDIM *rd_avg = NULL;
+ static RRDDIM *rd_max = NULL;
+
+ if (unlikely(!st)) {
+ st = rrdset_create_localhost(
+ "netdata", "aclk_latency_mqtt", NULL, "aclk", NULL, "ACLK Message Publish Latency", "ms",
+ "netdata", "stats", 200002, localhost->rrd_update_every, RRDSET_TYPE_LINE);
+
+ rd_avg = rrddim_add(st, "avg", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_max = rrddim_add(st, "max", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ if(per_sample->latency_count)
+ rrddim_set_by_pointer(st, rd_avg, roundf((float)per_sample->latency_total / per_sample->latency_count));
+ else
+ rrddim_set_by_pointer(st, rd_avg, 0);
+
+ rrddim_set_by_pointer(st, rd_max, per_sample->latency_max);
+
+ rrdset_done(st);
+}
+#endif
+
+static void aclk_stats_cloud_req(struct aclk_metrics_per_sample *per_sample)
+{
+ static RRDSET *st = NULL;
+ static RRDDIM *rd_rq_rcvd = NULL;
+ static RRDDIM *rd_rq_err = NULL;
+
+ if (unlikely(!st)) {
+ st = rrdset_create_localhost(
+ "netdata", "aclk_cloud_req", NULL, "aclk", NULL, "Requests received from cloud", "req/s",
+ "netdata", "stats", 200005, localhost->rrd_update_every, RRDSET_TYPE_STACKED);
+
+ rd_rq_rcvd = rrddim_add(st, "received", NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ rd_rq_err = rrddim_add(st, "malformed", NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ rrddim_set_by_pointer(st, rd_rq_rcvd, per_sample->cloud_req_recvd - per_sample->cloud_req_err);
+ rrddim_set_by_pointer(st, rd_rq_err, per_sample->cloud_req_err);
+
+ rrdset_done(st);
+}
+
+static void aclk_stats_cloud_req_type(struct aclk_metrics_per_sample *per_sample)
+{
+ static RRDSET *st = NULL;
+ static RRDDIM *dims[ACLK_QUERY_TYPE_COUNT];
+
+ if (unlikely(!st)) {
+ st = rrdset_create_localhost(
+ "netdata", "aclk_processed_query_type", NULL, "aclk", NULL, "Query thread commands processed by their type", "cmd/s",
+ "netdata", "stats", 200006, localhost->rrd_update_every, RRDSET_TYPE_STACKED);
+
+ for (int i = 0; i < ACLK_QUERY_TYPE_COUNT; i++)
+ dims[i] = rrddim_add(st, aclk_query_get_name(i, 1), NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+
+ }
+
+ for (int i = 0; i < ACLK_QUERY_TYPE_COUNT; i++)
+ rrddim_set_by_pointer(st, dims[i], per_sample->queries_per_type[i]);
+
+ rrdset_done(st);
+}
+
+static char *cloud_req_http_type_names[ACLK_STATS_CLOUD_HTTP_REQ_TYPE_CNT] = {
+ "other",
+ "info",
+ "data",
+ "alarms",
+ "alarm_log",
+ "chart",
+ "charts",
+ "function",
+ "functions"
+ // if you change then update `ACLK_STATS_CLOUD_HTTP_REQ_TYPE_CNT`.
+};
+
+int aclk_cloud_req_http_type_to_idx(const char *name)
+{
+ for (int i = 1; i < ACLK_STATS_CLOUD_HTTP_REQ_TYPE_CNT; i++)
+ if (!strcmp(cloud_req_http_type_names[i], name))
+ return i;
+ return 0;
+}
+
+static void aclk_stats_cloud_req_http_type(struct aclk_metrics_per_sample *per_sample)
+{
+ static RRDSET *st = NULL;
+ static RRDDIM *rd_rq_types[ACLK_STATS_CLOUD_HTTP_REQ_TYPE_CNT];
+
+ if (unlikely(!st)) {
+ st = rrdset_create_localhost(
+ "netdata", "aclk_cloud_req_http_type", NULL, "aclk", NULL, "Requests received from cloud via HTTP by their type", "req/s",
+ "netdata", "stats", 200007, localhost->rrd_update_every, RRDSET_TYPE_STACKED);
+
+ for (int i = 0; i < ACLK_STATS_CLOUD_HTTP_REQ_TYPE_CNT; i++)
+ rd_rq_types[i] = rrddim_add(st, cloud_req_http_type_names[i], NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ for (int i = 0; i < ACLK_STATS_CLOUD_HTTP_REQ_TYPE_CNT; i++)
+ rrddim_set_by_pointer(st, rd_rq_types[i], per_sample->cloud_req_http_by_type[i]);
+
+ rrdset_done(st);
+}
+
+#define MAX_DIM_NAME 22
+static void aclk_stats_query_threads(uint32_t *queries_per_thread)
+{
+ static RRDSET *st = NULL;
+
+ char dim_name[MAX_DIM_NAME];
+
+ if (unlikely(!st)) {
+ st = rrdset_create_localhost(
+ "netdata", "aclk_query_threads", NULL, "aclk", NULL, "Queries Processed Per Thread", "req/s",
+ "netdata", "stats", 200009, localhost->rrd_update_every, RRDSET_TYPE_STACKED);
+
+ for (int i = 0; i < aclk_stats_cfg.query_thread_count; i++) {
+ if (snprintfz(dim_name, MAX_DIM_NAME, "Query %d", i) < 0)
+ netdata_log_error("snprintf encoding error");
+ aclk_qt_data[i].dim = rrddim_add(st, dim_name, NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ }
+ }
+
+ for (int i = 0; i < aclk_stats_cfg.query_thread_count; i++) {
+ rrddim_set_by_pointer(st, aclk_qt_data[i].dim, queries_per_thread[i]);
+ }
+
+ rrdset_done(st);
+}
+
+static void aclk_stats_query_time(struct aclk_metrics_per_sample *per_sample)
+{
+ static RRDSET *st = NULL;
+ static RRDDIM *rd_rq_avg = NULL;
+ static RRDDIM *rd_rq_max = NULL;
+ static RRDDIM *rd_rq_total = NULL;
+
+ if (unlikely(!st)) {
+ st = rrdset_create_localhost(
+ "netdata", "aclk_query_time", NULL, "aclk", NULL, "Time it took to process cloud requested DB queries", "us",
+ "netdata", "stats", 200008, localhost->rrd_update_every, RRDSET_TYPE_LINE);
+
+ rd_rq_avg = rrddim_add(st, "avg", NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ rd_rq_max = rrddim_add(st, "max", NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ rd_rq_total = rrddim_add(st, "total", NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ if(per_sample->cloud_q_process_count)
+ rrddim_set_by_pointer(st, rd_rq_avg, roundf((float)per_sample->cloud_q_process_total / per_sample->cloud_q_process_count));
+ else
+ rrddim_set_by_pointer(st, rd_rq_avg, 0);
+ rrddim_set_by_pointer(st, rd_rq_max, per_sample->cloud_q_process_max);
+ rrddim_set_by_pointer(st, rd_rq_total, per_sample->cloud_q_process_total);
+
+ rrdset_done(st);
+}
+
+const char *rx_handler_get_name(size_t i);
+static void aclk_stats_newproto_rx(uint32_t *rx_msgs_sample)
+{
+ static RRDSET *st = NULL;
+
+ if (unlikely(!st)) {
+ st = rrdset_create_localhost(
+ "netdata", "aclk_protobuf_rx_types", NULL, "aclk", NULL, "Received new cloud architecture messages by their type.", "msg/s",
+ "netdata", "stats", 200010, localhost->rrd_update_every, RRDSET_TYPE_STACKED);
+
+ for (unsigned int i = 0; i < aclk_stats_cfg.proto_hdl_cnt; i++) {
+ aclk_stats_cfg.rx_msg_dims[i] = rrddim_add(st, rx_handler_get_name(i), NULL, 1, localhost->rrd_update_every, RRD_ALGORITHM_ABSOLUTE);
+ }
+ }
+
+ for (unsigned int i = 0; i < aclk_stats_cfg.proto_hdl_cnt; i++)
+ rrddim_set_by_pointer(st, aclk_stats_cfg.rx_msg_dims[i], rx_msgs_sample[i]);
+
+ rrdset_done(st);
+}
+
+static void aclk_stats_mqtt_wss(struct mqtt_wss_stats *stats)
+{
+ static RRDSET *st = NULL;
+ static RRDDIM *rd_sent = NULL;
+ static RRDDIM *rd_recvd = NULL;
+ static uint64_t sent = 0;
+ static uint64_t recvd = 0;
+
+ static RRDSET *st_txbuf_perc = NULL;
+ static RRDDIM *rd_txbuf_perc = NULL;
+
+ static RRDSET *st_txbuf = NULL;
+ static RRDDIM *rd_tx_buffer_usable = NULL;
+ static RRDDIM *rd_tx_buffer_reclaimable = NULL;
+ static RRDDIM *rd_tx_buffer_used = NULL;
+ static RRDDIM *rd_tx_buffer_free = NULL;
+ static RRDDIM *rd_tx_buffer_size = NULL;
+
+ static RRDSET *st_timing = NULL;
+ static RRDDIM *rd_keepalive = NULL;
+ static RRDDIM *rd_read_socket = NULL;
+ static RRDDIM *rd_write_socket = NULL;
+ static RRDDIM *rd_process_websocket = NULL;
+ static RRDDIM *rd_process_mqtt = NULL;
+
+ sent += stats->bytes_tx;
+ recvd += stats->bytes_rx;
+
+ if (unlikely(!st)) {
+ st = rrdset_create_localhost(
+ "netdata", "aclk_openssl_bytes", NULL, "aclk", NULL, "Received and Sent bytes.", "B/s",
+ "netdata", "stats", 200011, localhost->rrd_update_every, RRDSET_TYPE_STACKED);
+
+ rd_sent = rrddim_add(st, "sent", NULL, -1, 1, RRD_ALGORITHM_INCREMENTAL);
+ rd_recvd = rrddim_add(st, "received", NULL, 1, 1, RRD_ALGORITHM_INCREMENTAL);
+ }
+
+ if (unlikely(!st_txbuf_perc)) {
+ st_txbuf_perc = rrdset_create_localhost(
+ "netdata", "aclk_mqtt_tx_perc", NULL, "aclk", NULL, "Actively used percentage of MQTT Tx Buffer,", "%",
+ "netdata", "stats", 200012, localhost->rrd_update_every, RRDSET_TYPE_LINE);
+
+ rd_txbuf_perc = rrddim_add(st_txbuf_perc, "used", NULL, 1, 100, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ if (unlikely(!st_txbuf)) {
+ st_txbuf = rrdset_create_localhost(
+ "netdata", "aclk_mqtt_tx_queue", NULL, "aclk", NULL, "State of transmit MQTT queue.", "B",
+ "netdata", "stats", 200013, localhost->rrd_update_every, RRDSET_TYPE_LINE);
+
+ rd_tx_buffer_usable = rrddim_add(st_txbuf, "usable", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_tx_buffer_reclaimable = rrddim_add(st_txbuf, "reclaimable", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_tx_buffer_used = rrddim_add(st_txbuf, "used", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_tx_buffer_free = rrddim_add(st_txbuf, "free", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_tx_buffer_size = rrddim_add(st_txbuf, "size", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ if (unlikely(!st_timing)) {
+ st_timing = rrdset_create_localhost(
+ "netdata", "aclk_mqtt_wss_time", NULL, "aclk", NULL, "Time spent handling MQTT, WSS, SSL and network communication.", "us",
+ "netdata", "stats", 200014, localhost->rrd_update_every, RRDSET_TYPE_STACKED);
+
+ rd_keepalive = rrddim_add(st_timing, "keep-alive", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_read_socket = rrddim_add(st_timing, "socket_read_ssl", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_write_socket = rrddim_add(st_timing, "socket_write_ssl", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_process_websocket = rrddim_add(st_timing, "process_websocket", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ rd_process_mqtt = rrddim_add(st_timing, "process_mqtt", NULL, 1, 1, RRD_ALGORITHM_ABSOLUTE);
+ }
+
+ rrddim_set_by_pointer(st, rd_sent, sent);
+ rrddim_set_by_pointer(st, rd_recvd, recvd);
+
+ float usage = ((float)stats->mqtt.tx_buffer_free + stats->mqtt.tx_buffer_reclaimable) / stats->mqtt.tx_buffer_size;
+ usage = (1 - usage) * 10000;
+ rrddim_set_by_pointer(st_txbuf_perc, rd_txbuf_perc, usage);
+
+ rrddim_set_by_pointer(st_txbuf, rd_tx_buffer_usable, stats->mqtt.tx_buffer_reclaimable + stats->mqtt.tx_buffer_free);
+ rrddim_set_by_pointer(st_txbuf, rd_tx_buffer_reclaimable, stats->mqtt.tx_buffer_reclaimable);
+ rrddim_set_by_pointer(st_txbuf, rd_tx_buffer_used, stats->mqtt.tx_buffer_used);
+ rrddim_set_by_pointer(st_txbuf, rd_tx_buffer_free, stats->mqtt.tx_buffer_free);
+ rrddim_set_by_pointer(st_txbuf, rd_tx_buffer_size, stats->mqtt.tx_buffer_size);
+
+ rrddim_set_by_pointer(st_timing, rd_keepalive, stats->time_keepalive);
+ rrddim_set_by_pointer(st_timing, rd_read_socket, stats->time_read_socket);
+ rrddim_set_by_pointer(st_timing, rd_write_socket, stats->time_write_socket);
+ rrddim_set_by_pointer(st_timing, rd_process_websocket, stats->time_process_websocket);
+ rrddim_set_by_pointer(st_timing, rd_process_mqtt, stats->time_process_mqtt);
+
+ rrdset_done(st);
+ rrdset_done(st_txbuf_perc);
+ rrdset_done(st_txbuf);
+ rrdset_done(st_timing);
+}
+
+void aclk_stats_thread_prepare(int query_thread_count, unsigned int proto_hdl_cnt)
+{
+ aclk_qt_data = callocz(query_thread_count, sizeof(struct aclk_qt_data));
+ aclk_queries_per_thread = callocz(query_thread_count, sizeof(uint32_t));
+ aclk_queries_per_thread_sample = callocz(query_thread_count, sizeof(uint32_t));
+
+ memset(&aclk_metrics_per_sample, 0, sizeof(struct aclk_metrics_per_sample));
+
+ aclk_stats_cfg.proto_hdl_cnt = proto_hdl_cnt;
+ aclk_stats_cfg.aclk_proto_rx_msgs_sample = callocz(proto_hdl_cnt, sizeof(*aclk_proto_rx_msgs_sample));
+ aclk_proto_rx_msgs_sample = callocz(proto_hdl_cnt, sizeof(*aclk_proto_rx_msgs_sample));
+ aclk_stats_cfg.rx_msg_dims = callocz(proto_hdl_cnt, sizeof(RRDDIM*));
+}
+
+void aclk_stats_thread_cleanup()
+{
+ freez(aclk_stats_cfg.rx_msg_dims);
+ freez(aclk_proto_rx_msgs_sample);
+ freez(aclk_stats_cfg.aclk_proto_rx_msgs_sample);
+ freez(aclk_qt_data);
+ freez(aclk_queries_per_thread);
+ freez(aclk_queries_per_thread_sample);
+}
+
+void *aclk_stats_main_thread(void *ptr)
+{
+ struct aclk_stats_thread *args = ptr;
+
+ aclk_stats_cfg.query_thread_count = args->query_thread_count;
+
+ heartbeat_t hb;
+ heartbeat_init(&hb);
+ usec_t step_ut = localhost->rrd_update_every * USEC_PER_SEC;
+
+ struct aclk_metrics_per_sample per_sample;
+ struct aclk_metrics permanent;
+
+ while (service_running(SERVICE_ACLK | SERVICE_COLLECTORS)) {
+
+ // ------------------------------------------------------------------------
+ // Wait for the next iteration point.
+
+ heartbeat_next(&hb, step_ut);
+
+ if (!service_running(SERVICE_ACLK | SERVICE_COLLECTORS)) break;
+
+ ACLK_STATS_LOCK;
+ // to not hold lock longer than necessary, especially not to hold it
+ // during database rrd* operations
+ memcpy(&per_sample, &aclk_metrics_per_sample, sizeof(struct aclk_metrics_per_sample));
+
+ memcpy(aclk_stats_cfg.aclk_proto_rx_msgs_sample, aclk_proto_rx_msgs_sample, sizeof(*aclk_proto_rx_msgs_sample) * aclk_stats_cfg.proto_hdl_cnt);
+ memset(aclk_proto_rx_msgs_sample, 0, sizeof(*aclk_proto_rx_msgs_sample) * aclk_stats_cfg.proto_hdl_cnt);
+
+ memcpy(&permanent, &aclk_metrics, sizeof(struct aclk_metrics));
+ memset(&aclk_metrics_per_sample, 0, sizeof(struct aclk_metrics_per_sample));
+
+ memcpy(aclk_queries_per_thread_sample, aclk_queries_per_thread, sizeof(uint32_t) * aclk_stats_cfg.query_thread_count);
+ memset(aclk_queries_per_thread, 0, sizeof(uint32_t) * aclk_stats_cfg.query_thread_count);
+ ACLK_STATS_UNLOCK;
+
+ aclk_stats_collect(&per_sample, &permanent);
+ aclk_stats_query_queue(&per_sample);
+#ifdef NETDATA_INTERNAL_CHECKS
+ aclk_stats_latency(&per_sample);
+#endif
+
+ aclk_stats_cloud_req(&per_sample);
+ aclk_stats_cloud_req_type(&per_sample);
+ aclk_stats_cloud_req_http_type(&per_sample);
+
+ aclk_stats_query_threads(aclk_queries_per_thread_sample);
+
+ aclk_stats_query_time(&per_sample);
+
+ struct mqtt_wss_stats mqtt_wss_stats = mqtt_wss_get_stats(args->client);
+ aclk_stats_mqtt_wss(&mqtt_wss_stats);
+
+ aclk_stats_newproto_rx(aclk_stats_cfg.aclk_proto_rx_msgs_sample);
+ }
+
+ return 0;
+}
+
+void aclk_stats_upd_online(int online) {
+ if(!aclk_stats_enabled)
+ return;
+
+ ACLK_STATS_LOCK;
+ aclk_metrics.online = online;
+
+ if(!online)
+ aclk_metrics_per_sample.offline_during_sample = 1;
+ ACLK_STATS_UNLOCK;
+}
+
+#ifdef NETDATA_INTERNAL_CHECKS
+static usec_t pub_time[UINT16_MAX + 1] = {0};
+void aclk_stats_msg_published(uint16_t id)
+{
+ ACLK_STATS_LOCK;
+ pub_time[id] = now_boottime_usec();
+ ACLK_STATS_UNLOCK;
+}
+
+void aclk_stats_msg_puback(uint16_t id)
+{
+ ACLK_STATS_LOCK;
+ usec_t t;
+
+ if (!aclk_stats_enabled) {
+ ACLK_STATS_UNLOCK;
+ return;
+ }
+
+ if (unlikely(!pub_time[id])) {
+ ACLK_STATS_UNLOCK;
+ netdata_log_error("Received PUBACK for unknown message?!");
+ return;
+ }
+
+ t = now_boottime_usec() - pub_time[id];
+ t /= USEC_PER_MS;
+ pub_time[id] = 0;
+ if (aclk_metrics_per_sample.latency_max < t)
+ aclk_metrics_per_sample.latency_max = t;
+
+ aclk_metrics_per_sample.latency_total += t;
+ aclk_metrics_per_sample.latency_count++;
+ ACLK_STATS_UNLOCK;
+}
+#endif /* NETDATA_INTERNAL_CHECKS */
diff --git a/src/aclk/aclk_stats.h b/src/aclk/aclk_stats.h
new file mode 100644
index 000000000..e13269557
--- /dev/null
+++ b/src/aclk/aclk_stats.h
@@ -0,0 +1,77 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef NETDATA_ACLK_STATS_H
+#define NETDATA_ACLK_STATS_H
+
+#include "daemon/common.h"
+#include "libnetdata/libnetdata.h"
+#include "aclk_query_queue.h"
+#include "mqtt_websockets/mqtt_wss_client.h"
+
+extern netdata_mutex_t aclk_stats_mutex;
+
+#define ACLK_STATS_LOCK netdata_mutex_lock(&aclk_stats_mutex)
+#define ACLK_STATS_UNLOCK netdata_mutex_unlock(&aclk_stats_mutex)
+
+// if you change update `cloud_req_http_type_names`.
+#define ACLK_STATS_CLOUD_HTTP_REQ_TYPE_CNT 9
+
+int aclk_cloud_req_http_type_to_idx(const char *name);
+
+struct aclk_stats_thread {
+ ND_THREAD *thread;
+ int query_thread_count;
+ mqtt_wss_client client;
+};
+
+// preserve between samples
+struct aclk_metrics {
+ volatile uint8_t online;
+};
+
+// reset to 0 on every sample
+extern struct aclk_metrics_per_sample {
+ /* in the unlikely event of ACLK disconnecting
+ and reconnecting under 1 sampling rate
+ we want to make sure we record the disconnection
+ despite it being then seemingly longer in graph */
+ volatile uint8_t offline_during_sample;
+
+ volatile uint32_t queries_queued;
+ volatile uint32_t queries_dispatched;
+
+#ifdef NETDATA_INTERNAL_CHECKS
+ volatile uint32_t latency_max;
+ volatile uint32_t latency_total;
+ volatile uint32_t latency_count;
+#endif
+
+ volatile uint32_t cloud_req_recvd;
+ volatile uint32_t cloud_req_err;
+
+ // query types.
+ volatile uint32_t queries_per_type[ACLK_QUERY_TYPE_COUNT];
+
+ // HTTP-specific request types.
+ volatile uint32_t cloud_req_http_by_type[ACLK_STATS_CLOUD_HTTP_REQ_TYPE_CNT];
+
+ volatile uint32_t cloud_q_process_total;
+ volatile uint32_t cloud_q_process_count;
+ volatile uint32_t cloud_q_process_max;
+} aclk_metrics_per_sample;
+
+extern uint32_t *aclk_proto_rx_msgs_sample;
+
+extern uint32_t *aclk_queries_per_thread;
+
+void *aclk_stats_main_thread(void *ptr);
+void aclk_stats_thread_prepare(int query_thread_count, unsigned int proto_hdl_cnt);
+void aclk_stats_thread_cleanup();
+void aclk_stats_upd_online(int online);
+
+#ifdef NETDATA_INTERNAL_CHECKS
+void aclk_stats_msg_published(uint16_t id);
+void aclk_stats_msg_puback(uint16_t id);
+#endif /* NETDATA_INTERNAL_CHECKS */
+
+#endif /* NETDATA_ACLK_STATS_H */
diff --git a/src/aclk/aclk_tx_msgs.c b/src/aclk/aclk_tx_msgs.c
new file mode 100644
index 000000000..c1ed68052
--- /dev/null
+++ b/src/aclk/aclk_tx_msgs.c
@@ -0,0 +1,276 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_tx_msgs.h"
+#include "daemon/common.h"
+#include "aclk_util.h"
+#include "aclk_stats.h"
+#include "aclk.h"
+#include "aclk_capas.h"
+
+#include "schema-wrappers/proto_2_json.h"
+
+#ifndef __GNUC__
+#pragma region aclk_tx_msgs helper functions
+#endif
+
+// version for aclk legacy (old cloud arch)
+#define ACLK_VERSION 2
+
+static void freez_aclk_publish5a(void *ptr) {
+ freez(ptr);
+}
+static void freez_aclk_publish5b(void *ptr) {
+ freez(ptr);
+}
+
+uint16_t aclk_send_bin_message_subtopic_pid(mqtt_wss_client client, char *msg, size_t msg_len, enum aclk_topics subtopic, const char *msgname)
+{
+#ifndef ACLK_LOG_CONVERSATION_DIR
+ UNUSED(msgname);
+#endif
+ uint16_t packet_id;
+ const char *topic = aclk_get_topic(subtopic);
+
+ if (unlikely(!topic)) {
+ netdata_log_error("Couldn't get topic. Aborting message send.");
+ return 0;
+ }
+
+ mqtt_wss_publish5(client, (char*)topic, NULL, msg, &freez_aclk_publish5a, msg_len, MQTT_WSS_PUB_QOS1, &packet_id);
+
+#ifdef NETDATA_INTERNAL_CHECKS
+ aclk_stats_msg_published(packet_id);
+#endif
+
+ if (aclklog_enabled) {
+ char *json = protomsg_to_json(msg, msg_len, msgname);
+ log_aclk_message_bin(json, strlen(json), 1, topic, msgname);
+ freez(json);
+ }
+
+ return packet_id;
+}
+
+#define TOPIC_MAX_LEN 512
+#define V2_BIN_PAYLOAD_SEPARATOR "\x0D\x0A\x0D\x0A"
+static int aclk_send_message_with_bin_payload(mqtt_wss_client client, json_object *msg, const char *topic, const void *payload, size_t payload_len)
+{
+ uint16_t packet_id;
+ const char *str;
+ char *full_msg = NULL;
+ int len;
+
+ if (unlikely(!topic || topic[0] != '/')) {
+ netdata_log_error("Full topic required!");
+ json_object_put(msg);
+ return HTTP_RESP_INTERNAL_SERVER_ERROR;
+ }
+
+ str = json_object_to_json_string_ext(msg, JSON_C_TO_STRING_PLAIN);
+ len = strlen(str);
+
+ size_t full_msg_len = len;
+ if (payload_len)
+ full_msg_len += strlen(V2_BIN_PAYLOAD_SEPARATOR) + payload_len;
+
+ full_msg = mallocz(full_msg_len);
+ memcpy(full_msg, str, len);
+ json_object_put(msg);
+
+ if (payload_len) {
+ memcpy(&full_msg[len], V2_BIN_PAYLOAD_SEPARATOR, strlen(V2_BIN_PAYLOAD_SEPARATOR));
+ len += strlen(V2_BIN_PAYLOAD_SEPARATOR);
+ memcpy(&full_msg[len], payload, payload_len);
+ }
+
+ int rc = mqtt_wss_publish5(client, (char*)topic, NULL, full_msg, &freez_aclk_publish5b, full_msg_len, MQTT_WSS_PUB_QOS1, &packet_id);
+
+ if (rc == MQTT_WSS_ERR_TOO_BIG_FOR_SERVER)
+ return HTTP_RESP_CONTENT_TOO_LONG;
+
+#ifdef NETDATA_INTERNAL_CHECKS
+ aclk_stats_msg_published(packet_id);
+#endif
+
+ return 0;
+}
+
+/*
+ * Creates universal header common for all ACLK messages. User gets ownership of json object created.
+ * Usually this is freed by send function after message has been sent.
+ */
+static struct json_object *create_hdr(const char *type, const char *msg_id, time_t ts_secs, usec_t ts_us, int version)
+{
+ nd_uuid_t uuid;
+ char uuid_str[36 + 1];
+ json_object *tmp;
+ json_object *obj = json_object_new_object();
+
+ tmp = json_object_new_string(type);
+ json_object_object_add(obj, "type", tmp);
+
+ if (unlikely(!msg_id)) {
+ uuid_generate(uuid);
+ uuid_unparse(uuid, uuid_str);
+ msg_id = uuid_str;
+ }
+
+ if (ts_secs == 0) {
+ ts_us = now_realtime_usec();
+ ts_secs = ts_us / USEC_PER_SEC;
+ ts_us = ts_us % USEC_PER_SEC;
+ }
+
+ tmp = json_object_new_string(msg_id);
+ json_object_object_add(obj, "msg-id", tmp);
+
+ tmp = json_object_new_int64(ts_secs);
+ json_object_object_add(obj, "timestamp", tmp);
+
+// TODO handle this somehow on older json-c
+// tmp = json_object_new_uint64(ts_us);
+// probably jso->_to_json_string -> custom function
+// jso->o.c_uint64 -> map this with pointer to signed int
+// commit that implements json_object_new_uint64 is 3c3b592
+// between 0.14 and 0.15
+ tmp = json_object_new_int64(ts_us);
+ json_object_object_add(obj, "timestamp-offset-usec", tmp);
+
+ tmp = json_object_new_int64(aclk_session_sec);
+ json_object_object_add(obj, "connect", tmp);
+
+// TODO handle this somehow see above
+// tmp = json_object_new_uint64(0 /* TODO aclk_session_us */);
+ tmp = json_object_new_int64(aclk_session_us);
+ json_object_object_add(obj, "connect-offset-usec", tmp);
+
+ tmp = json_object_new_int(version);
+ json_object_object_add(obj, "version", tmp);
+
+ return obj;
+}
+
+#ifndef __GNUC__
+#pragma endregion
+#endif
+
+#ifndef __GNUC__
+#pragma region aclk_tx_msgs message generators
+#endif
+
+void aclk_http_msg_v2_err(mqtt_wss_client client, const char *topic, const char *msg_id, int http_code, int ec, const char* emsg, const char *payload, size_t payload_len)
+{
+ json_object *tmp, *msg;
+ msg = create_hdr("http", msg_id, 0, 0, 2);
+ tmp = json_object_new_int(http_code);
+ json_object_object_add(msg, "http-code", tmp);
+
+ tmp = json_object_new_int(ec);
+ json_object_object_add(msg, "error-code", tmp);
+
+ tmp = json_object_new_string(emsg);
+ json_object_object_add(msg, "error-description", tmp);
+
+ if (aclk_send_message_with_bin_payload(client, msg, topic, payload, payload_len)) {
+ netdata_log_error("Failed to send cancellation message for http reply %zu %s", payload_len, payload);
+ }
+}
+
+int aclk_http_msg_v2(mqtt_wss_client client, const char *topic, const char *msg_id, usec_t t_exec, usec_t created, int http_code, const char *payload, size_t payload_len)
+{
+ json_object *tmp, *msg;
+
+ msg = create_hdr("http", msg_id, 0, 0, 2);
+
+ tmp = json_object_new_int64(t_exec);
+ json_object_object_add(msg, "t-exec", tmp);
+
+ tmp = json_object_new_int64(created);
+ json_object_object_add(msg, "t-rx", tmp);
+
+ tmp = json_object_new_int(http_code);
+ json_object_object_add(msg, "http-code", tmp);
+
+ int rc = aclk_send_message_with_bin_payload(client, msg, topic, payload, payload_len);
+
+ switch (rc) {
+ case HTTP_RESP_CONTENT_TOO_LONG:
+ aclk_http_msg_v2_err(client, topic, msg_id, rc, CLOUD_EC_REQ_REPLY_TOO_BIG, CLOUD_EMSG_REQ_REPLY_TOO_BIG, NULL, 0);
+ break;
+ case HTTP_RESP_INTERNAL_SERVER_ERROR:
+ aclk_http_msg_v2_err(client, topic, msg_id, rc, CLOUD_EC_FAIL_TOPIC, CLOUD_EMSG_FAIL_TOPIC, payload, payload_len);
+ break;
+ case HTTP_RESP_GATEWAY_TIMEOUT:
+ case HTTP_RESP_SERVICE_UNAVAILABLE:
+ aclk_http_msg_v2_err(client, topic, msg_id, rc, CLOUD_EC_SND_TIMEOUT, CLOUD_EMSG_SND_TIMEOUT, payload, payload_len);
+ break;
+ }
+ return rc ? rc : http_code;
+}
+
+uint16_t aclk_send_agent_connection_update(mqtt_wss_client client, int reachable) {
+ size_t len;
+ uint16_t pid;
+
+ update_agent_connection_t conn = {
+ .reachable = (reachable ? 1 : 0),
+ .lwt = 0,
+ .session_id = aclk_session_newarch,
+ .capabilities = aclk_get_agent_capas()
+ };
+
+ rrdhost_aclk_state_lock(localhost);
+ if (unlikely(!localhost->aclk_state.claimed_id)) {
+ netdata_log_error("Internal error. Should not come here if not claimed");
+ rrdhost_aclk_state_unlock(localhost);
+ return 0;
+ }
+ if (localhost->aclk_state.prev_claimed_id)
+ conn.claim_id = localhost->aclk_state.prev_claimed_id;
+ else
+ conn.claim_id = localhost->aclk_state.claimed_id;
+
+ char *msg = generate_update_agent_connection(&len, &conn);
+ rrdhost_aclk_state_unlock(localhost);
+
+ if (!msg) {
+ netdata_log_error("Error generating agent::v1::UpdateAgentConnection payload");
+ return 0;
+ }
+
+ pid = aclk_send_bin_message_subtopic_pid(client, msg, len, ACLK_TOPICID_AGENT_CONN, "UpdateAgentConnection");
+ if (localhost->aclk_state.prev_claimed_id) {
+ freez(localhost->aclk_state.prev_claimed_id);
+ localhost->aclk_state.prev_claimed_id = NULL;
+ }
+ return pid;
+}
+
+char *aclk_generate_lwt(size_t *size) {
+ update_agent_connection_t conn = {
+ .reachable = 0,
+ .lwt = 1,
+ .session_id = aclk_session_newarch,
+ .capabilities = NULL
+ };
+
+ rrdhost_aclk_state_lock(localhost);
+ if (unlikely(!localhost->aclk_state.claimed_id)) {
+ netdata_log_error("Internal error. Should not come here if not claimed");
+ rrdhost_aclk_state_unlock(localhost);
+ return NULL;
+ }
+ conn.claim_id = localhost->aclk_state.claimed_id;
+
+ char *msg = generate_update_agent_connection(size, &conn);
+ rrdhost_aclk_state_unlock(localhost);
+
+ if (!msg)
+ netdata_log_error("Error generating agent::v1::UpdateAgentConnection payload for LWT");
+
+ return msg;
+}
+
+#ifndef __GNUC__
+#pragma endregion
+#endif
diff --git a/src/aclk/aclk_tx_msgs.h b/src/aclk/aclk_tx_msgs.h
new file mode 100644
index 000000000..86ed20c38
--- /dev/null
+++ b/src/aclk/aclk_tx_msgs.h
@@ -0,0 +1,20 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+#ifndef ACLK_TX_MSGS_H
+#define ACLK_TX_MSGS_H
+
+#include <json-c/json.h>
+#include "libnetdata/libnetdata.h"
+#include "daemon/common.h"
+#include "mqtt_websockets/mqtt_wss_client.h"
+#include "schema-wrappers/schema_wrappers.h"
+#include "aclk_util.h"
+
+uint16_t aclk_send_bin_message_subtopic_pid(mqtt_wss_client client, char *msg, size_t msg_len, enum aclk_topics subtopic, const char *msgname);
+
+void aclk_http_msg_v2_err(mqtt_wss_client client, const char *topic, const char *msg_id, int http_code, int ec, const char* emsg, const char *payload, size_t payload_len);
+int aclk_http_msg_v2(mqtt_wss_client client, const char *topic, const char *msg_id, usec_t t_exec, usec_t created, int http_code, const char *payload, size_t payload_len);
+
+uint16_t aclk_send_agent_connection_update(mqtt_wss_client client, int reachable);
+char *aclk_generate_lwt(size_t *size);
+
+#endif
diff --git a/src/aclk/aclk_util.c b/src/aclk/aclk_util.c
new file mode 100644
index 000000000..3bf2e3f18
--- /dev/null
+++ b/src/aclk/aclk_util.c
@@ -0,0 +1,484 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "aclk_util.h"
+
+#ifdef ENABLE_ACLK
+
+#include "aclk_proxy.h"
+
+#include "daemon/common.h"
+
+usec_t aclk_session_newarch = 0;
+
+aclk_env_t *aclk_env = NULL;
+
+int chart_batch_id;
+
+aclk_encoding_type_t aclk_encoding_type_t_from_str(const char *str) {
+ if (!strcmp(str, "json")) {
+ return ACLK_ENC_JSON;
+ }
+ if (!strcmp(str, "proto")) {
+ return ACLK_ENC_PROTO;
+ }
+ return ACLK_ENC_UNKNOWN;
+}
+
+aclk_transport_type_t aclk_transport_type_t_from_str(const char *str) {
+ if (!strcmp(str, "MQTTv3")) {
+ return ACLK_TRP_MQTT_3_1_1;
+ }
+ if (!strcmp(str, "MQTTv5")) {
+ return ACLK_TRP_MQTT_5;
+ }
+ return ACLK_TRP_UNKNOWN;
+}
+
+void aclk_transport_desc_t_destroy(aclk_transport_desc_t *trp_desc) {
+ freez(trp_desc->endpoint);
+}
+
+void aclk_env_t_destroy(aclk_env_t *env) {
+ freez(env->auth_endpoint);
+ if (env->transports) {
+ for (size_t i = 0; i < env->transport_count; i++) {
+ if(env->transports[i]) {
+ aclk_transport_desc_t_destroy(env->transports[i]);
+ freez(env->transports[i]);
+ env->transports[i] = NULL;
+ }
+ }
+ freez(env->transports);
+ }
+ if (env->capabilities) {
+ for (size_t i = 0; i < env->capability_count; i++)
+ freez(env->capabilities[i]);
+ freez(env->capabilities);
+ }
+}
+
+int aclk_env_has_capa(const char *capa)
+{
+ for (int i = 0; i < (int) aclk_env->capability_count; i++) {
+ if (!strcasecmp(capa, aclk_env->capabilities[i]))
+ return 1;
+ }
+ return 0;
+}
+
+#ifdef ACLK_LOG_CONVERSATION_DIR
+volatile int aclk_conversation_log_counter = 0;
+#endif
+
+#define ACLK_TOPIC_PREFIX "/agent/"
+
+struct aclk_topic {
+ enum aclk_topics topic_id;
+ // as received from cloud - we keep this for
+ // eventual topic list update when claim_id changes
+ char *topic_recvd;
+ // constructed topic
+ char *topic;
+};
+
+// This helps to cache finalized topics (assembled with claim_id)
+// to not have to alloc or create buffer and construct topic every
+// time message is sent as in old ACLK
+static struct aclk_topic **aclk_topic_cache = NULL;
+static size_t aclk_topic_cache_items = 0;
+
+void free_topic_cache(void)
+{
+ if (aclk_topic_cache) {
+ for (size_t i = 0; i < aclk_topic_cache_items; i++) {
+ freez(aclk_topic_cache[i]->topic);
+ freez(aclk_topic_cache[i]->topic_recvd);
+ freez(aclk_topic_cache[i]);
+ }
+ freez(aclk_topic_cache);
+ aclk_topic_cache = NULL;
+ aclk_topic_cache_items = 0;
+ }
+}
+
+#define JSON_TOPIC_KEY_TOPIC "topic"
+#define JSON_TOPIC_KEY_NAME "name"
+
+struct topic_name {
+ enum aclk_topics id;
+ // cloud name - how is it called
+ // in answer to /password endpoint
+ const char *name;
+} topic_names[] = {
+ { .id = ACLK_TOPICID_CHART, .name = "chart" },
+ { .id = ACLK_TOPICID_ALARMS, .name = "alarms" },
+ { .id = ACLK_TOPICID_METADATA, .name = "meta" },
+ { .id = ACLK_TOPICID_COMMAND, .name = "inbox-cmd" },
+ { .id = ACLK_TOPICID_AGENT_CONN, .name = "agent-connection" },
+ { .id = ACLK_TOPICID_CMD_NG_V1, .name = "inbox-cmd-v1" },
+ { .id = ACLK_TOPICID_CREATE_NODE, .name = "create-node-instance" },
+ { .id = ACLK_TOPICID_NODE_CONN, .name = "node-instance-connection" },
+ { .id = ACLK_TOPICID_CHART_DIMS, .name = "chart-and-dims-updated" },
+ { .id = ACLK_TOPICID_CHART_CONFIGS_UPDATED, .name = "chart-configs-updated" },
+ { .id = ACLK_TOPICID_CHART_RESET, .name = "reset-charts" },
+ { .id = ACLK_TOPICID_RETENTION_UPDATED, .name = "chart-retention-updated" },
+ { .id = ACLK_TOPICID_NODE_INFO, .name = "node-instance-info" },
+ { .id = ACLK_TOPICID_ALARM_LOG, .name = "alarm-log-v2" },
+ { .id = ACLK_TOPICID_ALARM_CHECKPOINT, .name = "alarm-checkpoint" },
+ { .id = ACLK_TOPICID_ALARM_CONFIG, .name = "alarm-config" },
+ { .id = ACLK_TOPICID_ALARM_SNAPSHOT, .name = "alarm-snapshot-v2" },
+ { .id = ACLK_TOPICID_NODE_COLLECTORS, .name = "node-instance-collectors" },
+ { .id = ACLK_TOPICID_CTXS_SNAPSHOT, .name = "contexts-snapshot" },
+ { .id = ACLK_TOPICID_CTXS_UPDATED, .name = "contexts-updated" },
+ { .id = ACLK_TOPICID_UNKNOWN, .name = NULL }
+};
+
+enum aclk_topics compulsory_topics[] = {
+// TODO remove old topics once not needed anymore
+ ACLK_TOPICID_CHART, //TODO from legacy
+ ACLK_TOPICID_ALARMS, //TODO from legacy
+ ACLK_TOPICID_METADATA, //TODO from legacy
+ ACLK_TOPICID_COMMAND,
+ ACLK_TOPICID_AGENT_CONN,
+ ACLK_TOPICID_CMD_NG_V1,
+ ACLK_TOPICID_CREATE_NODE,
+ ACLK_TOPICID_NODE_CONN,
+ ACLK_TOPICID_CHART_DIMS,
+ ACLK_TOPICID_CHART_CONFIGS_UPDATED,
+ ACLK_TOPICID_CHART_RESET,
+ ACLK_TOPICID_RETENTION_UPDATED,
+ ACLK_TOPICID_NODE_INFO,
+ ACLK_TOPICID_ALARM_LOG,
+ ACLK_TOPICID_ALARM_CHECKPOINT,
+ ACLK_TOPICID_ALARM_CONFIG,
+ ACLK_TOPICID_ALARM_SNAPSHOT,
+ ACLK_TOPICID_NODE_COLLECTORS,
+ ACLK_TOPICID_CTXS_SNAPSHOT,
+ ACLK_TOPICID_CTXS_UPDATED,
+ ACLK_TOPICID_UNKNOWN
+};
+
+static enum aclk_topics topic_name_to_id(const char *name) {
+ struct topic_name *topic = topic_names;
+ while (topic->name) {
+ if (!strcmp(topic->name, name)) {
+ return topic->id;
+ }
+ topic++;
+ }
+ return ACLK_TOPICID_UNKNOWN;
+}
+
+static const char *topic_id_to_name(enum aclk_topics tid) {
+ struct topic_name *topic = topic_names;
+ while (topic->name) {
+ if (topic->id == tid)
+ return topic->name;
+ topic++;
+ }
+ return "unknown";
+}
+
+#define CLAIM_ID_REPLACE_TAG "#{claim_id}"
+static void topic_generate_final(struct aclk_topic *t) {
+ char *dest;
+ char *replace_tag = strstr(t->topic_recvd, CLAIM_ID_REPLACE_TAG);
+ if (!replace_tag)
+ return;
+
+ rrdhost_aclk_state_lock(localhost);
+ if (unlikely(!localhost->aclk_state.claimed_id)) {
+ netdata_log_error("This should never be called if agent not claimed");
+ rrdhost_aclk_state_unlock(localhost);
+ return;
+ }
+
+ t->topic = mallocz(strlen(t->topic_recvd) + 1 - strlen(CLAIM_ID_REPLACE_TAG) + strlen(localhost->aclk_state.claimed_id));
+ memcpy(t->topic, t->topic_recvd, replace_tag - t->topic_recvd);
+ dest = t->topic + (replace_tag - t->topic_recvd);
+
+ memcpy(dest, localhost->aclk_state.claimed_id, strlen(localhost->aclk_state.claimed_id));
+ dest += strlen(localhost->aclk_state.claimed_id);
+ rrdhost_aclk_state_unlock(localhost);
+ replace_tag += strlen(CLAIM_ID_REPLACE_TAG);
+ strcpy(dest, replace_tag);
+ dest += strlen(replace_tag);
+ *dest = 0;
+}
+
+static int topic_cache_add_topic(struct json_object *json, struct aclk_topic *topic)
+{
+ struct json_object_iterator it;
+ struct json_object_iterator itEnd;
+
+ it = json_object_iter_begin(json);
+ itEnd = json_object_iter_end(json);
+
+ while (!json_object_iter_equal(&it, &itEnd)) {
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_TOPIC_KEY_NAME)) {
+ if (json_object_get_type(json_object_iter_peek_value(&it)) != json_type_string) {
+ netdata_log_error("topic dictionary key \"" JSON_TOPIC_KEY_NAME "\" is expected to be json_type_string");
+ return 1;
+ }
+ topic->topic_id = topic_name_to_id(json_object_get_string(json_object_iter_peek_value(&it)));
+ if (topic->topic_id == ACLK_TOPICID_UNKNOWN) {
+ netdata_log_debug(D_ACLK, "topic dictionary has unknown topic name \"%s\"", json_object_get_string(json_object_iter_peek_value(&it)));
+ }
+ json_object_iter_next(&it);
+ continue;
+ }
+ if (!strcmp(json_object_iter_peek_name(&it), JSON_TOPIC_KEY_TOPIC)) {
+ if (json_object_get_type(json_object_iter_peek_value(&it)) != json_type_string) {
+ netdata_log_error("topic dictionary key \"" JSON_TOPIC_KEY_TOPIC "\" is expected to be json_type_string");
+ return 1;
+ }
+ topic->topic_recvd = strdupz(json_object_get_string(json_object_iter_peek_value(&it)));
+ json_object_iter_next(&it);
+ continue;
+ }
+
+ netdata_log_error("topic dictionary has Unknown/Unexpected key \"%s\" in topic description. Ignoring!", json_object_iter_peek_name(&it));
+ json_object_iter_next(&it);
+ }
+
+ if (!topic->topic_recvd) {
+ netdata_log_error("topic dictionary Missig compulsory key %s", JSON_TOPIC_KEY_TOPIC);
+ return 1;
+ }
+
+ topic_generate_final(topic);
+ aclk_topic_cache_items++;
+
+ return 0;
+}
+
+int aclk_generate_topic_cache(struct json_object *json)
+{
+ json_object *obj;
+
+ size_t array_size = json_object_array_length(json);
+ if (!array_size) {
+ netdata_log_error("Empty topic list!");
+ return 1;
+ }
+
+ if (aclk_topic_cache)
+ free_topic_cache();
+
+ aclk_topic_cache = callocz(array_size, sizeof(struct aclk_topic *));
+
+ for (size_t i = 0; i < array_size; i++) {
+ obj = json_object_array_get_idx(json, i);
+ if (json_object_get_type(obj) != json_type_object) {
+ netdata_log_error("expected json_type_object");
+ return 1;
+ }
+ aclk_topic_cache[i] = callocz(1, sizeof(struct aclk_topic));
+ if (topic_cache_add_topic(obj, aclk_topic_cache[i])) {
+ netdata_log_error("failed to parse topic @idx=%d", (int)i);
+ return 1;
+ }
+ }
+
+ for (int i = 0; compulsory_topics[i] != ACLK_TOPICID_UNKNOWN; i++) {
+ if (!aclk_get_topic(compulsory_topics[i])) {
+ netdata_log_error("missing compulsory topic \"%s\" in password response from cloud", topic_id_to_name(compulsory_topics[i]));
+ return 1;
+ }
+ }
+
+ return 0;
+}
+
+/*
+ * Build a topic based on sub_topic and final_topic
+ * if the sub topic starts with / assume that is an absolute topic
+ *
+ */
+const char *aclk_get_topic(enum aclk_topics topic)
+{
+ if (!aclk_topic_cache) {
+ netdata_log_error("Topic cache not initialized");
+ return NULL;
+ }
+
+ for (size_t i = 0; i < aclk_topic_cache_items; i++) {
+ if (aclk_topic_cache[i]->topic_id == topic)
+ return aclk_topic_cache[i]->topic;
+ }
+ netdata_log_error("Unknown topic");
+ return NULL;
+}
+
+/*
+ * Allows iterating all topics in topic cache without
+ * having to resort to callbacks.
+ */
+
+const char *aclk_topic_cache_iterate(aclk_topic_cache_iter_t *iter)
+{
+ if (!aclk_topic_cache) {
+ netdata_log_error("Topic cache not initialized when %s was called.", __FUNCTION__);
+ return NULL;
+ }
+
+ if (*iter >= aclk_topic_cache_items)
+ return NULL;
+
+ return aclk_topic_cache[(*iter)++]->topic;
+}
+
+/*
+ * TBEB with randomness
+ *
+ * @param reset 1 - to reset the delay,
+ * 0 - to advance a step and calculate sleep time in ms
+ * @param min, max in seconds
+ * @returns delay in ms
+ *
+ */
+
+unsigned long int aclk_tbeb_delay(int reset, int base, unsigned long int min, unsigned long int max) {
+ static int attempt = -1;
+
+ if (reset) {
+ attempt = -1;
+ return 0;
+ }
+
+ attempt++;
+
+ if (attempt == 0) {
+ srandom(time(NULL));
+ return 0;
+ }
+
+ unsigned long int delay = pow(base, attempt - 1);
+ delay *= MSEC_PER_SEC;
+
+ delay += (random() % (MAX(1000, delay/2)));
+
+ if (delay <= min * MSEC_PER_SEC)
+ return min;
+
+ if (delay >= max * MSEC_PER_SEC)
+ return max;
+
+ return delay;
+}
+
+static inline int aclk_parse_pair(const char *src, const char c, char **a, char **b)
+{
+ const char *ptr = strchr(src, c);
+ if (ptr == NULL)
+ return 1;
+
+// allow empty string
+/* if (!*(ptr+1))
+ return 1;*/
+
+ *a = callocz(1, ptr - src + 1);
+ memcpy (*a, src, ptr - src);
+
+ *b = strdupz(ptr+1);
+
+ return 0;
+}
+
+#define HTTP_PROXY_PREFIX "http://"
+void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd, enum mqtt_wss_proxy_type *type)
+{
+ ACLK_PROXY_TYPE pt;
+ const char *ptr = aclk_get_proxy(&pt);
+ char *tmp;
+
+ if (pt != PROXY_TYPE_HTTP)
+ return;
+
+ *uname = NULL;
+ *pwd = NULL;
+ *port = 0;
+
+ char *proxy = strdupz(ptr);
+ ptr = proxy;
+
+ if (!strncmp(ptr, HTTP_PROXY_PREFIX, strlen(HTTP_PROXY_PREFIX)))
+ ptr += strlen(HTTP_PROXY_PREFIX);
+
+ if ((tmp = strchr(ptr, '@'))) {
+ *tmp = 0;
+ if(aclk_parse_pair(ptr, ':', uname, pwd)) {
+ error_report("Failed to get username and password for proxy. Will attempt connection without authentication");
+ }
+ ptr = tmp+1;
+ }
+
+ if (!*ptr) {
+ freez(proxy);
+ freez(*uname);
+ freez(*pwd);
+ return;
+ }
+
+ if ((tmp = strchr(ptr, ':'))) {
+ *tmp = 0;
+ tmp++;
+ if(*tmp)
+ *port = atoi(tmp);
+ }
+ *ohost = strdupz(ptr);
+
+ if (*port <= 0 || *port > 65535)
+ *port = 8080;
+
+ if (type)
+ *type = MQTT_WSS_PROXY_HTTP;
+ else {
+ freez(*uname);
+ freez(*pwd);
+ }
+
+ freez(proxy);
+}
+#endif /* ENABLE_ACLK */
+
+#if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_110
+static EVP_ENCODE_CTX *EVP_ENCODE_CTX_new(void)
+{
+ EVP_ENCODE_CTX *ctx = OPENSSL_malloc(sizeof(*ctx));
+
+ if (ctx != NULL) {
+ memset(ctx, 0, sizeof(*ctx));
+ }
+ return ctx;
+}
+static void EVP_ENCODE_CTX_free(EVP_ENCODE_CTX *ctx)
+{
+ OPENSSL_free(ctx);
+ return;
+}
+#endif
+
+int base64_encode_helper(unsigned char *out, int *outl, const unsigned char *in, int in_len)
+{
+ int len;
+ unsigned char *str = out;
+ EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
+ EVP_EncodeInit(ctx);
+ EVP_EncodeUpdate(ctx, str, outl, in, in_len);
+ str += *outl;
+ EVP_EncodeFinal(ctx, str, &len);
+ *outl += len;
+
+ str = out;
+ while(*str) {
+ if (*str != 0x0D && *str != 0x0A)
+ *out++ = *str++;
+ else
+ str++;
+ }
+ *out = 0;
+
+ EVP_ENCODE_CTX_free(ctx);
+ return 0;
+}
diff --git a/src/aclk/aclk_util.h b/src/aclk/aclk_util.h
new file mode 100644
index 000000000..6c0239cc3
--- /dev/null
+++ b/src/aclk/aclk_util.h
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+#ifndef ACLK_UTIL_H
+#define ACLK_UTIL_H
+
+#include "libnetdata/libnetdata.h"
+
+#ifdef ENABLE_ACLK
+#include "mqtt_websockets/mqtt_wss_client.h"
+
+#define CLOUD_EC_MALFORMED_NODE_ID 1
+#define CLOUD_EMSG_MALFORMED_NODE_ID "URL requests node_id but there is not enough chars following (for it to be valid uuid)."
+#define CLOUD_EC_NODE_NOT_FOUND 2
+#define CLOUD_EMSG_NODE_NOT_FOUND "Node with requested node_id not found"
+#define CLOUD_EC_ZLIB_ERROR 3
+#define CLOUD_EMSG_ZLIB_ERROR "Error during zlib compression"
+#define CLOUD_EC_REQ_REPLY_TOO_BIG 4
+#define CLOUD_EMSG_REQ_REPLY_TOO_BIG "Request reply produces message bigger than allowed maximum"
+#define CLOUD_EC_FAIL_TOPIC 5
+#define CLOUD_EMSG_FAIL_TOPIC "Internal Topic Error"
+#define CLOUD_EC_SND_TIMEOUT 6
+#define CLOUD_EMSG_SND_TIMEOUT "Timeout sending binpacked message"
+
+// Helper stuff which should not have any further inside ACLK dependency
+// and are supposed not to be needed outside of ACLK
+extern usec_t aclk_session_newarch;
+
+extern int chart_batch_id;
+
+typedef enum {
+ ACLK_ENC_UNKNOWN = 0,
+ ACLK_ENC_JSON,
+ ACLK_ENC_PROTO
+} aclk_encoding_type_t;
+
+typedef enum {
+ ACLK_TRP_UNKNOWN = 0,
+ ACLK_TRP_MQTT_3_1_1,
+ ACLK_TRP_MQTT_5
+} aclk_transport_type_t;
+
+typedef struct {
+ char *endpoint;
+ aclk_transport_type_t type;
+} aclk_transport_desc_t;
+
+typedef struct {
+ int base;
+ int max_s;
+ int min_s;
+} aclk_backoff_t;
+
+typedef struct {
+ char *auth_endpoint;
+ aclk_encoding_type_t encoding;
+
+ aclk_transport_desc_t **transports;
+ size_t transport_count;
+
+ char **capabilities;
+ size_t capability_count;
+
+ aclk_backoff_t backoff;
+} aclk_env_t;
+
+extern aclk_env_t *aclk_env;
+
+aclk_encoding_type_t aclk_encoding_type_t_from_str(const char *str);
+aclk_transport_type_t aclk_transport_type_t_from_str(const char *str);
+
+void aclk_transport_desc_t_destroy(aclk_transport_desc_t *trp_desc);
+void aclk_env_t_destroy(aclk_env_t *env);
+int aclk_env_has_capa(const char *capa);
+
+enum aclk_topics {
+ ACLK_TOPICID_UNKNOWN = 0,
+ ACLK_TOPICID_CHART = 1,
+ ACLK_TOPICID_ALARMS = 2,
+ ACLK_TOPICID_METADATA = 3,
+ ACLK_TOPICID_COMMAND = 4,
+ ACLK_TOPICID_AGENT_CONN = 5,
+ ACLK_TOPICID_CMD_NG_V1 = 6,
+ ACLK_TOPICID_CREATE_NODE = 7,
+ ACLK_TOPICID_NODE_CONN = 8,
+ ACLK_TOPICID_CHART_DIMS = 9,
+ ACLK_TOPICID_CHART_CONFIGS_UPDATED = 10,
+ ACLK_TOPICID_CHART_RESET = 11,
+ ACLK_TOPICID_RETENTION_UPDATED = 12,
+ ACLK_TOPICID_NODE_INFO = 13,
+ ACLK_TOPICID_ALARM_LOG = 14,
+ ACLK_TOPICID_ALARM_CHECKPOINT = 15,
+ ACLK_TOPICID_ALARM_CONFIG = 16,
+ ACLK_TOPICID_ALARM_SNAPSHOT = 17,
+ ACLK_TOPICID_NODE_COLLECTORS = 18,
+ ACLK_TOPICID_CTXS_SNAPSHOT = 19,
+ ACLK_TOPICID_CTXS_UPDATED = 20
+};
+
+typedef size_t aclk_topic_cache_iter_t;
+#define ACLK_TOPIC_CACHE_ITER_T_INITIALIZER (0)
+
+const char *aclk_get_topic(enum aclk_topics topic);
+int aclk_generate_topic_cache(struct json_object *json);
+void free_topic_cache(void);
+const char *aclk_topic_cache_iterate(aclk_topic_cache_iter_t *iter);
+// TODO
+// aclk_topics_reload //when claim id changes
+
+#ifdef ACLK_LOG_CONVERSATION_DIR
+extern volatile int aclk_conversation_log_counter;
+#define ACLK_GET_CONV_LOG_NEXT() __atomic_fetch_add(&aclk_conversation_log_counter, 1, __ATOMIC_SEQ_CST)
+#endif
+
+unsigned long int aclk_tbeb_delay(int reset, int base, unsigned long int min, unsigned long int max);
+#define aclk_tbeb_reset(x) aclk_tbeb_delay(1, 0, 0, 0)
+
+void aclk_set_proxy(char **ohost, int *port, char **uname, char **pwd, enum mqtt_wss_proxy_type *type);
+#endif /* ENABLE_ACLK */
+
+int base64_encode_helper(unsigned char *out, int *outl, const unsigned char *in, int in_len);
+
+#endif /* ACLK_UTIL_H */
diff --git a/src/aclk/helpers/mqtt_wss_pal.h b/src/aclk/helpers/mqtt_wss_pal.h
new file mode 100644
index 000000000..5c89f8bb7
--- /dev/null
+++ b/src/aclk/helpers/mqtt_wss_pal.h
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef MQTT_WSS_PAL_H
+#define MQTT_WSS_PAL_H
+
+#include "libnetdata/libnetdata.h"
+
+#undef OPENSSL_VERSION_095
+#undef OPENSSL_VERSION_097
+#undef OPENSSL_VERSION_110
+#undef OPENSSL_VERSION_111
+
+#define mw_malloc(...) mallocz(__VA_ARGS__)
+#define mw_calloc(...) callocz(__VA_ARGS__)
+#define mw_free(...) freez(__VA_ARGS__)
+#define mw_strdup(...) strdupz(__VA_ARGS__)
+#define mw_realloc(...) reallocz(__VA_ARGS__)
+
+#endif /* MQTT_WSS_PAL_H */
diff --git a/src/aclk/helpers/ringbuffer_pal.h b/src/aclk/helpers/ringbuffer_pal.h
new file mode 100644
index 000000000..2f7e1cb93
--- /dev/null
+++ b/src/aclk/helpers/ringbuffer_pal.h
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef RINGBUFFER_PAL_H
+#define RINGBUFFER_PAL_H
+
+#include "libnetdata/libnetdata.h"
+
+#define crbuf_malloc(...) mallocz(__VA_ARGS__)
+#define crbuf_free(...) freez(__VA_ARGS__)
+
+#endif /* RINGBUFFER_PAL_H */
diff --git a/src/aclk/https_client.c b/src/aclk/https_client.c
new file mode 100644
index 000000000..2bc768f24
--- /dev/null
+++ b/src/aclk/https_client.c
@@ -0,0 +1,866 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "libnetdata/libnetdata.h"
+
+#include "https_client.h"
+
+#include "aclk_util.h"
+
+#include "daemon/global_statistics.h"
+
+static const char *http_req_type_to_str(http_req_type_t req) {
+ switch (req) {
+ case HTTP_REQ_GET:
+ return "GET";
+ case HTTP_REQ_POST:
+ return "POST";
+ case HTTP_REQ_CONNECT:
+ return "CONNECT";
+ default:
+ return "unknown";
+ }
+}
+
+#define TRANSFER_ENCODING_CHUNKED (-2)
+
+void http_parse_ctx_create(http_parse_ctx *ctx, enum http_parse_state parse_state)
+{
+ ctx->state = parse_state;
+ ctx->content_length = -1;
+ ctx->http_code = 0;
+ ctx->headers = c_rhash_new(0);
+ ctx->flags = HTTP_PARSE_FLAGS_DEFAULT;
+}
+
+void http_parse_ctx_destroy(http_parse_ctx *ctx)
+{
+ c_rhash_iter_t iter;
+ const char *key;
+
+ c_rhash_iter_t_initialize(&iter);
+ while ( !c_rhash_iter_str_keys(ctx->headers, &iter, &key) ) {
+ void *val;
+ c_rhash_get_ptr_by_str(ctx->headers, key, &val);
+ freez(val);
+ }
+
+ c_rhash_destroy(ctx->headers);
+}
+
+#define POLL_TO_MS 100
+
+#define HTTP_LINE_TERM "\x0D\x0A"
+#define RESP_PROTO "HTTP/1.1 "
+#define RESP_PROTO10 "HTTP/1.0 "
+#define HTTP_KEYVAL_SEPARATOR ": "
+#define HTTP_HDR_BUFFER_SIZE 1024
+#define PORT_STR_MAX_BYTES 12
+
+static int process_http_hdr(http_parse_ctx *parse_ctx, const char *key, const char *val)
+{
+ // currently we care only about specific headers
+ // we can skip the rest
+ if (parse_ctx->content_length < 0 && !strcmp("content-length", key)) {
+ if (parse_ctx->content_length == TRANSFER_ENCODING_CHUNKED) {
+ netdata_log_error("Content-length and transfer-encoding: chunked headers are mutually exclusive");
+ return 1;
+ }
+ if (parse_ctx->content_length != -1) {
+ netdata_log_error("Duplicate content-length header");
+ return 1;
+ }
+ parse_ctx->content_length = str2u(val);
+ if (parse_ctx->content_length < 0) {
+ netdata_log_error("Invalid content-length %d", parse_ctx->content_length);
+ return 1;
+ }
+ return 0;
+ }
+ if (!strcmp("transfer-encoding", key)) {
+ if (!strcmp("chunked", val)) {
+ if (parse_ctx->content_length != -1) {
+ netdata_log_error("Content-length and transfer-encoding: chunked headers are mutually exclusive");
+ return 1;
+ }
+ parse_ctx->content_length = TRANSFER_ENCODING_CHUNKED;
+ }
+ return 0;
+ }
+ char *val_cpy = strdupz(val);
+ c_rhash_insert_str_ptr(parse_ctx->headers, key, val_cpy);
+ return 0;
+}
+
+const char *get_http_header_by_name(http_parse_ctx *ctx, const char *name)
+{
+ const char *ret;
+ if (c_rhash_get_ptr_by_str(ctx->headers, name, (void**)&ret))
+ return NULL;
+
+ return ret;
+}
+
+static int parse_http_hdr(rbuf_t buf, http_parse_ctx *parse_ctx)
+{
+ int idx, idx_end;
+ char buf_key[HTTP_HDR_BUFFER_SIZE];
+ char buf_val[HTTP_HDR_BUFFER_SIZE];
+ char *ptr = buf_key;
+ if (!rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx_end)) {
+ netdata_log_error("CRLF expected");
+ return 1;
+ }
+
+ char *separator = rbuf_find_bytes(buf, HTTP_KEYVAL_SEPARATOR, strlen(HTTP_KEYVAL_SEPARATOR), &idx);
+ if (!separator) {
+ netdata_log_error("Missing Key/Value separator");
+ return 1;
+ }
+ if (idx >= HTTP_HDR_BUFFER_SIZE) {
+ netdata_log_error("Key name is too long");
+ return 1;
+ }
+
+ rbuf_pop(buf, buf_key, idx);
+ buf_key[idx] = 0;
+
+ rbuf_bump_tail(buf, strlen(HTTP_KEYVAL_SEPARATOR));
+ idx_end -= strlen(HTTP_KEYVAL_SEPARATOR) + idx;
+ if (idx_end >= HTTP_HDR_BUFFER_SIZE) {
+ netdata_log_error("Value of key \"%s\" too long", buf_key);
+ return 1;
+ }
+
+ rbuf_pop(buf, buf_val, idx_end);
+ buf_val[idx_end] = 0;
+
+ for (ptr = buf_key; *ptr; ptr++)
+ *ptr = tolower(*ptr);
+
+ if (process_http_hdr(parse_ctx, buf_key, buf_val))
+ return 1;
+
+ return 0;
+}
+
+static inline void chunked_response_buffer_grow_by(http_parse_ctx *parse_ctx, size_t size)
+{
+ if (unlikely(parse_ctx->chunked_response_size == 0)) {
+ parse_ctx->chunked_response = mallocz(size);
+ parse_ctx->chunked_response_size = size;
+ return;
+ }
+ parse_ctx->chunked_response = reallocz((void *)parse_ctx->chunked_response, parse_ctx->chunked_response_size + size);
+ parse_ctx->chunked_response_size += size;
+}
+
+static int process_chunked_content(rbuf_t buf, http_parse_ctx *parse_ctx)
+{
+ int idx;
+ size_t bytes_to_copy;
+
+ do {
+ switch (parse_ctx->chunked_content_state) {
+ case CHUNKED_CONTENT_CHUNK_SIZE:
+ if (!rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx)) {
+ if (rbuf_bytes_available(buf) >= rbuf_get_capacity(buf))
+ return HTTP_PARSE_ERROR;
+ return HTTP_PARSE_NEED_MORE_DATA;
+ }
+ if (idx == 0) {
+ parse_ctx->chunked_content_state = CHUNKED_CONTENT_FINAL_CRLF;
+ continue;
+ }
+ if (idx >= HTTP_HDR_BUFFER_SIZE) {
+ netdata_log_error("Chunk size is too long");
+ return HTTP_PARSE_ERROR;
+ }
+ char buf_size[HTTP_HDR_BUFFER_SIZE];
+ rbuf_pop(buf, buf_size, idx);
+ buf_size[idx] = 0;
+ long chunk_size = strtol(buf_size, NULL, 16);
+ if (chunk_size < 0 || chunk_size == LONG_MAX) {
+ netdata_log_error("Chunk size out of range");
+ return HTTP_PARSE_ERROR;
+ }
+ parse_ctx->chunk_size = chunk_size;
+ if (parse_ctx->chunk_size == 0) {
+ if (errno == EINVAL) {
+ netdata_log_error("Invalid chunk size");
+ return HTTP_PARSE_ERROR;
+ }
+ parse_ctx->chunked_content_state = CHUNKED_CONTENT_CHUNK_END_CRLF;
+ continue;
+ }
+ parse_ctx->chunk_got = 0;
+ chunked_response_buffer_grow_by(parse_ctx, parse_ctx->chunk_size);
+ rbuf_bump_tail(buf, strlen(HTTP_LINE_TERM));
+ parse_ctx->chunked_content_state = CHUNKED_CONTENT_CHUNK_DATA;
+ // fallthrough
+ case CHUNKED_CONTENT_CHUNK_DATA:
+ if (!(bytes_to_copy = rbuf_bytes_available(buf)))
+ return HTTP_PARSE_NEED_MORE_DATA;
+ if (bytes_to_copy > parse_ctx->chunk_size - parse_ctx->chunk_got)
+ bytes_to_copy = parse_ctx->chunk_size - parse_ctx->chunk_got;
+ rbuf_pop(buf, parse_ctx->chunked_response + parse_ctx->chunked_response_written, bytes_to_copy);
+ parse_ctx->chunk_got += bytes_to_copy;
+ parse_ctx->chunked_response_written += bytes_to_copy;
+ if (parse_ctx->chunk_got != parse_ctx->chunk_size)
+ continue;
+ parse_ctx->chunked_content_state = CHUNKED_CONTENT_CHUNK_END_CRLF;
+ // fallthrough
+ case CHUNKED_CONTENT_FINAL_CRLF:
+ case CHUNKED_CONTENT_CHUNK_END_CRLF:
+ if (rbuf_bytes_available(buf) < strlen(HTTP_LINE_TERM))
+ return HTTP_PARSE_NEED_MORE_DATA;
+ char buf_crlf[strlen(HTTP_LINE_TERM)];
+ rbuf_pop(buf, buf_crlf, strlen(HTTP_LINE_TERM));
+ if (memcmp(buf_crlf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM))) {
+ netdata_log_error("CRLF expected");
+ return HTTP_PARSE_ERROR;
+ }
+ if (parse_ctx->chunked_content_state == CHUNKED_CONTENT_FINAL_CRLF) {
+ if (parse_ctx->chunked_response_size != parse_ctx->chunked_response_written)
+ netdata_log_error("Chunked response size mismatch");
+ chunked_response_buffer_grow_by(parse_ctx, 1);
+ parse_ctx->chunked_response[parse_ctx->chunked_response_written] = 0;
+ return HTTP_PARSE_SUCCESS;
+ }
+ if (parse_ctx->chunk_size == 0) {
+ parse_ctx->chunked_content_state = CHUNKED_CONTENT_FINAL_CRLF;
+ continue;
+ }
+ parse_ctx->chunked_content_state = CHUNKED_CONTENT_CHUNK_SIZE;
+ continue;
+ }
+ } while(1);
+}
+
+http_parse_rc parse_http_response(rbuf_t buf, http_parse_ctx *parse_ctx)
+{
+ int idx;
+ char rc[4];
+
+ do {
+ if (parse_ctx->state != HTTP_PARSE_CONTENT && !rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx))
+ return HTTP_PARSE_NEED_MORE_DATA;
+ switch (parse_ctx->state) {
+ case HTTP_PARSE_PROXY_CONNECT:
+ case HTTP_PARSE_INITIAL:
+ if (rbuf_memcmp_n(buf, RESP_PROTO, strlen(RESP_PROTO))) {
+ if (parse_ctx->state == HTTP_PARSE_PROXY_CONNECT) {
+ if (rbuf_memcmp_n(buf, RESP_PROTO10, strlen(RESP_PROTO10))) {
+ netdata_log_error(
+ "Expected response to start with \"%s\" or \"%s\"", RESP_PROTO, RESP_PROTO10);
+ return HTTP_PARSE_ERROR;
+ }
+ }
+ else {
+ netdata_log_error("Expected response to start with \"%s\"", RESP_PROTO);
+ return HTTP_PARSE_ERROR;
+ }
+ }
+ rbuf_bump_tail(buf, strlen(RESP_PROTO));
+ if (rbuf_pop(buf, rc, 4) != 4) {
+ netdata_log_error("Expected HTTP status code");
+ return HTTP_PARSE_ERROR;
+ }
+ if (rc[3] != ' ') {
+ netdata_log_error("Expected space after HTTP return code");
+ return HTTP_PARSE_ERROR;
+ }
+ rc[3] = 0;
+ parse_ctx->http_code = atoi(rc);
+ if (parse_ctx->http_code < 100 || parse_ctx->http_code >= 600) {
+ netdata_log_error("HTTP code not in range 100 to 599");
+ return HTTP_PARSE_ERROR;
+ }
+
+ rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx);
+
+ rbuf_bump_tail(buf, idx + strlen(HTTP_LINE_TERM));
+
+ parse_ctx->state = HTTP_PARSE_HEADERS;
+ break;
+ case HTTP_PARSE_HEADERS:
+ if (!idx) {
+ parse_ctx->state = HTTP_PARSE_CONTENT;
+ rbuf_bump_tail(buf, strlen(HTTP_LINE_TERM));
+ break;
+ }
+ if (parse_http_hdr(buf, parse_ctx))
+ return HTTP_PARSE_ERROR;
+ rbuf_find_bytes(buf, HTTP_LINE_TERM, strlen(HTTP_LINE_TERM), &idx);
+ rbuf_bump_tail(buf, idx + strlen(HTTP_LINE_TERM));
+ break;
+ case HTTP_PARSE_CONTENT:
+ // replies like CONNECT etc. do not have content
+ if (parse_ctx->content_length == TRANSFER_ENCODING_CHUNKED)
+ return process_chunked_content(buf, parse_ctx);
+
+ if (parse_ctx->content_length < 0)
+ return HTTP_PARSE_SUCCESS;
+
+ if (parse_ctx->flags & HTTP_PARSE_FLAG_DONT_WAIT_FOR_CONTENT)
+ return HTTP_PARSE_SUCCESS;
+
+ if (rbuf_bytes_available(buf) >= (size_t)parse_ctx->content_length)
+ return HTTP_PARSE_SUCCESS;
+ return HTTP_PARSE_NEED_MORE_DATA;
+ }
+ } while(1);
+}
+
+typedef struct https_req_ctx {
+ https_req_t *request;
+
+ int sock;
+ rbuf_t buf_rx;
+
+ struct pollfd poll_fd;
+
+ SSL_CTX *ssl_ctx;
+ SSL *ssl;
+
+ size_t written;
+
+ http_parse_ctx parse_ctx;
+
+ time_t req_start_time;
+} https_req_ctx_t;
+
+static int https_req_check_timedout(https_req_ctx_t *ctx) {
+ if (now_realtime_sec() > ctx->req_start_time + ctx->request->timeout_s) {
+ netdata_log_error("request timed out");
+ return 1;
+ }
+ return 0;
+}
+
+static char *_ssl_err_tos(int err)
+{
+ switch(err){
+ case SSL_ERROR_SSL:
+ return "SSL_ERROR_SSL";
+ case SSL_ERROR_WANT_READ:
+ return "SSL_ERROR_WANT_READ";
+ case SSL_ERROR_WANT_WRITE:
+ return "SSL_ERROR_WANT_WRITE";
+ case SSL_ERROR_NONE:
+ return "SSL_ERROR_NONE";
+ case SSL_ERROR_ZERO_RETURN:
+ return "SSL_ERROR_ZERO_RETURN";
+ case SSL_ERROR_WANT_CONNECT:
+ return "SSL_ERROR_WANT_CONNECT";
+ case SSL_ERROR_WANT_ACCEPT:
+ return "SSL_ERROR_WANT_ACCEPT";
+ }
+ return "Unknown!!!";
+}
+
+static int socket_write_all(https_req_ctx_t *ctx, char *data, size_t data_len) {
+ ctx->written = 0;
+ ctx->poll_fd.events = POLLOUT;
+
+ do {
+ int ret = poll(&ctx->poll_fd, 1, POLL_TO_MS);
+ if (ret < 0) {
+ netdata_log_error("poll error");
+ return 1;
+ }
+ if (ret == 0) {
+ if (https_req_check_timedout(ctx)) {
+ netdata_log_error("Poll timed out");
+ return 2;
+ }
+ continue;
+ }
+
+ ret = write(ctx->sock, &data[ctx->written], data_len - ctx->written);
+ if (ret > 0) {
+ ctx->written += ret;
+ } else if (errno != EAGAIN && errno != EWOULDBLOCK) {
+ netdata_log_error("Error writing to socket");
+ return 3;
+ }
+ } while (ctx->written < data_len);
+
+ return 0;
+}
+
+static int ssl_write_all(https_req_ctx_t *ctx, char *data, size_t data_len) {
+ ctx->written = 0;
+ ctx->poll_fd.events |= POLLOUT;
+
+ do {
+ int ret = poll(&ctx->poll_fd, 1, POLL_TO_MS);
+ if (ret < 0) {
+ netdata_log_error("poll error");
+ return 1;
+ }
+ if (ret == 0) {
+ if (https_req_check_timedout(ctx)) {
+ netdata_log_error("Poll timed out");
+ return 2;
+ }
+ continue;
+ }
+ ctx->poll_fd.events = 0;
+
+ ret = SSL_write(ctx->ssl, &data[ctx->written], data_len - ctx->written);
+ if (ret > 0) {
+ ctx->written += ret;
+ } else {
+ ret = SSL_get_error(ctx->ssl, ret);
+ switch (ret) {
+ case SSL_ERROR_WANT_READ:
+ ctx->poll_fd.events |= POLLIN;
+ break;
+ case SSL_ERROR_WANT_WRITE:
+ ctx->poll_fd.events |= POLLOUT;
+ break;
+ default:
+ netdata_log_error("SSL_write Err: %s", _ssl_err_tos(ret));
+ return 3;
+ }
+ }
+ } while (ctx->written < data_len);
+
+ return 0;
+}
+
+static inline int https_client_write_all(https_req_ctx_t *ctx, char *data, size_t data_len) {
+ if (ctx->ssl_ctx)
+ return ssl_write_all(ctx, data, data_len);
+ return socket_write_all(ctx, data, data_len);
+}
+
+static int read_parse_response(https_req_ctx_t *ctx) {
+ int ret;
+ char *ptr;
+ size_t size;
+
+ ctx->poll_fd.events = POLLIN;
+ do {
+ ret = poll(&ctx->poll_fd, 1, POLL_TO_MS);
+ if (ret < 0) {
+ netdata_log_error("poll error");
+ return 1;
+ }
+ if (ret == 0) {
+ if (https_req_check_timedout(ctx)) {
+ netdata_log_error("Poll timed out");
+ return 2;
+ }
+ if (!ctx->ssl_ctx)
+ continue;
+ }
+ ctx->poll_fd.events = 0;
+
+ do {
+ ptr = rbuf_get_linear_insert_range(ctx->buf_rx, &size);
+
+ if (ctx->ssl_ctx)
+ ret = SSL_read(ctx->ssl, ptr, size);
+ else
+ ret = read(ctx->sock, ptr, size);
+
+ if (ret > 0) {
+ rbuf_bump_head(ctx->buf_rx, ret);
+ } else {
+ if (ctx->ssl_ctx) {
+ ret = SSL_get_error(ctx->ssl, ret);
+ switch (ret) {
+ case SSL_ERROR_WANT_READ:
+ ctx->poll_fd.events |= POLLIN;
+ break;
+ case SSL_ERROR_WANT_WRITE:
+ ctx->poll_fd.events |= POLLOUT;
+ break;
+ default:
+ netdata_log_error("SSL_read Err: %s", _ssl_err_tos(ret));
+ return 3;
+ }
+ } else {
+ if (errno != EAGAIN && errno != EWOULDBLOCK) {
+ netdata_log_error("write error");
+ return 3;
+ }
+ ctx->poll_fd.events |= POLLIN;
+ }
+ }
+ } while (ctx->poll_fd.events == 0 && rbuf_bytes_free(ctx->buf_rx) > 0);
+ } while (!(ret = parse_http_response(ctx->buf_rx, &ctx->parse_ctx)));
+
+ if (ret != HTTP_PARSE_SUCCESS) {
+ netdata_log_error("Error parsing HTTP response");
+ return 1;
+ }
+
+ return 0;
+}
+
+static const char *http_methods[] = {
+ [HTTP_REQ_GET] = "GET ",
+ [HTTP_REQ_POST] = "POST ",
+ [HTTP_REQ_CONNECT] = "CONNECT ",
+};
+
+
+#define TX_BUFFER_SIZE 8192
+#define RX_BUFFER_SIZE (TX_BUFFER_SIZE*2)
+static int handle_http_request(https_req_ctx_t *ctx) {
+ BUFFER *hdr = buffer_create(TX_BUFFER_SIZE, &netdata_buffers_statistics.buffers_aclk);
+ int rc = 0;
+
+ http_req_type_t req_type = ctx->request->request_type;
+
+ if (req_type >= HTTP_REQ_INVALID) {
+ netdata_log_error("Unknown HTTPS request type!");
+ rc = 1;
+ goto err_exit;
+ }
+ buffer_strcat(hdr, http_methods[req_type]);
+
+ if (req_type == HTTP_REQ_CONNECT) {
+ buffer_strcat(hdr, ctx->request->host);
+ buffer_sprintf(hdr, ":%d", ctx->request->port);
+ http_parse_ctx_create(&ctx->parse_ctx, HTTP_PARSE_PROXY_CONNECT);
+ }
+ else {
+ buffer_strcat(hdr, ctx->request->url);
+ http_parse_ctx_create(&ctx->parse_ctx, HTTP_PARSE_INITIAL);
+ }
+
+ buffer_strcat(hdr, HTTP_1_1 HTTP_ENDL);
+
+ //TODO Headers!
+ buffer_sprintf(hdr, "Host: %s\x0D\x0A", ctx->request->host);
+ buffer_strcat(hdr, "User-Agent: Netdata/rocks newhttpclient\x0D\x0A");
+
+ if (req_type == HTTP_REQ_POST && ctx->request->payload && ctx->request->payload_size) {
+ buffer_sprintf(hdr, "Content-Length: %zu\x0D\x0A", ctx->request->payload_size);
+ }
+ if (ctx->request->proxy_username) {
+ size_t creds_plain_len = strlen(ctx->request->proxy_username) + strlen(ctx->request->proxy_password) + 1 /* ':' */;
+ char *creds_plain = callocz(1, creds_plain_len + 1);
+ char *ptr = creds_plain;
+ strcpy(ptr, ctx->request->proxy_username);
+ ptr += strlen(ctx->request->proxy_username);
+ *ptr++ = ':';
+ strcpy(ptr, ctx->request->proxy_password);
+
+ int creds_base64_len = (((4 * creds_plain_len / 3) + 3) & ~3);
+ // OpenSSL encoder puts newline every 64 output bytes
+ // we remove those but during encoding we need that space in the buffer
+ creds_base64_len += (1+(creds_base64_len/64)) * strlen("\n");
+ char *creds_base64 = callocz(1, creds_base64_len + 1);
+ base64_encode_helper((unsigned char*)creds_base64, &creds_base64_len, (unsigned char*)creds_plain, creds_plain_len);
+ buffer_sprintf(hdr, "Proxy-Authorization: Basic %s\x0D\x0A", creds_base64);
+ freez(creds_plain);
+ }
+
+ buffer_strcat(hdr, "\x0D\x0A");
+
+ // Send the request
+ if (https_client_write_all(ctx, hdr->buffer, hdr->len)) {
+ netdata_log_error("Couldn't write HTTP request header into SSL connection");
+ rc = 2;
+ goto err_exit;
+ }
+
+ if (req_type == HTTP_REQ_POST && ctx->request->payload && ctx->request->payload_size) {
+ if (https_client_write_all(ctx, ctx->request->payload, ctx->request->payload_size)) {
+ netdata_log_error("Couldn't write payload into SSL connection");
+ rc = 3;
+ goto err_exit;
+ }
+ }
+
+ // Read The Response
+ if (read_parse_response(ctx)) {
+ netdata_log_error("Error reading or parsing response from server");
+ if (ctx->parse_ctx.chunked_response)
+ freez(ctx->parse_ctx.chunked_response);
+ rc = 4;
+ goto err_exit;
+ }
+
+err_exit:
+ buffer_free(hdr);
+ return rc;
+}
+
+static int cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
+{
+ X509 *err_cert;
+ int err, depth;
+ char *err_str;
+
+ if (!preverify_ok) {
+ err = X509_STORE_CTX_get_error(ctx);
+ depth = X509_STORE_CTX_get_error_depth(ctx);
+ err_cert = X509_STORE_CTX_get_current_cert(ctx);
+ err_str = X509_NAME_oneline(X509_get_subject_name(err_cert), NULL, 0);
+
+ netdata_log_error("Cert Chain verify error:num=%d:%s:depth=%d:%s", err,
+ X509_verify_cert_error_string(err), depth, err_str);
+
+ free(err_str);
+ }
+
+#ifdef ACLK_SSL_ALLOW_SELF_SIGNED
+ if (!preverify_ok && err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT)
+ {
+ preverify_ok = 1;
+ netdata_log_error("Self Signed Certificate Accepted as the agent was built with ACLK_SSL_ALLOW_SELF_SIGNED");
+ }
+#endif
+
+ return preverify_ok;
+}
+
+int https_request(https_req_t *request, https_req_response_t *response) {
+ int rc = 1, ret;
+ char connect_port_str[PORT_STR_MAX_BYTES];
+
+ const char *connect_host = request->proxy_host ? request->proxy_host : request->host;
+ int connect_port = request->proxy_host ? request->proxy_port : request->port;
+ struct timeval timeout = { .tv_sec = request->timeout_s, .tv_usec = 0 };
+
+ https_req_ctx_t *ctx = callocz(1, sizeof(https_req_ctx_t));
+ ctx->req_start_time = now_realtime_sec();
+
+ ctx->buf_rx = rbuf_create(RX_BUFFER_SIZE);
+ if (!ctx->buf_rx) {
+ netdata_log_error("Couldn't allocate buffer for RX data");
+ goto exit_req_ctx;
+ }
+
+ snprintfz(connect_port_str, PORT_STR_MAX_BYTES, "%d", connect_port);
+
+ ctx->sock = connect_to_this_ip46(IPPROTO_TCP, SOCK_STREAM, connect_host, 0, connect_port_str, &timeout);
+ if (ctx->sock < 0) {
+ netdata_log_error("Error connecting TCP socket to \"%s\"", connect_host);
+ goto exit_buf_rx;
+ }
+
+ if (fcntl(ctx->sock, F_SETFL, fcntl(ctx->sock, F_GETFL, 0) | O_NONBLOCK) == -1) {
+ netdata_log_error("Error setting O_NONBLOCK to TCP socket.");
+ goto exit_sock;
+ }
+
+ ctx->poll_fd.fd = ctx->sock;
+
+ // Do the CONNECT if proxy is used
+ if (request->proxy_host) {
+ https_req_t req = HTTPS_REQ_T_INITIALIZER;
+ req.request_type = HTTP_REQ_CONNECT;
+ req.timeout_s = request->timeout_s;
+ req.host = request->host;
+ req.port = request->port;
+ req.url = request->url;
+ req.proxy_username = request->proxy_username;
+ req.proxy_password = request->proxy_password;
+ ctx->request = &req;
+ if (handle_http_request(ctx)) {
+ netdata_log_error("Failed to CONNECT with proxy");
+ http_parse_ctx_destroy(&ctx->parse_ctx);
+ goto exit_sock;
+ }
+ if (ctx->parse_ctx.http_code != 200) {
+ netdata_log_error("Proxy didn't return 200 OK (got %d)", ctx->parse_ctx.http_code);
+ http_parse_ctx_destroy(&ctx->parse_ctx);
+ goto exit_sock;
+ }
+ http_parse_ctx_destroy(&ctx->parse_ctx);
+ netdata_log_info("Proxy accepted CONNECT upgrade");
+ }
+ ctx->request = request;
+
+ ctx->ssl_ctx = netdata_ssl_create_client_ctx(0);
+ if (ctx->ssl_ctx==NULL) {
+ netdata_log_error("Cannot allocate SSL context");
+ goto exit_sock;
+ }
+
+ if (!SSL_CTX_set_default_verify_paths(ctx->ssl_ctx)) {
+ netdata_log_error("Error setting default verify paths");
+ goto exit_CTX;
+ }
+ SSL_CTX_set_verify(ctx->ssl_ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, cert_verify_callback);
+
+ ctx->ssl = SSL_new(ctx->ssl_ctx);
+ if (ctx->ssl==NULL) {
+ netdata_log_error("Cannot allocate SSL");
+ goto exit_CTX;
+ }
+
+ if (!SSL_set_tlsext_host_name(ctx->ssl, connect_host)) {
+ netdata_log_error("Error setting TLS SNI host");
+ goto exit_CTX;
+ }
+
+ SSL_set_fd(ctx->ssl, ctx->sock);
+ ret = SSL_connect(ctx->ssl);
+ if (ret != -1 && ret != 1) {
+ netdata_log_error("SSL could not connect");
+ goto exit_SSL;
+ }
+ if (ret == -1) {
+ // expected as underlying socket is non blocking!
+ // consult SSL_connect documentation for details
+ int ec = SSL_get_error(ctx->ssl, ret);
+ if (ec != SSL_ERROR_WANT_READ && ec != SSL_ERROR_WANT_WRITE) {
+ netdata_log_error("Failed to start SSL connection");
+ goto exit_SSL;
+ }
+ }
+
+ // The actual request here
+ if (handle_http_request(ctx)) {
+ netdata_log_error("Couldn't process request");
+ http_parse_ctx_destroy(&ctx->parse_ctx);
+ goto exit_SSL;
+ }
+ http_parse_ctx_destroy(&ctx->parse_ctx);
+ response->http_code = ctx->parse_ctx.http_code;
+ if (ctx->parse_ctx.content_length == TRANSFER_ENCODING_CHUNKED) {
+ response->payload_size = ctx->parse_ctx.chunked_response_size;
+ response->payload = ctx->parse_ctx.chunked_response;
+ }
+ if (ctx->parse_ctx.content_length > 0) {
+ response->payload_size = ctx->parse_ctx.content_length;
+ response->payload = mallocz(response->payload_size + 1);
+ ret = rbuf_pop(ctx->buf_rx, response->payload, response->payload_size);
+ if (ret != (int)response->payload_size) {
+ netdata_log_error("Payload size doesn't match remaining data on the buffer!");
+ response->payload_size = ret;
+ }
+ // normally we take payload as it is and copy it
+ // but for convenience in cases where payload is sth. like
+ // json we add terminating zero so that user of the data
+ // doesn't have to convert to C string (0 terminated)
+ // other uses still have correct payload_size and can copy
+ // only exact data without affixed 0x00
+ ((char*)response->payload)[response->payload_size] = 0; // mallocz(response->payload_size + 1);
+ }
+ netdata_log_info("HTTPS \"%s\" request to \"%s\" finished with HTTP code: %d", http_req_type_to_str(ctx->request->request_type), ctx->request->host, response->http_code);
+
+ rc = 0;
+
+exit_SSL:
+ SSL_free(ctx->ssl);
+exit_CTX:
+ SSL_CTX_free(ctx->ssl_ctx);
+exit_sock:
+ close(ctx->sock);
+exit_buf_rx:
+ rbuf_free(ctx->buf_rx);
+exit_req_ctx:
+ freez(ctx);
+ return rc;
+}
+
+void https_req_response_free(https_req_response_t *res) {
+ freez(res->payload);
+}
+
+static inline char *UNUSED_FUNCTION(min_non_null)(char *a, char *b) {
+ if (!a)
+ return b;
+ if (!b)
+ return a;
+ return (a < b ? a : b);
+}
+
+#define URI_PROTO_SEPARATOR "://"
+#define URL_PARSER_LOG_PREFIX "url_parser "
+
+static int parse_host_port(url_t *url) {
+ char *ptr = strrchr(url->host, ':');
+ if (ptr) {
+ size_t port_len = strlen(ptr + 1);
+ if (!port_len) {
+ netdata_log_error(URL_PARSER_LOG_PREFIX ": specified but no port number");
+ return 1;
+ }
+ if (port_len > 5 /* MAX port length is 5digit long in decimal */) {
+ netdata_log_error(URL_PARSER_LOG_PREFIX "port # is too long");
+ return 1;
+ }
+ *ptr = 0;
+ if (!strlen(url->host)) {
+ netdata_log_error(URL_PARSER_LOG_PREFIX "host empty after removing port");
+ return 1;
+ }
+ url->port = atoi (ptr + 1);
+ }
+ return 0;
+}
+
+static inline void port_by_proto(url_t *url) {
+ if (url->port)
+ return;
+ if (!url->proto)
+ return;
+ if (!strcmp(url->proto, "http")) {
+ url->port = 80;
+ return;
+ }
+ if (!strcmp(url->proto, "https")) {
+ url->port = 443;
+ return;
+ }
+}
+
+#define STRDUPZ_2PTR(dest, start, end) do { \
+ dest = mallocz(1 + end - start); \
+ memcpy(dest, start, end - start); \
+ dest[end - start] = 0; \
+ } while(0)
+
+int url_parse(const char *url, url_t *parsed) {
+ const char *start = url;
+ const char *end = strstr(url, URI_PROTO_SEPARATOR);
+
+ if (end) {
+ if (end == start) {
+ netdata_log_error(URL_PARSER_LOG_PREFIX "found " URI_PROTO_SEPARATOR " without protocol specified");
+ return 1;
+ }
+
+ STRDUPZ_2PTR(parsed->proto, start, end);
+ start = end + strlen(URI_PROTO_SEPARATOR);
+ }
+
+ end = strchr(start, '/');
+ if (!end)
+ end = start + strlen(start);
+
+ if (start == end) {
+ netdata_log_error(URL_PARSER_LOG_PREFIX "Host empty");
+ return 1;
+ }
+
+ STRDUPZ_2PTR(parsed->host, start, end);
+
+ if (parse_host_port(parsed))
+ return 1;
+
+ if (!*end) {
+ parsed->path = strdupz("/");
+ port_by_proto(parsed);
+ return 0;
+ }
+
+ parsed->path = strdupz(end);
+ port_by_proto(parsed);
+ return 0;
+}
+
+void url_t_destroy(url_t *url) {
+ freez(url->host);
+ freez(url->path);
+ freez(url->proto);
+}
diff --git a/src/aclk/https_client.h b/src/aclk/https_client.h
new file mode 100644
index 000000000..bc5ca30b8
--- /dev/null
+++ b/src/aclk/https_client.h
@@ -0,0 +1,136 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef NETDATA_HTTPS_CLIENT_H
+#define NETDATA_HTTPS_CLIENT_H
+
+#include "libnetdata/libnetdata.h"
+
+#include "mqtt_websockets/c-rbuf/cringbuffer.h"
+#include "mqtt_websockets/c_rhash/c_rhash.h"
+
+typedef enum http_req_type {
+ HTTP_REQ_GET = 0,
+ HTTP_REQ_POST,
+ HTTP_REQ_CONNECT,
+ HTTP_REQ_INVALID
+} http_req_type_t;
+
+typedef struct {
+ http_req_type_t request_type;
+
+ char *host;
+ int port;
+ char *url;
+
+ time_t timeout_s; //timeout in seconds for the network operation (send/recv)
+
+ void *payload;
+ size_t payload_size;
+
+ char *proxy_host;
+ int proxy_port;
+ const char *proxy_username;
+ const char *proxy_password;
+} https_req_t;
+
+typedef struct {
+ int http_code;
+
+ void *payload;
+ size_t payload_size;
+} https_req_response_t;
+
+
+// Non feature complete URL parser
+// feel free to extend when needed
+// currently implements only what ACLK
+// needs
+// proto://host[:port]/path
+typedef struct {
+ char *proto;
+ char *host;
+ int port;
+ char* path;
+} url_t;
+
+int url_parse(const char *url, url_t *parsed);
+void url_t_destroy(url_t *url);
+
+void https_req_response_free(https_req_response_t *res);
+
+#define HTTPS_REQ_RESPONSE_T_INITIALIZER \
+ { \
+ .http_code = 0, \
+ .payload = NULL, \
+ .payload_size = 0 \
+ }
+
+#define HTTPS_REQ_T_INITIALIZER \
+ { \
+ .request_type = HTTP_REQ_GET, \
+ .host = NULL, \
+ .port = 443, \
+ .url = NULL, \
+ .timeout_s = 30, \
+ .payload = NULL, \
+ .payload_size = 0, \
+ .proxy_host = NULL, \
+ .proxy_port = 8080 \
+ }
+
+int https_request(https_req_t *request, https_req_response_t *response);
+
+// we expose previously internal parser as this is usefull also from
+// other parts of the code
+enum http_parse_state {
+ HTTP_PARSE_PROXY_CONNECT = 0,
+ HTTP_PARSE_INITIAL,
+ HTTP_PARSE_HEADERS,
+ HTTP_PARSE_CONTENT
+};
+
+typedef uint32_t parse_ctx_flags_t;
+
+#define HTTP_PARSE_FLAG_DONT_WAIT_FOR_CONTENT ((parse_ctx_flags_t)0x01)
+
+#define HTTP_PARSE_FLAGS_DEFAULT ((parse_ctx_flags_t)0)
+
+typedef struct {
+ parse_ctx_flags_t flags;
+
+ enum http_parse_state state;
+ int content_length;
+ int http_code;
+
+ c_rhash headers;
+
+ // for chunked data only
+ char *chunked_response;
+ size_t chunked_response_size;
+ size_t chunked_response_written;
+
+ enum chunked_content_state {
+ CHUNKED_CONTENT_CHUNK_SIZE = 0,
+ CHUNKED_CONTENT_CHUNK_DATA,
+ CHUNKED_CONTENT_CHUNK_END_CRLF,
+ CHUNKED_CONTENT_FINAL_CRLF
+ } chunked_content_state;
+
+ size_t chunk_size;
+ size_t chunk_got;
+} http_parse_ctx;
+
+void http_parse_ctx_create(http_parse_ctx *ctx, enum http_parse_state parse_state);
+void http_parse_ctx_destroy(http_parse_ctx *ctx);
+
+typedef enum {
+ HTTP_PARSE_ERROR = -1,
+ HTTP_PARSE_NEED_MORE_DATA = 0,
+ HTTP_PARSE_SUCCESS = 1
+} http_parse_rc;
+
+http_parse_rc parse_http_response(rbuf_t buf, http_parse_ctx *parse_ctx);
+
+const char *get_http_header_by_name(http_parse_ctx *ctx, const char *name);
+
+#endif /* NETDATA_HTTPS_CLIENT_H */
diff --git a/src/aclk/mqtt_websockets/.github/workflows/run-tests.yaml b/src/aclk/mqtt_websockets/.github/workflows/run-tests.yaml
new file mode 100644
index 000000000..da5dde821
--- /dev/null
+++ b/src/aclk/mqtt_websockets/.github/workflows/run-tests.yaml
@@ -0,0 +1,14 @@
+name: run-tests
+on:
+ push:
+ schedule:
+ - cron: '5 3 * * 0'
+ pull_request:
+jobs:
+ run-tests:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Install ruby and deps
+ run: sudo apt-get install ruby ruby-dev mosquitto
+ - name: Checkout
+ uses: actions/checkout@v2
diff --git a/src/aclk/mqtt_websockets/.gitignore b/src/aclk/mqtt_websockets/.gitignore
new file mode 100644
index 000000000..9f1a0d89a
--- /dev/null
+++ b/src/aclk/mqtt_websockets/.gitignore
@@ -0,0 +1,10 @@
+build/*
+!build/.keep
+test
+.vscode
+mqtt/mqtt.c
+mqtt/include/mqtt.h
+libmqttwebsockets.*
+*.o
+.dirstamp
+.deps
diff --git a/src/aclk/mqtt_websockets/README.md b/src/aclk/mqtt_websockets/README.md
new file mode 100644
index 000000000..b159686df
--- /dev/null
+++ b/src/aclk/mqtt_websockets/README.md
@@ -0,0 +1,7 @@
+# mqtt_websockets
+
+Library to connect MQTT client over Websockets Secure (WSS).
+
+## License
+
+The Project is released under GPL v3 license. See [License](LICENSE)
diff --git a/src/aclk/mqtt_websockets/c-rbuf/cringbuffer.c b/src/aclk/mqtt_websockets/c-rbuf/cringbuffer.c
new file mode 100644
index 000000000..8950c6906
--- /dev/null
+++ b/src/aclk/mqtt_websockets/c-rbuf/cringbuffer.c
@@ -0,0 +1,203 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include "cringbuffer.h"
+#include "cringbuffer_internal.h"
+
+#include <stdlib.h>
+#include <assert.h>
+#include <string.h>
+
+#define MIN(a,b) (((a)<(b))?(a):(b))
+#define MAX(a,b) (((a)>(b))?(a):(b))
+
+// this allows user to use their own
+// custom memory allocation functions
+#ifdef RBUF_CUSTOM_MALLOC
+#include "../../helpers/ringbuffer_pal.h"
+#else
+#define crbuf_malloc(...) malloc(__VA_ARGS__)
+#define crbuf_free(...) free(__VA_ARGS__)
+#endif
+
+rbuf_t rbuf_create(size_t size)
+{
+ rbuf_t buffer = crbuf_malloc(sizeof(struct rbuf_t) + size);
+ if (!buffer)
+ return NULL;
+
+ memset(buffer, 0, sizeof(struct rbuf_t));
+
+ buffer->data = ((char*)buffer) + sizeof(struct rbuf_t);
+
+ buffer->head = buffer->data;
+ buffer->tail = buffer->data;
+ buffer->size = size;
+ buffer->end = buffer->data + size;
+
+ return buffer;
+}
+
+void rbuf_free(rbuf_t buffer)
+{
+ crbuf_free(buffer);
+}
+
+void rbuf_flush(rbuf_t buffer)
+{
+ buffer->head = buffer->data;
+ buffer->tail = buffer->data;
+ buffer->size_data = 0;
+}
+
+char *rbuf_get_linear_insert_range(rbuf_t buffer, size_t *bytes)
+{
+ *bytes = 0;
+ if (buffer->head == buffer->tail && buffer->size_data)
+ return NULL;
+
+ *bytes = ((buffer->head >= buffer->tail) ? buffer->end : buffer->tail) - buffer->head;
+ return buffer->head;
+}
+
+char *rbuf_get_linear_read_range(rbuf_t buffer, size_t *bytes)
+{
+ *bytes = 0;
+ if(buffer->head == buffer->tail && !buffer->size_data)
+ return NULL;
+
+ *bytes = ((buffer->tail >= buffer->head) ? buffer->end : buffer->head) - buffer->tail;
+
+ return buffer->tail;
+}
+
+int rbuf_bump_head(rbuf_t buffer, size_t bytes)
+{
+ size_t free_bytes = rbuf_bytes_free(buffer);
+ if (bytes > free_bytes)
+ return 0;
+ int i = buffer->head - buffer->data;
+ buffer->head = &buffer->data[(i + bytes) % buffer->size];
+ buffer->size_data += bytes;
+ return 1;
+}
+
+int rbuf_bump_tail(rbuf_t buffer, size_t bytes)
+{
+ if(!rbuf_bump_tail_noopt(buffer, bytes))
+ return 0;
+
+ // if tail catched up with head
+ // start writing buffer from beggining
+ // this is not necessary (rbuf must work well without it)
+ // but helps to optimize big writes as rbuf_get_linear_insert_range
+ // will return bigger continuous region
+ if(buffer->tail == buffer->head) {
+ assert(buffer->size_data == 0);
+ rbuf_flush(buffer);
+ }
+
+ return 1;
+}
+
+size_t rbuf_get_capacity(rbuf_t buffer)
+{
+ return buffer->size;
+}
+
+size_t rbuf_bytes_available(rbuf_t buffer)
+{
+ return buffer->size_data;
+}
+
+size_t rbuf_bytes_free(rbuf_t buffer)
+{
+ return buffer->size - buffer->size_data;
+}
+
+size_t rbuf_push(rbuf_t buffer, const char *data, size_t len)
+{
+ size_t to_cpy;
+ char *w_ptr = rbuf_get_linear_insert_range(buffer, &to_cpy);
+ if(!to_cpy)
+ return to_cpy;
+
+ to_cpy = MIN(to_cpy, len);
+ memcpy(w_ptr, data, to_cpy);
+ rbuf_bump_head(buffer, to_cpy);
+ if(to_cpy < len)
+ to_cpy += rbuf_push(buffer, &data[to_cpy], len - to_cpy);
+ return to_cpy;
+}
+
+size_t rbuf_pop(rbuf_t buffer, char *data, size_t len)
+{
+ size_t to_cpy;
+ const char *r_ptr = rbuf_get_linear_read_range(buffer, &to_cpy);
+ if(!to_cpy)
+ return to_cpy;
+
+ to_cpy = MIN(to_cpy, len);
+ memcpy(data, r_ptr, to_cpy);
+ rbuf_bump_tail(buffer, to_cpy);
+ if(to_cpy < len)
+ to_cpy += rbuf_pop(buffer, &data[to_cpy], len - to_cpy);
+ return to_cpy;
+}
+
+static inline void rbuf_ptr_inc(rbuf_t buffer, const char **ptr)
+{
+ (*ptr)++;
+ if(*ptr >= buffer->end)
+ *ptr = buffer->data;
+}
+
+int rbuf_memcmp(rbuf_t buffer, const char *haystack, const char *needle, size_t needle_bytes)
+{
+ const char *end = needle + needle_bytes;
+
+ // as head==tail can mean 2 things here
+ if (haystack == buffer->head && buffer->size_data) {
+ if (*haystack != *needle)
+ return (*haystack - *needle);
+ rbuf_ptr_inc(buffer, &haystack);
+ needle++;
+ }
+
+ while (haystack != buffer->head && needle != end) {
+ if (*haystack != *needle)
+ return (*haystack - *needle);
+ rbuf_ptr_inc(buffer, &haystack);
+ needle++;
+ }
+ return 0;
+}
+
+int rbuf_memcmp_n(rbuf_t buffer, const char *to_cmp, size_t to_cmp_bytes)
+{
+ return rbuf_memcmp(buffer, buffer->tail, to_cmp, to_cmp_bytes);
+}
+
+char *rbuf_find_bytes(rbuf_t buffer, const char *needle, size_t needle_bytes, int *found_idx)
+{
+ const char *ptr = buffer->tail;
+ *found_idx = 0;
+
+ if (!rbuf_bytes_available(buffer))
+ return NULL;
+
+ if (buffer->head == buffer->tail && buffer->size_data) {
+ if(!rbuf_memcmp(buffer, ptr, needle, needle_bytes))
+ return (char *)ptr;
+ rbuf_ptr_inc(buffer, &ptr);
+ (*found_idx)++;
+ }
+
+ while (ptr != buffer->head)
+ {
+ if(!rbuf_memcmp(buffer, ptr, needle, needle_bytes))
+ return (char *)ptr;
+ rbuf_ptr_inc(buffer, &ptr);
+ (*found_idx)++;
+ }
+ return NULL;
+}
diff --git a/src/aclk/mqtt_websockets/c-rbuf/cringbuffer.h b/src/aclk/mqtt_websockets/c-rbuf/cringbuffer.h
new file mode 100644
index 000000000..eb98035a9
--- /dev/null
+++ b/src/aclk/mqtt_websockets/c-rbuf/cringbuffer.h
@@ -0,0 +1,47 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#ifndef CRINGBUFFER_H
+#define CRINGBUFFER_H
+
+#include <stddef.h>
+
+typedef struct rbuf_t *rbuf_t;
+
+rbuf_t rbuf_create(size_t size);
+void rbuf_free(rbuf_t buffer);
+void rbuf_flush(rbuf_t buffer);
+
+/* /param bytes how much bytes can be copied into pointer returned
+ * /return pointer where data can be copied to or NULL if buffer full
+ */
+char *rbuf_get_linear_insert_range(rbuf_t buffer, size_t *bytes);
+char *rbuf_get_linear_read_range(rbuf_t buffer, size_t *bytes);
+
+int rbuf_bump_head(rbuf_t buffer, size_t bytes);
+int rbuf_bump_tail(rbuf_t buffer, size_t bytes);
+
+/* @param buffer related buffer instance
+ * @returns total capacity of buffer in bytes (not free/used)
+ */
+size_t rbuf_get_capacity(rbuf_t buffer);
+
+/* @param buffer related buffer instance
+ * @returns count of bytes stored in the buffer
+ */
+size_t rbuf_bytes_available(rbuf_t buffer);
+
+/* @param buffer related buffer instance
+ * @returns count of bytes available/free in the buffer (how many more bytes you can store in this buffer)
+ */
+size_t rbuf_bytes_free(rbuf_t buffer);
+
+/* writes as many bytes from `data` into the `buffer` as possible
+ * but maximum `len` bytes
+ */
+size_t rbuf_push(rbuf_t buffer, const char *data, size_t len);
+size_t rbuf_pop(rbuf_t buffer, char *data, size_t len);
+
+char *rbuf_find_bytes(rbuf_t buffer, const char *needle, size_t needle_bytes, int *found_idx);
+int rbuf_memcmp_n(rbuf_t buffer, const char *to_cmp, size_t to_cmp_bytes);
+
+#endif
diff --git a/src/aclk/mqtt_websockets/c-rbuf/cringbuffer_internal.h b/src/aclk/mqtt_websockets/c-rbuf/cringbuffer_internal.h
new file mode 100644
index 000000000..d32de187c
--- /dev/null
+++ b/src/aclk/mqtt_websockets/c-rbuf/cringbuffer_internal.h
@@ -0,0 +1,37 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#ifndef CRINGBUFFER_INTERNAL_H
+#define CRINGBUFFER_INTERNAL_H
+
+struct rbuf_t {
+ char *data;
+
+ // points to next byte where we can write
+ char *head;
+ // points to oldest (next to be poped) readable byte
+ char *tail;
+
+ // to avoid calculating data + size
+ // all the time
+ char *end;
+
+ size_t size;
+ size_t size_data;
+};
+
+/* this exists so that it can be tested by unit tests
+ * without optimization that resets head and tail to
+ * beginning if buffer empty
+ */
+inline static int rbuf_bump_tail_noopt(rbuf_t buffer, size_t bytes)
+{
+ if (bytes > buffer->size_data)
+ return 0;
+ int i = buffer->tail - buffer->data;
+ buffer->tail = &buffer->data[(i + bytes) % buffer->size];
+ buffer->size_data -= bytes;
+
+ return 1;
+}
+
+#endif
diff --git a/src/aclk/mqtt_websockets/c-rbuf/ringbuffer_test.c b/src/aclk/mqtt_websockets/c-rbuf/ringbuffer_test.c
new file mode 100644
index 000000000..6a17c9956
--- /dev/null
+++ b/src/aclk/mqtt_websockets/c-rbuf/ringbuffer_test.c
@@ -0,0 +1,485 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include "ringbuffer.h"
+
+// to be able to access internals
+// never do this from app
+#include "../src/ringbuffer_internal.h"
+
+#include <stdio.h>
+#include <string.h>
+
+#define KNRM "\x1B[0m"
+#define KRED "\x1B[31m"
+#define KGRN "\x1B[32m"
+#define KYEL "\x1B[33m"
+#define KBLU "\x1B[34m"
+#define KMAG "\x1B[35m"
+#define KCYN "\x1B[36m"
+#define KWHT "\x1B[37m"
+
+#define UNUSED(x) (void)(x)
+
+int total_fails = 0;
+int total_tests = 0;
+int total_checks = 0;
+
+#define CHECK_EQ_RESULT(x, y) \
+ while (s_len--) \
+ putchar('.'); \
+ printf("%s%s " KNRM "\n", (((x) == (y)) ? KGRN : KRED), (((x) == (y)) ? " PASS " : " FAIL ")); \
+ if ((x) != (y)) \
+ total_fails++; \
+ total_checks++;
+
+#define CHECK_EQ_PREFIX(x, y, prefix, subtest_name, ...) \
+ { \
+ int s_len = \
+ 100 - \
+ printf(("Checking: " KWHT "%s %s%2d " subtest_name " " KNRM), __func__, prefix, subtest_no, ##__VA_ARGS__); \
+ CHECK_EQ_RESULT(x, y) \
+ }
+
+#define CHECK_EQ(x, y, subtest_name, ...) \
+ { \
+ int s_len = \
+ 100 - printf(("Checking: " KWHT "%s %2d " subtest_name " " KNRM), __func__, subtest_no, ##__VA_ARGS__); \
+ CHECK_EQ_RESULT(x, y) \
+ }
+
+#define TEST_DECL() \
+ int subtest_no = 0; \
+ printf(KYEL "TEST SUITE: %s\n" KNRM, __func__); \
+ total_tests++;
+
+static void test_rbuf_get_linear_insert_range()
+{
+ TEST_DECL();
+
+ // check empty buffer behaviour
+ rbuf_t buff = rbuf_create(5);
+ char *to_write;
+ size_t ret;
+ to_write = rbuf_get_linear_insert_range(buff, &ret);
+ CHECK_EQ(ret, 5, "empty size");
+ CHECK_EQ(to_write, buff->head, "empty write ptr");
+ rbuf_free(buff);
+
+ // check full buffer behaviour
+ subtest_no++;
+ buff = rbuf_create(5);
+ ret = rbuf_bump_head(buff, 5);
+ CHECK_EQ(ret, 1, "ret");
+ to_write = rbuf_get_linear_insert_range(buff, &ret);
+ CHECK_EQ(to_write, NULL, "writable NULL");
+ CHECK_EQ(ret, 0, "writable count = 0");
+
+ // check buffer flush
+ subtest_no++;
+ rbuf_flush(buff);
+ CHECK_EQ(rbuf_bytes_free(buff), 5, "size_free");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check behaviour head > tail
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_bump_head(buff, 3);
+ to_write = rbuf_get_linear_insert_range(buff, &ret);
+ CHECK_EQ(to_write, buff->head, "write location");
+ CHECK_EQ(ret, 2, "availible to linear write");
+
+ // check behaviour tail > head
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_bump_head(buff, 5);
+ rbuf_bump_tail(buff, 3);
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data + 3, "tail_ptr");
+ to_write = rbuf_get_linear_insert_range(buff, &ret);
+ CHECK_EQ(to_write, buff->head, "write location");
+ CHECK_EQ(ret, 3, "availible to linear write");
+
+/* // check behaviour tail and head at last element
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_bump_head(buff, 4);
+ rbuf_bump_tail(buff, 4);
+ CHECK_EQ(buff->head, buff->end - 1, "head_ptr");
+ CHECK_EQ(buff->tail, buff->end - 1, "tail_ptr");
+ to_write = rbuf_get_linear_insert_range(buff, &ret);
+ CHECK_EQ(to_write, buff->head, "write location");
+ CHECK_EQ(ret, 1, "availible to linear write");*/
+
+ // check behaviour tail and head at last element
+ // after rbuf_bump_tail optimisation that restarts buffer
+ // in case tail catches up with head
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_bump_head(buff, 4);
+ rbuf_bump_tail(buff, 4);
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+ to_write = rbuf_get_linear_insert_range(buff, &ret);
+ CHECK_EQ(to_write, buff->head, "write location");
+ CHECK_EQ(ret, 5, "availible to linear write");
+}
+
+#define _CHECK_EQ(x, y, subtest_name, ...) CHECK_EQ_PREFIX(x, y, prefix, subtest_name, ##__VA_ARGS__)
+#define _PREFX "(size = %5zu) "
+static void test_rbuf_bump_head_bsize(size_t size)
+{
+ char prefix[16];
+ snprintf(prefix, 16, _PREFX, size);
+ int subtest_no = 0;
+ rbuf_t buff = rbuf_create(size);
+ _CHECK_EQ(rbuf_bytes_free(buff), size, "size_free");
+
+ subtest_no++;
+ int ret = rbuf_bump_head(buff, size);
+ _CHECK_EQ(buff->data, buff->head, "loc");
+ _CHECK_EQ(ret, 1, "ret");
+ _CHECK_EQ(buff->size_data, buff->size, "size");
+ _CHECK_EQ(rbuf_bytes_free(buff), 0, "size_free");
+
+ subtest_no++;
+ ret = rbuf_bump_head(buff, 1);
+ _CHECK_EQ(buff->data, buff->head, "loc no move");
+ _CHECK_EQ(ret, 0, "ret error");
+ _CHECK_EQ(buff->size_data, buff->size, "size");
+ _CHECK_EQ(rbuf_bytes_free(buff), 0, "size_free");
+ rbuf_free(buff);
+
+ subtest_no++;
+ buff = rbuf_create(size);
+ ret = rbuf_bump_head(buff, size - 1);
+ _CHECK_EQ(buff->head, buff->end-1, "loc end");
+ rbuf_free(buff);
+}
+#undef _CHECK_EQ
+
+static void test_rbuf_bump_head()
+{
+ TEST_DECL();
+ UNUSED(subtest_no);
+
+ size_t test_sizes[] = { 1, 2, 3, 5, 6, 7, 8, 100, 99999, 0 };
+ for (int i = 0; test_sizes[i]; i++)
+ test_rbuf_bump_head_bsize(test_sizes[i]);
+}
+
+static void test_rbuf_bump_tail_noopt(int subtest_no)
+{
+ rbuf_t buff = rbuf_create(10);
+ CHECK_EQ(rbuf_bytes_free(buff), 10, "size_free");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+
+ subtest_no++;
+ int ret = rbuf_bump_head(buff, 5);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_free(buff), 5, "size_free");
+ CHECK_EQ(rbuf_bytes_available(buff), 5, "size_avail");
+ CHECK_EQ(buff->head, buff->data + 5, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_tail_noopt(buff, 2);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 3, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 7, "size_free");
+ CHECK_EQ(buff->head, buff->data + 5, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data + 2, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_tail_noopt(buff, 3);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 10, "size_free");
+ CHECK_EQ(buff->head, buff->data + 5, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data + 5, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_tail_noopt(buff, 1);
+ CHECK_EQ(ret, 0, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 10, "size_free");
+ CHECK_EQ(buff->head, buff->data + 5, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data + 5, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_head(buff, 7);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 7, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 3, "size_free");
+ CHECK_EQ(buff->head, buff->data + 2, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data + 5, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_tail_noopt(buff, 5);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 2, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 8, "size_free");
+ CHECK_EQ(buff->head, buff->data + 2, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check tail can't overrun head
+ subtest_no++;
+ ret = rbuf_bump_tail_noopt(buff, 3);
+ CHECK_EQ(ret, 0, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 2, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 8, "size_free");
+ CHECK_EQ(buff->head, buff->data + 2, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check head can't overrun tail
+ subtest_no++;
+ ret = rbuf_bump_head(buff, 9);
+ CHECK_EQ(ret, 0, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 2, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 8, "size_free");
+ CHECK_EQ(buff->head, buff->data + 2, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check head can fill the buffer
+ subtest_no++;
+ ret = rbuf_bump_head(buff, 8);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 10, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 0, "size_free");
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check can empty the buffer
+ subtest_no++;
+ ret = rbuf_bump_tail_noopt(buff, 10);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 10, "size_free");
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+}
+
+static void test_rbuf_bump_tail_opt(int subtest_no)
+{
+ subtest_no++;
+ rbuf_t buff = rbuf_create(10);
+ CHECK_EQ(rbuf_bytes_free(buff), 10, "size_free");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+
+ subtest_no++;
+ int ret = rbuf_bump_head(buff, 5);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_free(buff), 5, "size_free");
+ CHECK_EQ(rbuf_bytes_available(buff), 5, "size_avail");
+ CHECK_EQ(buff->head, buff->data + 5, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_tail(buff, 2);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 3, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 7, "size_free");
+ CHECK_EQ(buff->head, buff->data + 5, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data + 2, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_tail(buff, 3);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 10, "size_free");
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_tail_noopt(buff, 1);
+ CHECK_EQ(ret, 0, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 10, "size_free");
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_head(buff, 6);
+ ret = rbuf_bump_tail(buff, 5);
+ ret = rbuf_bump_head(buff, 6);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 7, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 3, "size_free");
+ CHECK_EQ(buff->head, buff->data + 2, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data + 5, "tail_ptr");
+
+ subtest_no++;
+ ret = rbuf_bump_tail(buff, 5);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 2, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 8, "size_free");
+ CHECK_EQ(buff->head, buff->data + 2, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check tail can't overrun head
+ subtest_no++;
+ ret = rbuf_bump_tail(buff, 3);
+ CHECK_EQ(ret, 0, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 2, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 8, "size_free");
+ CHECK_EQ(buff->head, buff->data + 2, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check head can't overrun tail
+ subtest_no++;
+ ret = rbuf_bump_head(buff, 9);
+ CHECK_EQ(ret, 0, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 2, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 8, "size_free");
+ CHECK_EQ(buff->head, buff->data + 2, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check head can fill the buffer
+ subtest_no++;
+ ret = rbuf_bump_head(buff, 8);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 10, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 0, "size_free");
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+
+ // check can empty the buffer
+ subtest_no++;
+ ret = rbuf_bump_tail(buff, 10);
+ CHECK_EQ(ret, 1, "ret");
+ CHECK_EQ(rbuf_bytes_available(buff), 0, "size_avail");
+ CHECK_EQ(rbuf_bytes_free(buff), 10, "size_free");
+ CHECK_EQ(buff->head, buff->data, "head_ptr");
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+}
+
+static void test_rbuf_bump_tail()
+{
+ TEST_DECL();
+ test_rbuf_bump_tail_noopt(subtest_no);
+ test_rbuf_bump_tail_opt(subtest_no);
+}
+
+#define ASCII_A 0x61
+#define ASCII_Z 0x7A
+#define TEST_DATA_SIZE ASCII_Z-ASCII_A+1
+static void test_rbuf_push()
+{
+ TEST_DECL();
+ rbuf_t buff = rbuf_create(10);
+ int i;
+ char test_data[TEST_DATA_SIZE];
+
+ for (int i = 0; i <= TEST_DATA_SIZE; i++)
+ test_data[i] = i + ASCII_A;
+
+ int ret = rbuf_push(buff, test_data, 10);
+ CHECK_EQ(ret, 10, "written 10 bytes");
+ CHECK_EQ(rbuf_bytes_free(buff), 0, "empty size == 0");
+ for (i = 0; i < 10; i++)
+ CHECK_EQ(buff->data[i], i + ASCII_A, "Check data");
+
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_bump_head(buff, 5);
+ rbuf_bump_tail_noopt(buff, 5); //to not reset both pointers to beginning
+ ret = rbuf_push(buff, test_data, 10);
+ CHECK_EQ(ret, 10, "written 10 bytes");
+ for (i = 0; i < 10; i++)
+ CHECK_EQ(buff->data[i], ((i+5)%10) + ASCII_A, "Check Data");
+
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_bump_head(buff, 9);
+ rbuf_bump_tail_noopt(buff, 9);
+ ret = rbuf_push(buff, test_data, 10);
+ CHECK_EQ(ret, 10, "written 10 bytes");
+ for (i = 0; i < 10; i++)
+ CHECK_EQ(buff->data[i], ((i + 1) % 10) + ASCII_A, "Check data");
+
+ // let tail > head
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_bump_head(buff, 9);
+ rbuf_bump_tail_noopt(buff, 9);
+ rbuf_bump_head(buff, 1);
+ ret = rbuf_push(buff, test_data, 9);
+ CHECK_EQ(ret, 9, "written 9 bytes");
+ CHECK_EQ(buff->head, buff->end - 1, "head_ptr");
+ CHECK_EQ(buff->tail, buff->head, "tail_ptr");
+ rbuf_bump_tail(buff, 1);
+ //TODO push byte can be usefull optimisation
+ ret = rbuf_push(buff, &test_data[9], 1);
+ CHECK_EQ(ret, 1, "written 1 byte");
+ CHECK_EQ(rbuf_bytes_free(buff), 0, "empty size == 0");
+ for (i = 0; i < 10; i++)
+ CHECK_EQ(buff->data[i], i + ASCII_A, "Check data");
+
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_bump_head(buff, 9);
+ rbuf_bump_tail_noopt(buff, 7);
+ rbuf_bump_head(buff, 1);
+ ret = rbuf_push(buff, test_data, 7);
+ CHECK_EQ(ret, 7, "written 7 bytes");
+ CHECK_EQ(buff->head, buff->data + 7, "head_ptr");
+ CHECK_EQ(buff->tail, buff->head, "tail_ptr");
+ rbuf_bump_tail(buff, 3);
+ CHECK_EQ(buff->tail, buff->data, "tail_ptr");
+ //TODO push byte can be usefull optimisation
+ ret = rbuf_push(buff, &test_data[7], 3);
+ CHECK_EQ(ret, 3, "written 3 bytes");
+ CHECK_EQ(rbuf_bytes_free(buff), 0, "empty size == 0");
+ for (i = 0; i < 10; i++)
+ CHECK_EQ(buff->data[i], i + ASCII_A, "Check data");
+
+ // test can't overfill the buffer
+ subtest_no++;
+ rbuf_flush(buff);
+ rbuf_push(buff, test_data, TEST_DATA_SIZE);
+ CHECK_EQ(ret, 3, "written 10 bytes");
+ for (i = 0; i < 10; i++)
+ CHECK_EQ(buff->data[i], i + ASCII_A, "Check data");
+}
+
+#define TEST_RBUF_FIND_BYTES_SIZE 10
+void test_rbuf_find_bytes()
+{
+ TEST_DECL();
+ rbuf_t buff = rbuf_create(TEST_RBUF_FIND_BYTES_SIZE);
+ char *filler_3 = " ";
+ char *needle = "needle";
+ int idx;
+ char *ptr;
+
+ // make sure needle is wrapped aroung in the buffer
+ // to test we still can find it
+ // target "edle ne"
+ rbuf_bump_head(buff, TEST_RBUF_FIND_BYTES_SIZE / 2);
+ rbuf_push(buff, filler_3, strlen(filler_3));
+ rbuf_bump_tail(buff, TEST_RBUF_FIND_BYTES_SIZE / 2);
+ rbuf_push(buff, needle, strlen(needle));
+ ptr = rbuf_find_bytes(buff, needle, strlen(needle), &idx);
+ CHECK_EQ(ptr, buff->data + (TEST_RBUF_FIND_BYTES_SIZE / 2) + strlen(filler_3), "Pointer to needle correct");
+ CHECK_EQ(idx, ptr - buff->tail, "Check needle index");
+}
+
+int main()
+{
+ test_rbuf_bump_head();
+ test_rbuf_bump_tail();
+ test_rbuf_get_linear_insert_range();
+ test_rbuf_push();
+ test_rbuf_find_bytes();
+
+ printf(
+ KNRM "Total Tests %d, Total Checks %d, Successful Checks %d, Failed Checks %d\n",
+ total_tests, total_checks, total_checks - total_fails, total_fails);
+ if (total_fails)
+ printf(KRED "!!!Some test(s) Failed!!!\n");
+ else
+ printf(KGRN "ALL TESTS PASSED\n");
+
+ return total_fails;
+}
diff --git a/src/aclk/mqtt_websockets/c_rhash/c_rhash.c b/src/aclk/mqtt_websockets/c_rhash/c_rhash.c
new file mode 100644
index 000000000..a71b500e2
--- /dev/null
+++ b/src/aclk/mqtt_websockets/c_rhash/c_rhash.c
@@ -0,0 +1,264 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include "c_rhash_internal.h"
+
+#include <stdlib.h>
+#include <string.h>
+
+#ifdef DEBUG_VERBOSE
+#include <stdio.h>
+#endif
+
+#define c_rmalloc(...) malloc(__VA_ARGS__)
+#define c_rcalloc(...) calloc(__VA_ARGS__)
+#define c_rfree(...) free(__VA_ARGS__)
+
+static inline uint32_t simple_hash(const char *name) {
+ unsigned char *s = (unsigned char *) name;
+ uint32_t hval = 0x811c9dc5;
+ while (*s) {
+ hval *= 16777619;
+ hval ^= (uint32_t) *s++;
+ }
+ return hval;
+}
+
+c_rhash c_rhash_new(size_t bin_count) {
+ if (!bin_count)
+ bin_count = 1000;
+
+ c_rhash hash = c_rcalloc(1, sizeof(struct c_rhash_s) + (bin_count * sizeof(struct bin_ll*)) );
+ if (hash == NULL)
+ return NULL;
+
+ hash->bin_count = bin_count;
+ hash->bins = (c_rhash_bin *)((char*)hash + sizeof(struct c_rhash_s));
+
+ return hash;
+}
+
+static size_t get_itemtype_len(uint8_t item_type, const void* item_data) {
+ switch (item_type) {
+ case ITEMTYPE_STRING:
+ return strlen(item_data) + 1;
+ case ITEMTYPE_UINT64:
+ return sizeof(uint64_t);
+ case ITEMTYPE_UINT8:
+ return 1;
+ case ITEMTYPE_OPAQUE_PTR:
+ return sizeof(void*);
+ default:
+ return 0;
+ }
+}
+
+static int compare_bin_item(struct bin_item *item, uint8_t key_type, const void *key) {
+ if (item->key_type != key_type)
+ return 1;
+
+ size_t key_value_len = get_itemtype_len(key_type, key);
+
+ if(key_type == ITEMTYPE_STRING) {
+ size_t new_key_value_len = get_itemtype_len(item->key_type, item->key);
+ if (new_key_value_len != key_value_len)
+ return 1;
+ }
+
+ if(memcmp(item->key, key, key_value_len) == 0) {
+ return 0;
+ }
+
+ return 1;
+}
+
+static int insert_into_bin(c_rhash_bin *bin, uint8_t key_type, const void *key, uint8_t value_type, const void *value) {
+ struct bin_item *prev = NULL;
+ while (*bin != NULL) {
+ if (!compare_bin_item(*bin, key_type, key)) {
+#ifdef DEBUG_VERBOSE
+ printf("Key already present! Updating value!\n");
+#endif
+// TODO: optimize here if the new value is of different kind compared to the old one
+// in case it is not crazily bigger we can reuse the memory and avoid malloc and free
+ c_rfree((*bin)->value);
+ (*bin)->value_type = value_type;
+ (*bin)->value = c_rmalloc(get_itemtype_len(value_type, value));
+ if ((*bin)->value == NULL)
+ return 1;
+ memcpy((*bin)->value, value, get_itemtype_len(value_type, value));
+ return 0;
+ }
+ prev = *bin;
+ bin = &(*bin)->next;
+ }
+
+ if (*bin == NULL)
+ *bin = c_rcalloc(1, sizeof(struct bin_item));
+ if (prev != NULL)
+ prev->next = *bin;
+
+ (*bin)->key_type = key_type;
+ size_t len = get_itemtype_len(key_type, key);
+ (*bin)->key = c_rmalloc(len);
+ memcpy((*bin)->key, key, len);
+
+ (*bin)->value_type = value_type;
+ len = get_itemtype_len(value_type, value);
+ (*bin)->value = c_rmalloc(len);
+ memcpy((*bin)->value, value, len);
+ return 0;
+}
+
+static inline uint32_t get_bin_idx_str(c_rhash hash, const char *key) {
+ uint32_t nhash = simple_hash(key);
+ return nhash % hash->bin_count;
+}
+
+static inline c_rhash_bin *get_binptr_by_str(c_rhash hash, const char *key) {
+ return &hash->bins[get_bin_idx_str(hash, key)];
+}
+
+int c_rhash_insert_str_ptr(c_rhash hash, const char *key, void *value) {
+ c_rhash_bin *bin = get_binptr_by_str(hash, key);
+
+#ifdef DEBUG_VERBOSE
+ if (bin != NULL)
+ printf("COLLISION. There will be more than one item in bin idx=%d\n", nhash);
+#endif
+
+ return insert_into_bin(bin, ITEMTYPE_STRING, key, ITEMTYPE_OPAQUE_PTR, &value);
+}
+
+int c_rhash_insert_str_uint8(c_rhash hash, const char *key, uint8_t value) {
+ c_rhash_bin *bin = get_binptr_by_str(hash, key);
+
+#ifdef DEBUG_VERBOSE
+ if (bin != NULL)
+ printf("COLLISION. There will be more than one item in bin idx=%d\n", nhash);
+#endif
+
+ return insert_into_bin(bin, ITEMTYPE_STRING, key, ITEMTYPE_UINT8, &value);
+}
+
+int c_rhash_insert_uint64_ptr(c_rhash hash, uint64_t key, void *value) {
+ c_rhash_bin *bin = &hash->bins[key % hash->bin_count];
+
+#ifdef DEBUG_VERBOSE
+ if (bin != NULL)
+ printf("COLLISION. There will be more than one item in bin idx=%d\n", nhash);
+#endif
+
+ return insert_into_bin(bin, ITEMTYPE_UINT64, &key, ITEMTYPE_OPAQUE_PTR, &value);
+}
+
+int c_rhash_get_uint8_by_str(c_rhash hash, const char *key, uint8_t *ret_val) {
+ uint32_t nhash = get_bin_idx_str(hash, key);
+
+ struct bin_item *bin = hash->bins[nhash];
+
+ while (bin) {
+ if (bin->key_type == ITEMTYPE_STRING) {
+ if (!strcmp(bin->key, key)) {
+ *ret_val = *(uint8_t*)bin->value;
+ return 0;
+ }
+ }
+ bin = bin->next;
+ }
+ return 1;
+}
+
+int c_rhash_get_ptr_by_str(c_rhash hash, const char *key, void **ret_val) {
+ uint32_t nhash = get_bin_idx_str(hash, key);
+
+ struct bin_item *bin = hash->bins[nhash];
+
+ while (bin) {
+ if (bin->key_type == ITEMTYPE_STRING) {
+ if (!strcmp(bin->key, key)) {
+ *ret_val = *((void**)bin->value);
+ return 0;
+ }
+ }
+ bin = bin->next;
+ }
+ *ret_val = NULL;
+ return 1;
+}
+
+int c_rhash_get_ptr_by_uint64(c_rhash hash, uint64_t key, void **ret_val) {
+ uint32_t nhash = key % hash->bin_count;
+
+ struct bin_item *bin = hash->bins[nhash];
+
+ while (bin) {
+ if (bin->key_type == ITEMTYPE_UINT64) {
+ if (*((uint64_t *)bin->key) == key) {
+ *ret_val = *((void**)bin->value);
+ return 0;
+ }
+ }
+ bin = bin->next;
+ }
+ *ret_val = NULL;
+ return 1;
+}
+
+static void c_rhash_destroy_bin(c_rhash_bin bin) {
+ struct bin_item *next;
+ do {
+ next = bin->next;
+ c_rfree(bin->key);
+ c_rfree(bin->value);
+ c_rfree(bin);
+ bin = next;
+ } while (bin != NULL);
+}
+
+int c_rhash_iter_uint64_keys(c_rhash hash, c_rhash_iter_t *iter, uint64_t *key) {
+ while (iter->bin < hash->bin_count) {
+ if (iter->item != NULL)
+ iter->item = iter->item->next;
+ if (iter->item == NULL) {
+ if (iter->initialized)
+ iter->bin++;
+ else
+ iter->initialized = 1;
+ if (iter->bin < hash->bin_count)
+ iter->item = hash->bins[iter->bin];
+ }
+ if (iter->item != NULL && iter->item->key_type == ITEMTYPE_UINT64) {
+ *key = *(uint64_t*)iter->item->key;
+ return 0;
+ }
+ }
+ return 1;
+}
+
+int c_rhash_iter_str_keys(c_rhash hash, c_rhash_iter_t *iter, const char **key) {
+ while (iter->bin < hash->bin_count) {
+ if (iter->item != NULL)
+ iter->item = iter->item->next;
+ if (iter->item == NULL) {
+ if (iter->initialized)
+ iter->bin++;
+ else
+ iter->initialized = 1;
+ if (iter->bin < hash->bin_count)
+ iter->item = hash->bins[iter->bin];
+ }
+ if (iter->item != NULL && iter->item->key_type == ITEMTYPE_STRING) {
+ *key = (const char*)iter->item->key;
+ return 0;
+ }
+ }
+ return 1;
+}
+
+void c_rhash_destroy(c_rhash hash) {
+ for (size_t i = 0; i < hash->bin_count; i++) {
+ if (hash->bins[i] != NULL)
+ c_rhash_destroy_bin(hash->bins[i]);
+ }
+ c_rfree(hash);
+}
diff --git a/src/aclk/mqtt_websockets/c_rhash/c_rhash.h b/src/aclk/mqtt_websockets/c_rhash/c_rhash.h
new file mode 100644
index 000000000..37addd161
--- /dev/null
+++ b/src/aclk/mqtt_websockets/c_rhash/c_rhash.h
@@ -0,0 +1,61 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include <sys/types.h>
+#include <stdint.h>
+#include <stddef.h>
+
+#ifndef DEFAULT_BIN_COUNT
+ #define DEFAULT_BIN_COUNT 1000
+#endif
+
+#define ITEMTYPE_UNSET (0x0)
+#define ITEMTYPE_STRING (0x1)
+#define ITEMTYPE_UINT8 (0x2)
+#define ITEMTYPE_UINT64 (0x3)
+#define ITEMTYPE_OPAQUE_PTR (0x4)
+
+typedef struct c_rhash_s *c_rhash;
+
+c_rhash c_rhash_new(size_t bin_count);
+
+void c_rhash_destroy(c_rhash hash);
+
+// # Insert
+// ## Insert where key is string
+int c_rhash_insert_str_ptr(c_rhash hash, const char *key, void *value);
+int c_rhash_insert_str_uint8(c_rhash hash, const char *key, uint8_t value);
+// ## Insert where key is uint64
+int c_rhash_insert_uint64_ptr(c_rhash hash, uint64_t key, void *value);
+
+// # Get
+// ## Get where key is string
+int c_rhash_get_ptr_by_str(c_rhash hash, const char *key, void **ret_val);
+int c_rhash_get_uint8_by_str(c_rhash hash, const char *key, uint8_t *ret_val);
+// ## Get where key is uint64
+int c_rhash_get_ptr_by_uint64(c_rhash hash, uint64_t key, void **ret_val);
+
+typedef struct {
+ size_t bin;
+ struct bin_item *item;
+ int initialized;
+} c_rhash_iter_t;
+
+#define C_RHASH_ITER_T_INITIALIZER { .bin = 0, .item = NULL, .initialized = 0 }
+
+#define c_rhash_iter_t_initialize(p_iter) memset(p_iter, 0, sizeof(c_rhash_iter_t))
+
+/*
+ * goes trough whole hash map and returns every
+ * type uint64 key present/stored
+ *
+ * it is not necessary to finish iterating and iterator can be reinitialized
+ * there are no guarantees on the order in which the keys will come
+ * behavior here is implementation dependent and can change any time
+ *
+ * returns:
+ * 0 for every key and stores the key in *key
+ * 1 on error or when all keys of this type has been already iterated over
+ */
+int c_rhash_iter_uint64_keys(c_rhash hash, c_rhash_iter_t *iter, uint64_t *key);
+
+int c_rhash_iter_str_keys(c_rhash hash, c_rhash_iter_t *iter, const char **key);
diff --git a/src/aclk/mqtt_websockets/c_rhash/c_rhash_internal.h b/src/aclk/mqtt_websockets/c_rhash/c_rhash_internal.h
new file mode 100644
index 000000000..20f741076
--- /dev/null
+++ b/src/aclk/mqtt_websockets/c_rhash/c_rhash_internal.h
@@ -0,0 +1,19 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include "c_rhash.h"
+
+struct bin_item {
+ uint8_t key_type:4;
+ void *key;
+ uint8_t value_type:4;
+ void *value;
+
+ struct bin_item *next;
+};
+
+typedef struct bin_item *c_rhash_bin;
+
+struct c_rhash_s {
+ size_t bin_count;
+ c_rhash_bin *bins;
+};
diff --git a/src/aclk/mqtt_websockets/c_rhash/tests.c b/src/aclk/mqtt_websockets/c_rhash/tests.c
new file mode 100644
index 000000000..909c5562d
--- /dev/null
+++ b/src/aclk/mqtt_websockets/c_rhash/tests.c
@@ -0,0 +1,273 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include <stdio.h>
+#include <string.h>
+
+#include "c_rhash.h"
+
+// terminal color codes
+#define KNRM "\x1B[0m"
+#define KRED "\x1B[31m"
+#define KGRN "\x1B[32m"
+#define KYEL "\x1B[33m"
+#define KBLU "\x1B[34m"
+#define KMAG "\x1B[35m"
+#define KCYN "\x1B[36m"
+#define KWHT "\x1B[37m"
+
+#define KEY_1 "key1"
+#define KEY_2 "keya"
+
+#define PRINT_ERR(str, ...) fprintf(stderr, "└─╼ ❌ " KRED str KNRM "\n" __VA_OPT__(,) __VA_ARGS__)
+
+#define ASSERT_RETVAL(fnc, comparator, expected_retval, ...) \
+{ int rval; \
+if(!((rval = fnc(__VA_ARGS__)) comparator expected_retval)) { \
+ PRINT_ERR("Failed test. Value returned by \"%s\" in fnc:\"%s\",line:%d is not equal to expected value. Expected:%d, Got:%d", #fnc, __FUNCTION__, __LINE__, expected_retval, rval); \
+ rc = 1; \
+ goto test_cleanup; \
+} passed_subtest_count++;};
+
+#define ASSERT_VAL_UINT8(returned, expected) \
+if(returned != expected) { \
+ PRINT_ERR("Failed test. Value returned (%d) doesn't match expected (%d)! fnc:\"%s\",line:%d", returned, expected, __FUNCTION__, __LINE__); \
+ rc = 1; \
+ goto test_cleanup; \
+} passed_subtest_count++;
+
+#define ASSERT_VAL_PTR(returned, expected) \
+if((void*)returned != (void*)expected) { \
+ PRINT_ERR("Failed test. Value returned(%p) doesn't match expected(%p)! fnc:\"%s\",line:%d", (void*)returned, (void*)expected, __FUNCTION__, __LINE__); \
+ rc = 1; \
+ goto test_cleanup; \
+} passed_subtest_count++;
+
+#define ALL_SUBTESTS_PASS() printf("└─╼ ✅" KGRN " Test \"%s\" DONE. All of %zu subtests PASS. (line:%d)\n" KNRM, __FUNCTION__, passed_subtest_count, __LINE__);
+
+#define TEST_START() size_t passed_subtest_count = 0; int rc = 0; printf("╒═ Starting test \"%s\"\n", __FUNCTION__);
+
+int test_str_uint8() {
+ c_rhash hash = c_rhash_new(100);
+ uint8_t val;
+
+ TEST_START();
+ // function should fail on empty hash
+ ASSERT_RETVAL(c_rhash_get_uint8_by_str, !=, 0, hash, KEY_1, &val);
+
+ ASSERT_RETVAL(c_rhash_insert_str_uint8, ==, 0, hash, KEY_1, 5);
+ ASSERT_RETVAL(c_rhash_get_uint8_by_str, ==, 0, hash, KEY_1, &val);
+ ASSERT_VAL_UINT8(5, val);
+
+ ASSERT_RETVAL(c_rhash_insert_str_uint8, ==, 0, hash, KEY_2, 8);
+ ASSERT_RETVAL(c_rhash_get_uint8_by_str, ==, 0, hash, KEY_1, &val);
+ ASSERT_VAL_UINT8(5, val);
+ ASSERT_RETVAL(c_rhash_get_uint8_by_str, ==, 0, hash, KEY_2, &val);
+ ASSERT_VAL_UINT8(8, val);
+ ASSERT_RETVAL(c_rhash_get_uint8_by_str, !=, 0, hash, "sndnskjdf", &val);
+
+ // test update of key
+ ASSERT_RETVAL(c_rhash_insert_str_uint8, ==, 0, hash, KEY_1, 100);
+ ASSERT_RETVAL(c_rhash_get_uint8_by_str, ==, 0, hash, KEY_1, &val);
+ ASSERT_VAL_UINT8(100, val);
+
+ ALL_SUBTESTS_PASS();
+test_cleanup:
+ c_rhash_destroy(hash);
+ return rc;
+}
+
+int test_uint64_ptr() {
+ c_rhash hash = c_rhash_new(100);
+ void *val;
+
+ TEST_START();
+
+ // function should fail on empty hash
+ ASSERT_RETVAL(c_rhash_get_ptr_by_uint64, !=, 0, hash, 0, &val);
+
+ ASSERT_RETVAL(c_rhash_insert_uint64_ptr, ==, 0, hash, 0, &hash);
+ ASSERT_RETVAL(c_rhash_get_ptr_by_uint64, ==, 0, hash, 0, &val);
+ ASSERT_VAL_PTR(&hash, val);
+
+ ASSERT_RETVAL(c_rhash_insert_uint64_ptr, ==, 0, hash, 1, &val);
+ ASSERT_RETVAL(c_rhash_get_ptr_by_uint64, ==, 0, hash, 0, &val);
+ ASSERT_VAL_PTR(&hash, val);
+ ASSERT_RETVAL(c_rhash_get_ptr_by_uint64, ==, 0, hash, 1, &val);
+ ASSERT_VAL_PTR(&val, val);
+ ASSERT_RETVAL(c_rhash_get_ptr_by_uint64, !=, 0, hash, 2, &val);
+
+ ALL_SUBTESTS_PASS();
+test_cleanup:
+ c_rhash_destroy(hash);
+ return rc;
+}
+
+#define UINT64_PTR_INC_ITERATION_COUNT 5000
+int test_uint64_ptr_incremental() {
+ c_rhash hash = c_rhash_new(100);
+ void *val;
+
+ TEST_START();
+
+ char a = 0x20;
+ char *ptr = &a;
+ while(ptr < &a + UINT64_PTR_INC_ITERATION_COUNT) {
+ ASSERT_RETVAL(c_rhash_insert_uint64_ptr, ==, 0, hash, (ptr-&a), ptr);
+ ptr++;
+ }
+
+ ptr = &a;
+ char *retptr;
+ for(int i = 0; i < UINT64_PTR_INC_ITERATION_COUNT; i++) {
+ ASSERT_RETVAL(c_rhash_get_ptr_by_uint64, ==, 0, hash, i, (void**)&retptr);
+ ASSERT_VAL_PTR(retptr, (&a+i));
+ }
+
+ ALL_SUBTESTS_PASS();
+test_cleanup:
+ c_rhash_destroy(hash);
+ return rc;
+}
+
+struct test_string {
+ const char *str;
+ int counter;
+};
+
+struct test_string test_strings[] = {
+ { .str = "Cillum reprehenderit eiusmod elit nisi aliquip esse exercitation commodo Lorem voluptate esse.", .counter = 0 },
+ { .str = "Ullamco eiusmod tempor occaecat ad.", .counter = 0 },
+ { .str = "Esse aliquip tempor sint tempor ullamco duis aute incididunt ad.", .counter = 0 },
+ { .str = "Cillum Lorem labore cupidatat commodo proident adipisicing.", .counter = 0 },
+ { .str = "Quis ad cillum officia exercitation.", .counter = 0 },
+ { .str = "Ipsum enim dolor ullamco amet sint nisi ut occaecat sint non.", .counter = 0 },
+ { .str = "Id duis officia ipsum cupidatat velit fugiat.", .counter = 0 },
+ { .str = "Aliqua non occaecat voluptate reprehenderit reprehenderit veniam minim exercitation ea aliquip enim aliqua deserunt qui.", .counter = 0 },
+ { .str = "Ullamco elit tempor laboris reprehenderit quis deserunt duis quis tempor reprehenderit magna dolore reprehenderit exercitation.", .counter = 0 },
+ { .str = "Culpa do dolor quis incididunt et labore in ex.", .counter = 0 },
+ { .str = "Aliquip velit cupidatat qui incididunt ipsum nostrud eiusmod ut proident nisi magna fugiat excepteur.", .counter = 0 },
+ { .str = "Aliqua qui dolore tempor id proident ullamco sunt magna.", .counter = 0 },
+ { .str = "Labore eiusmod ut fugiat dolore reprehenderit mollit magna.", .counter = 0 },
+ { .str = "Veniam aliquip dolor excepteur minim nulla esse cupidatat esse.", .counter = 0 },
+ { .str = "Do quis dolor irure nostrud occaecat aute proident anim.", .counter = 0 },
+ { .str = "Enim veniam non nulla ad quis sit amet.", .counter = 0 },
+ { .str = "Cillum reprehenderit do enim esse do ullamco consectetur ea.", .counter = 0 },
+ { .str = "Sit et duis sint anim qui ad anim labore exercitation sunt cupidatat.", .counter = 0 },
+ { .str = "Dolor officia adipisicing sint pariatur in dolor occaecat officia reprehenderit magna.", .counter = 0 },
+ { .str = "Aliquip dolore qui occaecat eiusmod sunt incididunt reprehenderit minim et.", .counter = 0 },
+ { .str = "Aute fugiat laboris cillum tempor consequat tempor do non laboris culpa officia nisi.", .counter = 0 },
+ { .str = "Et excepteur do aliquip fugiat nisi velit tempor officia enim quis elit incididunt.", .counter = 0 },
+ { .str = "Eu officia adipisicing incididunt occaecat officia cupidatat enim sit sit officia.", .counter = 0 },
+ { .str = "Do amet cillum duis pariatur commodo nulla cillum magna nulla Lorem veniam cupidatat.", .counter = 0 },
+ { .str = "Dolor adipisicing voluptate laboris occaecat culpa aliquip ipsum ut consequat aliqua aliquip commodo sunt velit.", .counter = 0 },
+ { .str = "Nulla proident ipsum quis nulla.", .counter = 0 },
+ { .str = "Laborum adipisicing nulla do aute aliqua est quis sint culpa pariatur laborum voluptate qui.", .counter = 0 },
+ { .str = "Proident eiusmod sunt et nulla elit pariatur dolore irure ex voluptate excepteur adipisicing consectetur.", .counter = 0 },
+ { .str = "Consequat ex voluptate officia excepteur aute deserunt proident commodo et.", .counter = 0 },
+ { .str = "Velit sit cupidatat dolor dolore.", .counter = 0 },
+ { .str = "Sunt enim do non anim nostrud exercitation ullamco ex proident commodo.", .counter = 0 },
+ { .str = "Id ex officia cillum ad.", .counter = 0 },
+ { .str = "Laboris in sunt eiusmod veniam laboris nostrud.", .counter = 0 },
+ { .str = "Ex magna occaecat ea ea incididunt aliquip.", .counter = 0 },
+ { .str = "Sunt eiusmod ex nostrud eu pariatur sit cupidatat ea adipisicing cillum culpa esse consequat aliquip.", .counter = 0 },
+ { .str = "Excepteur commodo qui incididunt enim culpa sunt non excepteur Lorem adipisicing.", .counter = 0 },
+ { .str = "Quis officia est ullamco reprehenderit incididunt occaecat pariatur ex reprehenderit nisi.", .counter = 0 },
+ { .str = "Culpa irure proident proident et eiusmod irure aliqua ipsum cupidatat minim sit.", .counter = 0 },
+ { .str = "Qui cupidatat aliquip est velit magna veniam.", .counter = 0 },
+ { .str = "Pariatur ad ad mollit nostrud non irure minim veniam anim aliquip quis eu.", .counter = 0 },
+ { .str = "Nisi ex minim eu adipisicing tempor Lorem nisi do ad exercitation est non eu.", .counter = 0 },
+ { .str = "Cupidatat do mollit ad commodo cupidatat ut.", .counter = 0 },
+ { .str = "Est non excepteur eiusmod nostrud et eu.", .counter = 0 },
+ { .str = "Cupidatat mollit nisi magna officia ut elit eiusmod.", .counter = 0 },
+ { .str = "Est aliqua consectetur laboris ex consequat est ut dolor.", .counter = 0 },
+ { .str = "Duis eu laboris laborum ut id Lorem nostrud qui ad velit proident fugiat minim ullamco.", .counter = 0 },
+ { .str = "Pariatur esse excepteur anim amet excepteur irure sint quis esse ex cupidatat ut.", .counter = 0 },
+ { .str = "Esse reprehenderit amet qui excepteur aliquip amet.", .counter = 0 },
+ { .str = "Ullamco laboris elit labore adipisicing aute nulla qui laborum tempor officia ut dolor aute.", .counter = 0 },
+ { .str = "Commodo sunt cillum velit minim laborum Lorem aliqua tempor ad id eu.", .counter = 0 },
+ { .str = NULL, .counter = 0 }
+};
+
+uint32_t test_strings_contain_element(const char *str) {
+ struct test_string *str_desc = test_strings;
+ while(str_desc->str) {
+ if (!strcmp(str, str_desc->str))
+ return str_desc - test_strings;
+ str_desc++;
+ }
+ return -1;
+}
+
+#define TEST_INCREMENT_STR_KEYS_HASH_SIZE 20
+int test_increment_str_keys() {
+ c_rhash hash;
+ const char *key;
+
+ TEST_START();
+
+ hash = c_rhash_new(TEST_INCREMENT_STR_KEYS_HASH_SIZE); // less than element count of test_strings
+
+ c_rhash_iter_t iter = C_RHASH_ITER_T_INITIALIZER;
+
+ // check iter on empty hash
+ ASSERT_RETVAL(c_rhash_iter_str_keys, !=, 0, hash, &iter, &key);
+
+ int32_t element_count = 0;
+ while (test_strings[element_count].str) {
+ ASSERT_RETVAL(c_rhash_insert_str_ptr, ==, 0, hash, test_strings[element_count].str, NULL);
+ test_strings[element_count].counter++; // we want to test we got each key exactly once
+ element_count++;
+ }
+
+ if (element_count <= TEST_INCREMENT_STR_KEYS_HASH_SIZE * 2) {
+ // verify we are actually test also iteration trough single bin (when 2 keys have same hash pointing them to same bin)
+ PRINT_ERR("For this test to properly test all the hash size needs to be much smaller than all test key count.");
+ rc = 1;
+ goto test_cleanup;
+ }
+
+ // we insert another type of key as iterator should skip it
+ // in case is another type
+ ASSERT_RETVAL(c_rhash_insert_uint64_ptr, ==, 0, hash, 5, NULL);
+
+ c_rhash_iter_t_initialize(&iter);
+ while(!c_rhash_iter_str_keys(hash, &iter, &key)) {
+ element_count--;
+ int i;
+ if ( (i = test_strings_contain_element(key)) < 0) {
+ PRINT_ERR("Key \"%s\" is not present in test_strings array! (Fnc: %s, Line: %d)", key, __FUNCTION__, __LINE__);
+ rc = 1;
+ goto test_cleanup;
+ }
+ passed_subtest_count++;
+
+ test_strings[i].counter--;
+ }
+ ASSERT_VAL_UINT8(element_count, 0); // we added also same non string keys
+
+ // check each key was present exactly once
+ struct test_string *str_desc = test_strings;
+ while (str_desc->str) {
+ ASSERT_VAL_UINT8(str_desc->counter, 0);
+ str_desc++;
+ }
+
+ ALL_SUBTESTS_PASS();
+test_cleanup:
+ c_rhash_destroy(hash);
+ return rc;
+}
+
+#define RUN_TEST(fnc) \
+if(fnc()) \
+ return 1;
+
+int main(int argc, char *argv[]) {
+ RUN_TEST(test_str_uint8);
+ RUN_TEST(test_uint64_ptr);
+ RUN_TEST(test_uint64_ptr_incremental);
+ RUN_TEST(test_increment_str_keys);
+ // TODO hash with mixed key tests
+ // TODO iterator test
+ return 0;
+}
diff --git a/src/aclk/mqtt_websockets/common_internal.h b/src/aclk/mqtt_websockets/common_internal.h
new file mode 100644
index 000000000..2be1c45b8
--- /dev/null
+++ b/src/aclk/mqtt_websockets/common_internal.h
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-3.0-only
+
+#ifndef COMMON_INTERNAL_H
+#define COMMON_INTERNAL_H
+
+#include "endian_compat.h"
+
+#ifdef MQTT_WSS_CUSTOM_ALLOC
+#include "../helpers/mqtt_wss_pal.h"
+#else
+#define mw_malloc(...) malloc(__VA_ARGS__)
+#define mw_calloc(...) calloc(__VA_ARGS__)
+#define mw_free(...) free(__VA_ARGS__)
+#define mw_strdup(...) strdup(__VA_ARGS__)
+#define mw_realloc(...) realloc(__VA_ARGS__)
+#endif
+
+#ifndef MQTT_WSS_FRAG_MEMALIGN
+#define MQTT_WSS_FRAG_MEMALIGN (8)
+#endif
+
+#define OPENSSL_VERSION_095 0x00905100L
+#define OPENSSL_VERSION_097 0x00907000L
+#define OPENSSL_VERSION_110 0x10100000L
+#define OPENSSL_VERSION_111 0x10101000L
+
+#endif /* COMMON_INTERNAL_H */
diff --git a/src/aclk/mqtt_websockets/common_public.c b/src/aclk/mqtt_websockets/common_public.c
new file mode 100644
index 000000000..7991b0c23
--- /dev/null
+++ b/src/aclk/mqtt_websockets/common_public.c
@@ -0,0 +1,9 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include "common_public.h"
+
+// this dummy exists to have a special pointer with special meaning
+// other than NULL
+void _caller_responsibility(void *ptr) {
+ (void)(ptr);
+}
diff --git a/src/aclk/mqtt_websockets/common_public.h b/src/aclk/mqtt_websockets/common_public.h
new file mode 100644
index 000000000..a855737f9
--- /dev/null
+++ b/src/aclk/mqtt_websockets/common_public.h
@@ -0,0 +1,33 @@
+#ifndef MQTT_WEBSOCKETS_COMMON_PUBLIC_H
+#define MQTT_WEBSOCKETS_COMMON_PUBLIC_H
+
+#include <stddef.h>
+
+/* free_fnc_t in general (in whatever function or struct it is used)
+ * decides how the related data will be handled.
+ * - If NULL the data are copied internally (causing malloc and later free)
+ * - If pointer provided the free function pointed will be called when data are no longer needed
+ * to free associated memory. This is effectively transfering ownership of that pointer to the library.
+ * This also allows caller to provide custom free function other than system one.
+ * - If == CALLER_RESPONSIBILITY the library will not copy the data pointed to and will not call free
+ * at the end. This is usefull to avoid copying memory (and associated malloc/free) when data are for
+ * example static. In this case caller has to guarantee the memory pointed to will be valid for entire duration
+ * it is needed. For example by freeing the data after PUBACK is received or by data being static.
+ */
+typedef void (*free_fnc_t)(void *ptr);
+void _caller_responsibility(void *ptr);
+#define CALLER_RESPONSIBILITY ((free_fnc_t)&_caller_responsibility)
+
+struct mqtt_ng_stats {
+ size_t tx_bytes_queued;
+ int tx_messages_queued;
+ int tx_messages_sent;
+ int rx_messages_rcvd;
+ size_t tx_buffer_used;
+ size_t tx_buffer_free;
+ size_t tx_buffer_size;
+ // part of transaction buffer that containes mesages we can free alredy during the garbage colleciton step
+ size_t tx_buffer_reclaimable;
+};
+
+#endif /* MQTT_WEBSOCKETS_COMMON_PUBLIC_H */
diff --git a/src/aclk/mqtt_websockets/endian_compat.h b/src/aclk/mqtt_websockets/endian_compat.h
new file mode 100644
index 000000000..b36d2c858
--- /dev/null
+++ b/src/aclk/mqtt_websockets/endian_compat.h
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-3.0-only
+
+#ifndef MQTT_WSS_ENDIAN_COMPAT_H
+#define MQTT_WSS_ENDIAN_COMPAT_H
+
+#ifdef __APPLE__
+ #include <libkern/OSByteOrder.h>
+
+ #define htobe16(x) OSSwapHostToBigInt16(x)
+ #define htole16(x) OSSwapHostToLittleInt16(x)
+ #define be16toh(x) OSSwapBigToHostInt16(x)
+ #define le16toh(x) OSSwapLittleToHostInt16(x)
+
+ #define htobe32(x) OSSwapHostToBigInt32(x)
+ #define htole32(x) OSSwapHostToLittleInt32(x)
+ #define be32toh(x) OSSwapBigToHostInt32(x)
+ #define le32toh(x) OSSwapLittleToHostInt32(x)
+
+ #define htobe64(x) OSSwapHostToBigInt64(x)
+ #define htole64(x) OSSwapHostToLittleInt64(x)
+ #define be64toh(x) OSSwapBigToHostInt64(x)
+ #define le64toh(x) OSSwapLittleToHostInt64(x)
+#else
+#ifdef __FreeBSD__
+ #include <sys/endian.h>
+#else
+ #include <endian.h>
+#endif
+#endif
+
+#endif /* MQTT_WSS_ENDIAN_COMPAT_H */
diff --git a/src/aclk/mqtt_websockets/mqtt_constants.h b/src/aclk/mqtt_websockets/mqtt_constants.h
new file mode 100644
index 000000000..3d6a2aa4a
--- /dev/null
+++ b/src/aclk/mqtt_websockets/mqtt_constants.h
@@ -0,0 +1,103 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#ifndef MQTT_CONSTANTS_H
+#define MQTT_CONSTANTS_H
+
+#define MQTT_MAX_QOS 0x02
+
+#define MQTT_VERSION_5_0 0x5
+
+/* [MQTT-1.5.5] most significant bit
+ of MQTT Variable Byte Integer signifies
+ there are more bytes following */
+#define MQTT_VBI_CONTINUATION_FLAG 0x80
+#define MQTT_VBI_DATA_MASK 0x7F
+#define MQTT_VBI_MAXBYTES 4
+
+/* MQTT control packet types as defined in
+ 2.1.2 MQTT Control Packet type */
+#define MQTT_CPT_CONNECT 0x1
+#define MQTT_CPT_CONNACK 0x2
+#define MQTT_CPT_PUBLISH 0x3
+#define MQTT_CPT_PUBACK 0x4
+#define MQTT_CPT_PUBREC 0x5
+#define MQTT_CPT_PUBREL 0x6
+#define MQTT_CPT_PUBCOMP 0x7
+#define MQTT_CPT_SUBSCRIBE 0x8
+#define MQTT_CPT_SUBACK 0x9
+#define MQTT_CPT_UNSUBSCRIBE 0xA
+#define MQTT_CPT_UNSUBACK 0xB
+#define MQTT_CPT_PINGREQ 0xC
+#define MQTT_CPT_PINGRESP 0xD
+#define MQTT_CPT_DISCONNECT 0xE
+#define MQTT_CPT_AUTH 0xF
+
+// MQTT CONNECT FLAGS (spec:3.1.2.3)
+#define MQTT_CONNECT_FLAG_USERNAME 0x80
+#define MQTT_CONNECT_FLAG_PASSWORD 0x40
+#define MQTT_CONNECT_FLAG_LWT_RETAIN 0x20
+#define MQTT_CONNECT_FLAG_LWT 0x04
+#define MQTT_CONNECT_FLAG_CLEAN_START 0x02
+
+#define MQTT_CONNECT_FLAG_QOS_MASK 0x18
+#define MQTT_CONNECT_FLAG_QOS_BITSHIFT 3
+
+#define MQTT_MAX_CLIENT_ID 23 /* [MQTT-3.1.3-5] */
+
+// MQTT Property identifiers [MQTT-2.2.2.2]
+#define MQTT_PROP_PAYLOAD_FMT_INDICATOR 0x01
+#define MQTT_PROP_PAYLOAD_FMT_INDICATOR_NAME "Payload Format Indicator"
+#define MQTT_PROP_MSG_EXPIRY_INTERVAL 0x02
+#define MQTT_PROP_MSG_EXPIRY_INTERVAL_NAME "Message Expiry Interval"
+#define MQTT_PROP_CONTENT_TYPE 0x03
+#define MQTT_PROP_CONTENT_TYPE_NAME "Content Type"
+#define MQTT_PROP_RESPONSE_TOPIC 0x08
+#define MQTT_PROP_RESPONSE_TOPIC_NAME "Response Topic"
+#define MQTT_PROP_CORRELATION_DATA 0x09
+#define MQTT_PROP_CORRELATION_DATA_NAME "Correlation Data"
+#define MQTT_PROP_SUB_IDENTIFIER 0x0B
+#define MQTT_PROP_SUB_IDENTIFIER_NAME "Subscription Identifier"
+#define MQTT_PROP_SESSION_EXPIRY_INTERVAL 0x11
+#define MQTT_PROP_SESSION_EXPIRY_INTERVAL_NAME "Session Expiry Interval"
+#define MQTT_PROP_ASSIGNED_CLIENT_ID 0x12
+#define MQTT_PROP_ASSIGNED_CLIENT_ID_NAME "Assigned Client Identifier"
+#define MQTT_PROP_SERVER_KEEP_ALIVE 0x13
+#define MQTT_PROP_SERVER_KEEP_ALIVE_NAME "Server Keep Alive"
+#define MQTT_PROP_AUTH_METHOD 0x15
+#define MQTT_PROP_AUTH_METHOD_NAME "Authentication Method"
+#define MQTT_PROP_AUTH_DATA 0x16
+#define MQTT_PROP_AUTH_DATA_NAME "Authentication Data"
+#define MQTT_PROP_REQ_PROBLEM_INFO 0x17
+#define MQTT_PROP_REQ_PROBLEM_INFO_NAME "Request Problem Information"
+#define MQTT_PROP_WILL_DELAY_INTERVAL 0x18
+#define MQTT_PROP_WIIL_DELAY_INTERVAL_NAME "Will Delay Interval"
+#define MQTT_PROP_REQ_RESP_INFORMATION 0x19
+#define MQTT_PROP_REQ_RESP_INFORMATION_NAME "Request Response Information"
+#define MQTT_PROP_RESP_INFORMATION 0x1A
+#define MQTT_PROP_RESP_INFORMATION_NAME "Response Information"
+#define MQTT_PROP_SERVER_REF 0x1C
+#define MQTT_PROP_SERVER_REF_NAME "Server Reference"
+#define MQTT_PROP_REASON_STR 0x1F
+#define MQTT_PROP_REASON_STR_NAME "Reason String"
+#define MQTT_PROP_RECEIVE_MAX 0x21
+#define MQTT_PROP_RECEIVE_MAX_NAME "Receive Maximum"
+#define MQTT_PROP_TOPIC_ALIAS_MAX 0x22
+#define MQTT_PROP_TOPIC_ALIAS_MAX_NAME "Topic Alias Maximum"
+#define MQTT_PROP_TOPIC_ALIAS 0x23
+#define MQTT_PROP_TOPIC_ALIAS_NAME "Topic Alias"
+#define MQTT_PROP_MAX_QOS 0x24
+#define MQTT_PROP_MAX_QOS_NAME "Maximum QoS"
+#define MQTT_PROP_RETAIN_AVAIL 0x25
+#define MQTT_PROP_RETAIN_AVAIL_NAME "Retain Available"
+#define MQTT_PROP_USR 0x26
+#define MQTT_PROP_USR_NAME "User Property"
+#define MQTT_PROP_MAX_PKT_SIZE 0x27
+#define MQTT_PROP_MAX_PKT_SIZE_NAME "Maximum Packet Size"
+#define MQTT_PROP_WILDCARD_SUB_AVAIL 0x28
+#define MQTT_PROP_WILDCARD_SUB_AVAIL_NAME "Wildcard Subscription Available"
+#define MQTT_PROP_SUB_ID_AVAIL 0x29
+#define MQTT_PROP_SUB_ID_AVAIL_NAME "Subscription Identifier Available"
+#define MQTT_PROP_SHARED_SUB_AVAIL 0x2A
+#define MQTT_PROP_SHARED_SUB_AVAIL_NAME "Shared Subscription Available"
+
+#endif /* MQTT_CONSTANTS_H */
diff --git a/src/aclk/mqtt_websockets/mqtt_ng.c b/src/aclk/mqtt_websockets/mqtt_ng.c
new file mode 100644
index 000000000..f570fde71
--- /dev/null
+++ b/src/aclk/mqtt_websockets/mqtt_ng.c
@@ -0,0 +1,2237 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+#include <pthread.h>
+#include <inttypes.h>
+
+#include "c_rhash/c_rhash.h"
+
+#include "common_internal.h"
+#include "mqtt_constants.h"
+#include "mqtt_wss_log.h"
+#include "mqtt_ng.h"
+
+#define UNIT_LOG_PREFIX "mqtt_client: "
+#define FATAL(fmt, ...) mws_fatal(client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+#define ERROR(fmt, ...) mws_error(client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+#define WARN(fmt, ...) mws_warn (client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+#define INFO(fmt, ...) mws_info (client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+#define DEBUG(fmt, ...) mws_debug(client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+
+#define SMALL_STRING_DONT_FRAGMENT_LIMIT 128
+
+#define MIN(a,b) (((a)<(b))?(a):(b))
+
+#define LOCK_HDR_BUFFER(buffer) pthread_mutex_lock(&((buffer)->mutex))
+#define UNLOCK_HDR_BUFFER(buffer) pthread_mutex_unlock(&((buffer)->mutex))
+
+#define BUFFER_FRAG_GARBAGE_COLLECT 0x01
+// some packets can be marked for garbage collection
+// immediately when they are sent (e.g. sent PUBACK on QoS1)
+#define BUFFER_FRAG_GARBAGE_COLLECT_ON_SEND 0x02
+// as buffer fragment can point to both
+// external data and data in the same buffer
+// we mark the former case with BUFFER_FRAG_DATA_EXTERNAL
+#define BUFFER_FRAG_DATA_EXTERNAL 0x04
+// as single MQTT Packet can be stored into multiple
+// buffer fragments (depending on copy requirements)
+// this marks this fragment to be the first/last
+#define BUFFER_FRAG_MQTT_PACKET_HEAD 0x10
+#define BUFFER_FRAG_MQTT_PACKET_TAIL 0x20
+
+typedef uint16_t buffer_frag_flag_t;
+struct buffer_fragment {
+ size_t len;
+ size_t sent;
+ buffer_frag_flag_t flags;
+ void (*free_fnc)(void *ptr);
+ unsigned char *data;
+
+ uint16_t packet_id;
+
+ struct buffer_fragment *next;
+};
+
+typedef struct buffer_fragment *mqtt_msg_data;
+
+// buffer used for MQTT headers only
+// not for actual data sent
+struct header_buffer {
+ size_t size;
+ unsigned char *data;
+ unsigned char *tail;
+ struct buffer_fragment *tail_frag;
+};
+
+struct transaction_buffer {
+ struct header_buffer hdr_buffer;
+ // used while building new message
+ // to be able to revert state easily
+ // in case of error mid processing
+ struct header_buffer state_backup;
+ pthread_mutex_t mutex;
+ struct buffer_fragment *sending_frag;
+};
+
+enum mqtt_client_state {
+ RAW = 0,
+ CONNECT_PENDING,
+ CONNECTING,
+ CONNECTED,
+ ERROR,
+ DISCONNECTED
+};
+
+enum parser_state {
+ MQTT_PARSE_FIXED_HEADER_PACKET_TYPE = 0,
+ MQTT_PARSE_FIXED_HEADER_LEN,
+ MQTT_PARSE_VARIABLE_HEADER,
+ MQTT_PARSE_MQTT_PACKET_DONE
+};
+
+enum varhdr_parser_state {
+ MQTT_PARSE_VARHDR_INITIAL = 0,
+ MQTT_PARSE_VARHDR_OPTIONAL_REASON_CODE,
+ MQTT_PARSE_VARHDR_PROPS,
+ MQTT_PARSE_VARHDR_TOPICNAME,
+ MQTT_PARSE_VARHDR_POST_TOPICNAME,
+ MQTT_PARSE_VARHDR_PACKET_ID,
+ MQTT_PARSE_REASONCODES,
+ MQTT_PARSE_PAYLOAD
+};
+
+struct mqtt_vbi_parser_ctx {
+ char data[MQTT_VBI_MAXBYTES];
+ uint8_t bytes;
+ uint32_t result;
+};
+
+enum mqtt_datatype {
+ MQTT_TYPE_UNKNOWN = 0,
+ MQTT_TYPE_UINT_8,
+ MQTT_TYPE_UINT_16,
+ MQTT_TYPE_UINT_32,
+ MQTT_TYPE_VBI,
+ MQTT_TYPE_STR,
+ MQTT_TYPE_STR_PAIR,
+ MQTT_TYPE_BIN
+};
+
+struct mqtt_property {
+ uint8_t id;
+ enum mqtt_datatype type;
+ union {
+ char *strings[2];
+ void *bindata;
+ uint8_t uint8;
+ uint16_t uint16;
+ uint32_t uint32;
+ } data;
+ size_t bindata_len;
+ struct mqtt_property *next;
+};
+
+enum mqtt_properties_parser_state {
+ PROPERTIES_LENGTH = 0,
+ PROPERTY_CREATE,
+ PROPERTY_ID,
+ PROPERTY_TYPE_UINT8,
+ PROPERTY_TYPE_UINT16,
+ PROPERTY_TYPE_UINT32,
+ PROPERTY_TYPE_STR_BIN_LEN,
+ PROPERTY_TYPE_STR,
+ PROPERTY_TYPE_BIN,
+ PROPERTY_TYPE_VBI,
+ PROPERTY_NEXT
+};
+
+struct mqtt_properties_parser_ctx {
+ enum mqtt_properties_parser_state state;
+ struct mqtt_property *head;
+ struct mqtt_property *tail;
+ uint32_t properties_length;
+ uint32_t vbi_length;
+ struct mqtt_vbi_parser_ctx vbi_parser_ctx;
+ size_t bytes_consumed;
+ int str_idx;
+};
+
+struct mqtt_connack {
+ uint8_t flags;
+ uint8_t reason_code;
+};
+struct mqtt_puback {
+ uint16_t packet_id;
+ uint8_t reason_code;
+};
+
+struct mqtt_suback {
+ uint16_t packet_id;
+ uint8_t *reason_codes;
+ uint8_t reason_code_count;
+ uint8_t reason_codes_pending;
+};
+
+struct mqtt_publish {
+ uint16_t topic_len;
+ char *topic;
+ uint16_t packet_id;
+ size_t data_len;
+ char *data;
+ uint8_t qos;
+};
+
+struct mqtt_disconnect {
+ uint8_t reason_code;
+};
+
+struct mqtt_ng_parser {
+ rbuf_t received_data;
+
+ uint8_t mqtt_control_packet_type;
+ uint32_t mqtt_fixed_hdr_remaining_length;
+ size_t mqtt_parsed_len;
+
+ struct mqtt_vbi_parser_ctx vbi_parser;
+ struct mqtt_properties_parser_ctx properties_parser;
+
+ enum parser_state state;
+ enum varhdr_parser_state varhdr_state;
+
+ struct mqtt_property *varhdr_properties;
+
+ union {
+ struct mqtt_connack connack;
+ struct mqtt_puback puback;
+ struct mqtt_suback suback;
+ struct mqtt_publish publish;
+ struct mqtt_disconnect disconnect;
+ } mqtt_packet;
+};
+
+struct topic_alias_data {
+ uint16_t idx;
+ uint32_t usage_count;
+};
+
+struct topic_aliases_data {
+ c_rhash stoi_dict;
+ uint32_t idx_max;
+ uint32_t idx_assigned;
+ pthread_rwlock_t rwlock;
+};
+
+struct mqtt_ng_client {
+ struct transaction_buffer main_buffer;
+
+ enum mqtt_client_state client_state;
+
+ mqtt_msg_data connect_msg;
+
+ mqtt_wss_log_ctx_t log;
+
+ mqtt_ng_send_fnc_t send_fnc_ptr;
+ void *user_ctx;
+
+ // time when last fragment of MQTT message was sent
+ time_t time_of_last_send;
+
+ struct mqtt_ng_parser parser;
+
+ size_t max_mem_bytes;
+
+ void (*puback_callback)(uint16_t packet_id);
+ void (*connack_callback)(void* user_ctx, int connack_reply);
+ void (*msg_callback)(const char *topic, const void *msg, size_t msglen, int qos);
+
+ unsigned int ping_pending:1;
+
+ struct mqtt_ng_stats stats;
+ pthread_mutex_t stats_mutex;
+
+ struct topic_aliases_data tx_topic_aliases;
+ c_rhash rx_aliases;
+
+ size_t max_msg_size;
+};
+
+unsigned char pingreq[] = { MQTT_CPT_PINGREQ << 4, 0x00 };
+
+struct buffer_fragment ping_frag = {
+ .data = pingreq,
+ .flags = BUFFER_FRAG_MQTT_PACKET_HEAD | BUFFER_FRAG_MQTT_PACKET_TAIL,
+ .free_fnc = NULL,
+ .len = sizeof(pingreq),
+ .next = NULL,
+ .sent = 0,
+ .packet_id = 0
+};
+
+int uint32_to_mqtt_vbi(uint32_t input, unsigned char *output) {
+ int i = 1;
+ *output = 0;
+
+ /* MQTT 5 specs allows max 4 bytes of output
+ making it 0xFF, 0xFF, 0xFF, 0x7F
+ representing number 268435455 decimal
+ see 1.5.5. Variable Byte Integer */
+ if(input >= 256 * 1024 * 1024)
+ return 0;
+
+ if(!input) {
+ *output = 0;
+ return 1;
+ }
+
+ while(input) {
+ output[i-1] = input & MQTT_VBI_DATA_MASK;
+ input >>= 7;
+ if (input)
+ output[i-1] |= MQTT_VBI_CONTINUATION_FLAG;
+ i++;
+ }
+ return i - 1;
+}
+
+int mqtt_vbi_to_uint32(char *input, uint32_t *output) {
+ // dont want to operate directly on output
+ // as I want it to be possible for input and output
+ // pointer to be the same
+ uint32_t result = 0;
+ uint32_t multiplier = 1;
+
+ do {
+ result += (uint32_t)(*input & MQTT_VBI_DATA_MASK) * multiplier;
+ if (multiplier > 128*128*128)
+ return 1;
+ multiplier <<= 7;
+ } while (*input++ & MQTT_VBI_CONTINUATION_FLAG);
+ *output = result;
+ return 0;
+}
+
+#ifdef TESTS
+#include <stdio.h>
+#define MQTT_VBI_MAXLEN 4
+// we add extra byte to check we dont write out of bounds
+// in case where 4 bytes are supposed to be written
+static const char _mqtt_vbi_0[MQTT_VBI_MAXLEN + 1] = { 0x00, 0x00, 0x00, 0x00, 0x00 };
+static const char _mqtt_vbi_127[MQTT_VBI_MAXLEN + 1] = { 0x7F, 0x00, 0x00, 0x00, 0x00 };
+static const char _mqtt_vbi_128[MQTT_VBI_MAXLEN + 1] = { 0x80, 0x01, 0x00, 0x00, 0x00 };
+static const char _mqtt_vbi_16383[MQTT_VBI_MAXLEN + 1] = { 0xFF, 0x7F, 0x00, 0x00, 0x00 };
+static const char _mqtt_vbi_16384[MQTT_VBI_MAXLEN + 1] = { 0x80, 0x80, 0x01, 0x00, 0x00 };
+static const char _mqtt_vbi_2097151[MQTT_VBI_MAXLEN + 1] = { 0xFF, 0xFF, 0x7F, 0x00, 0x00 };
+static const char _mqtt_vbi_2097152[MQTT_VBI_MAXLEN + 1] = { 0x80, 0x80, 0x80, 0x01, 0x00 };
+static const char _mqtt_vbi_268435455[MQTT_VBI_MAXLEN + 1] = { 0xFF, 0xFF, 0xFF, 0x7F, 0x00 };
+static const char _mqtt_vbi_999999999[MQTT_VBI_MAXLEN + 1] = { 0x80, 0x80, 0x80, 0x80, 0x01 };
+
+#define MQTT_VBI_TESTCASE(case, expected_len) \
+ { \
+ memset(buf, 0, MQTT_VBI_MAXLEN + 1); \
+ int len; \
+ if ((len=uint32_to_mqtt_vbi(case, buf)) != expected_len) { \
+ fprintf(stderr, "uint32_to_mqtt_vbi(case:%d, line:%d): Incorrect length returned. Expected %d, Got %d\n", case, __LINE__, expected_len, len); \
+ return 1; \
+ } \
+ if (memcmp(buf, _mqtt_vbi_ ## case, MQTT_VBI_MAXLEN + 1 )) { \
+ fprintf(stderr, "uint32_to_mqtt_vbi(case:%d, line:%d): Wrong output\n", case, __LINE__); \
+ return 1; \
+ } }
+
+
+int test_uint32_mqtt_vbi() {
+ char buf[MQTT_VBI_MAXLEN + 1];
+
+ MQTT_VBI_TESTCASE(0, 1)
+ MQTT_VBI_TESTCASE(127, 1)
+ MQTT_VBI_TESTCASE(128, 2)
+ MQTT_VBI_TESTCASE(16383, 2)
+ MQTT_VBI_TESTCASE(16384, 3)
+ MQTT_VBI_TESTCASE(2097151, 3)
+ MQTT_VBI_TESTCASE(2097152, 4)
+ MQTT_VBI_TESTCASE(268435455, 4)
+
+ memset(buf, 0, MQTT_VBI_MAXLEN + 1);
+ int len;
+ if ((len=uint32_to_mqtt_vbi(268435456, buf)) != 0) {
+ fprintf(stderr, "uint32_to_mqtt_vbi(case:268435456, line:%d): Incorrect length returned. Expected 0, Got %d\n", __LINE__, len);
+ return 1;
+ }
+
+ return 0;
+}
+
+#define MQTT_VBI2UINT_TESTCASE(case, expected_error) \
+ { \
+ uint32_t result; \
+ int ret = mqtt_vbi_to_uint32(_mqtt_vbi_ ## case, &result); \
+ if (ret && !(expected_error)) { \
+ fprintf(stderr, "mqtt_vbi_to_uint(case:%d, line:%d): Unexpectedly Errored\n", (case), __LINE__); \
+ return 1; \
+ } \
+ if (!ret && (expected_error)) { \
+ fprintf(stderr, "mqtt_vbi_to_uint(case:%d, line:%d): Should return error but didnt\n", (case), __LINE__); \
+ return 1; \
+ } \
+ if (!ret && result != (case)) { \
+ fprintf(stderr, "mqtt_vbi_to_uint(case:%d, line:%d): Returned wrong result %d\n", (case), __LINE__, result); \
+ return 1; \
+ }}
+
+
+int test_mqtt_vbi_to_uint32() {
+ MQTT_VBI2UINT_TESTCASE(0, 0)
+ MQTT_VBI2UINT_TESTCASE(127, 0)
+ MQTT_VBI2UINT_TESTCASE(128, 0)
+ MQTT_VBI2UINT_TESTCASE(16383, 0)
+ MQTT_VBI2UINT_TESTCASE(16384, 0)
+ MQTT_VBI2UINT_TESTCASE(2097151, 0)
+ MQTT_VBI2UINT_TESTCASE(2097152, 0)
+ MQTT_VBI2UINT_TESTCASE(268435455, 0)
+ MQTT_VBI2UINT_TESTCASE(999999999, 1)
+ return 0;
+}
+#endif /* TESTS */
+
+// this helps with switch statements
+// as they have to use integer type (not pointer)
+enum memory_mode {
+ MEMCPY,
+ EXTERNAL_FREE_AFTER_USE,
+ CALLER_RESPONSIBLE
+};
+
+static inline enum memory_mode ptr2memory_mode(void * ptr) {
+ if (ptr == NULL)
+ return MEMCPY;
+ if (ptr == CALLER_RESPONSIBILITY)
+ return CALLER_RESPONSIBLE;
+ return EXTERNAL_FREE_AFTER_USE;
+}
+
+#define frag_is_marked_for_gc(frag) ((frag->flags & BUFFER_FRAG_GARBAGE_COLLECT) || ((frag->flags & BUFFER_FRAG_GARBAGE_COLLECT_ON_SEND) && frag->sent == frag->len))
+#define FRAG_SIZE_IN_BUFFER(frag) (sizeof(struct buffer_fragment) + ((frag->flags & BUFFER_FRAG_DATA_EXTERNAL) ? 0 : frag->len))
+
+static void buffer_frag_free_data(struct buffer_fragment *frag)
+{
+ if ( frag->flags & BUFFER_FRAG_DATA_EXTERNAL && frag->data != NULL) {
+ switch (ptr2memory_mode(frag->free_fnc)) {
+ case MEMCPY:
+ mw_free(frag->data);
+ break;
+ case EXTERNAL_FREE_AFTER_USE:
+ frag->free_fnc(frag->data);
+ break;
+ case CALLER_RESPONSIBLE:
+ break;
+ }
+ frag->data = NULL;
+ }
+}
+
+#define HEADER_BUFFER_SIZE 1024*1024
+#define GROWTH_FACTOR 1.25
+
+#define BUFFER_BYTES_USED(buf) ((size_t)((buf)->tail - (buf)->data))
+#define BUFFER_BYTES_AVAILABLE(buf) ((buf)->size - BUFFER_BYTES_USED(buf))
+#define BUFFER_FIRST_FRAG(buf) ((struct buffer_fragment *)((buf)->tail_frag ? (buf)->data : NULL))
+static void buffer_purge(struct header_buffer *buf) {
+ struct buffer_fragment *frag = BUFFER_FIRST_FRAG(buf);
+ while (frag) {
+ buffer_frag_free_data(frag);
+ frag = frag->next;
+ }
+ buf->tail = buf->data;
+ buf->tail_frag = NULL;
+}
+
+#define FRAG_PADDING(addr) ((MQTT_WSS_FRAG_MEMALIGN - ((uintptr_t)addr % MQTT_WSS_FRAG_MEMALIGN)) % MQTT_WSS_FRAG_MEMALIGN)
+static struct buffer_fragment *buffer_new_frag(struct header_buffer *buf, buffer_frag_flag_t flags)
+{
+ uint8_t padding = FRAG_PADDING(buf->tail);
+
+ if (BUFFER_BYTES_AVAILABLE(buf) < sizeof(struct buffer_fragment) + padding)
+ return NULL;
+
+ struct buffer_fragment *frag = (struct buffer_fragment *)(buf->tail + padding);
+
+ memset(frag, 0, sizeof(*frag));
+ buf->tail += sizeof(*frag) + padding;
+
+ if (/*!((frag)->flags & BUFFER_FRAG_MQTT_PACKET_HEAD) &&*/ buf->tail_frag)
+ buf->tail_frag->next = frag;
+
+ buf->tail_frag = frag;
+
+ frag->data = buf->tail;
+
+ frag->flags = flags;
+
+ return frag;
+}
+
+static void buffer_rebuild(struct header_buffer *buf)
+{
+ struct buffer_fragment *frag = (struct buffer_fragment*)buf->data;
+ do {
+ buf->tail = (unsigned char *) frag + sizeof(struct buffer_fragment);
+ buf->tail_frag = frag;
+ if (!(frag->flags & BUFFER_FRAG_DATA_EXTERNAL)) {
+ buf->tail_frag->data = buf->tail;
+ buf->tail += frag->len;
+ }
+ if (frag->next != NULL)
+ frag->next = (struct buffer_fragment*)(buf->tail + FRAG_PADDING(buf->tail));
+ frag = frag->next;
+ } while(frag);
+}
+
+static void buffer_garbage_collect(struct header_buffer *buf, mqtt_wss_log_ctx_t log_ctx)
+{
+#if !defined(MQTT_DEBUG_VERBOSE) && !defined(ADDITIONAL_CHECKS)
+ (void) log_ctx;
+#endif
+#ifdef MQTT_DEBUG_VERBOSE
+ mws_debug(log_ctx, "Buffer Garbage Collection!");
+#endif
+
+ struct buffer_fragment *frag = BUFFER_FIRST_FRAG(buf);
+ while (frag) {
+ if (!frag_is_marked_for_gc(frag))
+ break;
+
+ buffer_frag_free_data(frag);
+
+ frag = frag->next;
+ }
+
+ if (frag == BUFFER_FIRST_FRAG(buf)) {
+#ifdef MQTT_DEBUG_VERBOSE
+ mws_debug(log_ctx, "Buffer Garbage Collection! No Space Reclaimed!");
+#endif
+ return;
+ }
+
+ if (!frag) {
+ buf->tail_frag = NULL;
+ buf->tail = buf->data;
+ return;
+ }
+
+#ifdef ADDITIONAL_CHECKS
+ if (!(frag->flags & BUFFER_FRAG_MQTT_PACKET_HEAD)) {
+ mws_error(log_ctx, "Expected to find end of buffer (NULL) or next packet head!");
+ return;
+ }
+#endif
+
+ memmove(buf->data, frag, buf->tail - (unsigned char *) frag);
+ buffer_rebuild(buf);
+}
+
+static void transaction_buffer_garbage_collect(struct transaction_buffer *buf, mqtt_wss_log_ctx_t log_ctx)
+{
+#ifdef MQTT_DEBUG_VERBOSE
+ mws_debug(log_ctx, "Transaction Buffer Garbage Collection! %s", buf->sending_frag == NULL ? "NULL" : "in flight message");
+#endif
+
+ // Invalidate the cached sending fragment
+ // as we will move data around
+ if (buf->sending_frag != &ping_frag)
+ buf->sending_frag = NULL;
+
+ buffer_garbage_collect(&buf->hdr_buffer, log_ctx);
+}
+
+static int transaction_buffer_grow(struct transaction_buffer *buf, mqtt_wss_log_ctx_t log_ctx, float rate, size_t max)
+{
+ if (buf->hdr_buffer.size >= max)
+ return 0;
+
+ // Invalidate the cached sending fragment
+ // as we will move data around
+ if (buf->sending_frag != &ping_frag)
+ buf->sending_frag = NULL;
+
+ buf->hdr_buffer.size *= rate;
+ if (buf->hdr_buffer.size > max)
+ buf->hdr_buffer.size = max;
+
+ void *ret = mw_realloc(buf->hdr_buffer.data, buf->hdr_buffer.size);
+ if (ret == NULL) {
+ mws_warn(log_ctx, "Buffer growth failed (realloc)");
+ return 1;
+ }
+
+ mws_debug(log_ctx, "Message metadata buffer was grown");
+
+ buf->hdr_buffer.data = ret;
+ buffer_rebuild(&buf->hdr_buffer);
+ return 0;
+}
+
+inline static int transaction_buffer_init(struct transaction_buffer *to_init, size_t size)
+{
+ pthread_mutex_init(&to_init->mutex, NULL);
+
+ to_init->hdr_buffer.size = size;
+ to_init->hdr_buffer.data = mw_malloc(size);
+ if (to_init->hdr_buffer.data == NULL)
+ return 1;
+
+ to_init->hdr_buffer.tail = to_init->hdr_buffer.data;
+ to_init->hdr_buffer.tail_frag = NULL;
+ return 0;
+}
+
+static void transaction_buffer_destroy(struct transaction_buffer *to_init)
+{
+ buffer_purge(&to_init->hdr_buffer);
+ pthread_mutex_destroy(&to_init->mutex);
+ mw_free(to_init->hdr_buffer.data);
+}
+
+// Creates transaction
+// saves state of buffer before any operation was done
+// allowing for rollback if things go wrong
+#define transaction_buffer_transaction_start(buf) \
+ { LOCK_HDR_BUFFER(buf); \
+ memcpy(&(buf)->state_backup, &(buf)->hdr_buffer, sizeof((buf)->hdr_buffer)); }
+
+#define transaction_buffer_transaction_commit(buf) UNLOCK_HDR_BUFFER(buf);
+
+void transaction_buffer_transaction_rollback(struct transaction_buffer *buf, struct buffer_fragment *frag)
+{
+ memcpy(&buf->hdr_buffer, &buf->state_backup, sizeof(buf->hdr_buffer));
+ if (buf->hdr_buffer.tail_frag != NULL)
+ buf->hdr_buffer.tail_frag->next = NULL;
+
+ while(frag) {
+ buffer_frag_free_data(frag);
+ // we are not actually freeing the structure itself
+ // just the data it manages
+ // structure itself is in permanent buffer
+ // which is locked by HDR_BUFFER lock
+ frag = frag->next;
+ }
+
+ UNLOCK_HDR_BUFFER(buf);
+}
+
+#define TX_ALIASES_INITIALIZE() c_rhash_new(0)
+#define RX_ALIASES_INITIALIZE() c_rhash_new(UINT16_MAX >> 8)
+struct mqtt_ng_client *mqtt_ng_init(struct mqtt_ng_init *settings)
+{
+ struct mqtt_ng_client *client = mw_calloc(1, sizeof(struct mqtt_ng_client));
+ if (client == NULL)
+ return NULL;
+
+ if (transaction_buffer_init(&client->main_buffer, HEADER_BUFFER_SIZE))
+ goto err_free_client;
+
+ client->rx_aliases = RX_ALIASES_INITIALIZE();
+ if (client->rx_aliases == NULL)
+ goto err_free_trx_buf;
+
+ if (pthread_mutex_init(&client->stats_mutex, NULL))
+ goto err_free_rx_alias;
+
+ client->tx_topic_aliases.stoi_dict = TX_ALIASES_INITIALIZE();
+ if (client->tx_topic_aliases.stoi_dict == NULL)
+ goto err_free_stats_mutex;
+ client->tx_topic_aliases.idx_max = UINT16_MAX;
+
+ if (pthread_rwlock_init(&client->tx_topic_aliases.rwlock, NULL))
+ goto err_free_tx_alias;
+
+ // TODO just embed the struct into mqtt_ng_client
+ client->parser.received_data = settings->data_in;
+ client->send_fnc_ptr = settings->data_out_fnc;
+ client->user_ctx = settings->user_ctx;
+
+ client->log = settings->log;
+
+ client->puback_callback = settings->puback_callback;
+ client->connack_callback = settings->connack_callback;
+ client->msg_callback = settings->msg_callback;
+
+ return client;
+
+err_free_tx_alias:
+ c_rhash_destroy(client->tx_topic_aliases.stoi_dict);
+err_free_stats_mutex:
+ pthread_mutex_destroy(&client->stats_mutex);
+err_free_rx_alias:
+ c_rhash_destroy(client->rx_aliases);
+err_free_trx_buf:
+ transaction_buffer_destroy(&client->main_buffer);
+err_free_client:
+ mw_free(client);
+ return NULL;
+}
+
+static inline uint8_t get_control_packet_type(uint8_t first_hdr_byte)
+{
+ return first_hdr_byte >> 4;
+}
+
+static void mqtt_ng_destroy_rx_alias_hash(c_rhash hash)
+{
+ c_rhash_iter_t i = C_RHASH_ITER_T_INITIALIZER;
+ uint64_t stored_key;
+ void *to_free;
+ while(!c_rhash_iter_uint64_keys(hash, &i, &stored_key)) {
+ c_rhash_get_ptr_by_uint64(hash, stored_key, &to_free);
+ mw_free(to_free);
+ }
+ c_rhash_destroy(hash);
+}
+
+static void mqtt_ng_destroy_tx_alias_hash(c_rhash hash)
+{
+ c_rhash_iter_t i = C_RHASH_ITER_T_INITIALIZER;
+ const char *stored_key;
+ void *to_free;
+ while(!c_rhash_iter_str_keys(hash, &i, &stored_key)) {
+ c_rhash_get_ptr_by_str(hash, stored_key, &to_free);
+ mw_free(to_free);
+ }
+ c_rhash_destroy(hash);
+}
+
+void mqtt_ng_destroy(struct mqtt_ng_client *client)
+{
+ transaction_buffer_destroy(&client->main_buffer);
+ pthread_mutex_destroy(&client->stats_mutex);
+
+ mqtt_ng_destroy_tx_alias_hash(client->tx_topic_aliases.stoi_dict);
+ pthread_rwlock_destroy(&client->tx_topic_aliases.rwlock);
+ mqtt_ng_destroy_rx_alias_hash(client->rx_aliases);
+
+ mw_free(client);
+}
+
+int frag_set_external_data(mqtt_wss_log_ctx_t log, struct buffer_fragment *frag, void *data, size_t data_len, free_fnc_t data_free_fnc)
+{
+ if (frag->len) {
+ // TODO?: This could potentially be done in future if we set rule
+ // external data always follows in buffer data
+ // could help reduce fragmentation in some messages but
+ // currently not worth it considering time is tight
+ mws_fatal(log, UNIT_LOG_PREFIX "INTERNAL ERROR: Cannot set external data to fragment already containing in buffer data!");
+ return 1;
+ }
+
+ switch (ptr2memory_mode(data_free_fnc)) {
+ case MEMCPY:
+ frag->data = mw_malloc(data_len);
+ if (frag->data == NULL) {
+ mws_error(log, UNIT_LOG_PREFIX "OOM while malloc @_optimized_add");
+ return 1;
+ }
+ memcpy(frag->data, data, data_len);
+ break;
+ case EXTERNAL_FREE_AFTER_USE:
+ case CALLER_RESPONSIBLE:
+ frag->data = data;
+ break;
+ }
+ frag->free_fnc = data_free_fnc;
+ frag->len = data_len;
+
+ frag->flags |= BUFFER_FRAG_DATA_EXTERNAL;
+ return 0;
+ }
+
+// this is fixed part of variable header for connect packet
+// mqtt-v5.0-cs1, 3.1.2.1, 2.1.2.2
+static const char mqtt_protocol_name_frag[] =
+ { 0x00, 0x04, 'M', 'Q', 'T', 'T', MQTT_VERSION_5_0 };
+
+#define MQTT_UTF8_STRING_SIZE(string) (2 + strlen(string))
+
+// see 1.5.5
+#define MQTT_VARSIZE_INT_BYTES(value) ( value > 2097152 ? 4 : ( value > 16384 ? 3 : ( value > 128 ? 2 : 1 ) ) )
+
+static size_t mqtt_ng_connect_size(struct mqtt_auth_properties *auth,
+ struct mqtt_lwt_properties *lwt)
+{
+ // First get the size of payload + variable header
+ size_t size =
+ + sizeof(mqtt_protocol_name_frag) /* Proto Name and Version */
+ + 1 /* Connect Flags */
+ + 2 /* Keep Alive */
+ + 4 /* 3.1.2.11.1 Property Length - for now fixed to only Topic Alias Maximum, TODO TODO*/;
+
+ // CONNECT payload. 3.1.3
+ if (auth->client_id)
+ size += MQTT_UTF8_STRING_SIZE(auth->client_id);
+
+ if (lwt) {
+ // 3.1.3.2 will properties TODO TODO
+ size += 1;
+
+ // 3.1.3.3
+ if (lwt->will_topic)
+ size += MQTT_UTF8_STRING_SIZE(lwt->will_topic);
+
+ // 3.1.3.4 will payload
+ if (lwt->will_message) {
+ size += 2 + lwt->will_message_size;
+ }
+ }
+
+ // 3.1.3.5
+ if (auth->username)
+ size += MQTT_UTF8_STRING_SIZE(auth->username);
+
+ // 3.1.3.6
+ if (auth->password)
+ size += MQTT_UTF8_STRING_SIZE(auth->password);
+
+ return size;
+}
+
+#define BUFFER_TRANSACTION_NEW_FRAG(buf, flags, frag, on_fail) \
+ { if(frag==NULL) { \
+ frag = buffer_new_frag(buf, (flags)); } \
+ if(frag==NULL) { on_fail; }}
+
+#define CHECK_BYTES_AVAILABLE(buf, needed, fail) \
+ { if (BUFFER_BYTES_AVAILABLE(buf) < (size_t)needed) { \
+ fail; } }
+
+#define DATA_ADVANCE(buf, bytes, frag) { size_t b = (bytes); (buf)->tail += b; (frag)->len += b; }
+
+// TODO maybe just user client->buf.tail?
+#define WRITE_POS(frag) (&(frag->data[frag->len]))
+
+// [MQTT-1.5.2] Two Byte Integer
+#define PACK_2B_INT(buffer, integer, frag) { *(uint16_t *)WRITE_POS(frag) = htobe16((integer)); \
+ DATA_ADVANCE(buffer, sizeof(uint16_t), frag); }
+
+static int _optimized_add(struct header_buffer *buf, mqtt_wss_log_ctx_t log_ctx, void *data, size_t data_len, free_fnc_t data_free_fnc, struct buffer_fragment **frag)
+{
+ if (data_len > SMALL_STRING_DONT_FRAGMENT_LIMIT) {
+ buffer_frag_flag_t flags = BUFFER_FRAG_DATA_EXTERNAL;
+ if ((*frag)->flags & BUFFER_FRAG_GARBAGE_COLLECT_ON_SEND)
+ flags |= BUFFER_FRAG_GARBAGE_COLLECT_ON_SEND;
+ if( (*frag = buffer_new_frag(buf, flags)) == NULL ) {
+ mws_error(log_ctx, "Out of buffer space while generating the message");
+ return 1;
+ }
+ if (frag_set_external_data(log_ctx, *frag, data, data_len, data_free_fnc)) {
+ mws_error(log_ctx, "Error adding external data to newly created fragment");
+ return 1;
+ }
+ // we dont want to write to this fragment anymore
+ *frag = NULL;
+ } else if (data_len) {
+ // if the data are small dont bother creating new fragments
+ // store in buffer directly
+ CHECK_BYTES_AVAILABLE(buf, data_len, return 1);
+ memcpy(buf->tail, data, data_len);
+ DATA_ADVANCE(buf, data_len, *frag);
+ }
+ return 0;
+}
+
+#define TRY_GENERATE_MESSAGE(generator_function, client, ...) \
+ int rc = generator_function(&client->main_buffer, client->log, ##__VA_ARGS__); \
+ if (rc == MQTT_NG_MSGGEN_BUFFER_OOM) { \
+ LOCK_HDR_BUFFER(&client->main_buffer); \
+ transaction_buffer_garbage_collect((&client->main_buffer), client->log); \
+ UNLOCK_HDR_BUFFER(&client->main_buffer); \
+ rc = generator_function(&client->main_buffer, client->log, ##__VA_ARGS__); \
+ if (rc == MQTT_NG_MSGGEN_BUFFER_OOM && client->max_mem_bytes) { \
+ LOCK_HDR_BUFFER(&client->main_buffer); \
+ transaction_buffer_grow((&client->main_buffer), client->log, GROWTH_FACTOR, client->max_mem_bytes); \
+ UNLOCK_HDR_BUFFER(&client->main_buffer); \
+ rc = generator_function(&client->main_buffer, client->log, ##__VA_ARGS__); \
+ } \
+ if (rc == MQTT_NG_MSGGEN_BUFFER_OOM) \
+ mws_error(client->log, "%s failed to generate message due to insufficient buffer space (line %d)", __FUNCTION__, __LINE__); \
+ } \
+ if (rc == MQTT_NG_MSGGEN_OK) { \
+ pthread_mutex_lock(&client->stats_mutex); \
+ client->stats.tx_messages_queued++; \
+ pthread_mutex_unlock(&client->stats_mutex); \
+ } \
+ return rc;
+
+mqtt_msg_data mqtt_ng_generate_connect(struct transaction_buffer *trx_buf,
+ mqtt_wss_log_ctx_t log_ctx,
+ struct mqtt_auth_properties *auth,
+ struct mqtt_lwt_properties *lwt,
+ uint8_t clean_start,
+ uint16_t keep_alive)
+{
+ // Sanity Checks First (are given parameters correct and up to MQTT spec)
+ if (!auth->client_id) {
+ mws_error(log_ctx, "ClientID must be set. [MQTT-3.1.3-3]");
+ return NULL;
+ }
+
+ size_t len = strlen(auth->client_id);
+ if (!len) {
+ // [MQTT-3.1.3-6] server MAY allow empty client_id and treat it
+ // as specific client_id (not same as client_id not given)
+ // however server MUST allow ClientIDs between 1-23 bytes [MQTT-3.1.3-5]
+ // so we will warn client server might not like this and he is using it
+ // at his own risk!
+ mws_warn(log_ctx, "client_id provided is empty string. This might not be allowed by server [MQTT-3.1.3-6]");
+ }
+ if(len > MQTT_MAX_CLIENT_ID) {
+ // [MQTT-3.1.3-5] server MUST allow client_id length 1-32
+ // server MAY allow longer client_id, if user provides longer client_id
+ // warn them he is doing so at his own risk!
+ mws_warn(log_ctx, "client_id provided is longer than 23 bytes, server might not allow that [MQTT-3.1.3-5]");
+ }
+
+ if (lwt) {
+ if (lwt->will_message && lwt->will_message_size > 65535) {
+ mws_error(log_ctx, "Will message cannot be longer than 65535 bytes due to MQTT protocol limitations [MQTT-3.1.3-4] and [MQTT-1.5.6]");
+ return NULL;
+ }
+
+ if (!lwt->will_topic) { //TODO topic given with strlen==0 ? check specs
+ mws_error(log_ctx, "If will message is given will topic must also be given [MQTT-3.1.3.3]");
+ return NULL;
+ }
+
+ if (lwt->will_qos > MQTT_MAX_QOS) {
+ // refer to [MQTT-3-1.2-12]
+ mws_error(log_ctx, "QOS for LWT message is bigger than max");
+ return NULL;
+ }
+ }
+
+ // >> START THE RODEO <<
+ transaction_buffer_transaction_start(trx_buf);
+
+ // Calculate the resulting message size sans fixed MQTT header
+ size_t size = mqtt_ng_connect_size(auth, lwt);
+
+ // Start generating the message
+ struct buffer_fragment *frag = NULL;
+ mqtt_msg_data ret = NULL;
+
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, BUFFER_FRAG_MQTT_PACKET_HEAD, frag, goto fail_rollback );
+ ret = frag;
+
+ // MQTT Fixed Header
+ size_t needed_bytes = 1 /* Packet type */ + MQTT_VARSIZE_INT_BYTES(size) + sizeof(mqtt_protocol_name_frag) + 1 /* CONNECT FLAGS */ + 2 /* keepalive */ + 1 /* Properties TODO now fixed 0*/;
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, needed_bytes, goto fail_rollback);
+
+ *WRITE_POS(frag) = MQTT_CPT_CONNECT << 4;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+ DATA_ADVANCE(&trx_buf->hdr_buffer, uint32_to_mqtt_vbi(size, WRITE_POS(frag)), frag);
+
+ memcpy(WRITE_POS(frag), mqtt_protocol_name_frag, sizeof(mqtt_protocol_name_frag));
+ DATA_ADVANCE(&trx_buf->hdr_buffer, sizeof(mqtt_protocol_name_frag), frag);
+
+ // [MQTT-3.1.2.3] Connect flags
+ unsigned char *connect_flags = WRITE_POS(frag);
+ *connect_flags = 0;
+ if (auth->username)
+ *connect_flags |= MQTT_CONNECT_FLAG_USERNAME;
+ if (auth->password)
+ *connect_flags |= MQTT_CONNECT_FLAG_PASSWORD;
+ if (lwt) {
+ *connect_flags |= MQTT_CONNECT_FLAG_LWT;
+ *connect_flags |= lwt->will_qos << MQTT_CONNECT_FLAG_QOS_BITSHIFT;
+ if (lwt->will_retain)
+ *connect_flags |= MQTT_CONNECT_FLAG_LWT_RETAIN;
+ }
+ if (clean_start)
+ *connect_flags |= MQTT_CONNECT_FLAG_CLEAN_START;
+
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+
+ PACK_2B_INT(&trx_buf->hdr_buffer, keep_alive, frag);
+
+ // TODO Property Length [MQTT-3.1.3.2.1] temporary fixed to 3 (one property topic alias max)
+ DATA_ADVANCE(&trx_buf->hdr_buffer, uint32_to_mqtt_vbi(3, WRITE_POS(frag)), frag);
+ *WRITE_POS(frag) = MQTT_PROP_TOPIC_ALIAS_MAX;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+
+ PACK_2B_INT(&trx_buf->hdr_buffer, 65535, frag);
+
+ // [MQTT-3.1.3.1] Client identifier
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, 2, goto fail_rollback);
+ PACK_2B_INT(&trx_buf->hdr_buffer, strlen(auth->client_id), frag);
+ if (_optimized_add(&trx_buf->hdr_buffer, log_ctx, auth->client_id, strlen(auth->client_id), auth->client_id_free, &frag))
+ goto fail_rollback;
+
+ if (lwt != NULL) {
+ // Will Properties [MQTT-3.1.3.2]
+ // TODO for now fixed 0
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, 0, frag, goto fail_rollback);
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, 1, goto fail_rollback);
+ *WRITE_POS(frag) = 0;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+
+ // Will Topic [MQTT-3.1.3.3]
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, 2, goto fail_rollback);
+ PACK_2B_INT(&trx_buf->hdr_buffer, strlen(lwt->will_topic), frag);
+ if (_optimized_add(&trx_buf->hdr_buffer, log_ctx, lwt->will_topic, strlen(lwt->will_topic), lwt->will_topic_free, &frag))
+ goto fail_rollback;
+
+ // Will Payload [MQTT-3.1.3.4]
+ if (lwt->will_message_size) {
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, 0, frag, goto fail_rollback);
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, 2, goto fail_rollback);
+ PACK_2B_INT(&trx_buf->hdr_buffer, lwt->will_message_size, frag);
+ if (_optimized_add(&trx_buf->hdr_buffer, log_ctx, lwt->will_message, lwt->will_message_size, lwt->will_topic_free, &frag))
+ goto fail_rollback;
+ }
+ }
+
+ // [MQTT-3.1.3.5]
+ if (auth->username) {
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, 0, frag, goto fail_rollback);
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, 2, goto fail_rollback);
+ PACK_2B_INT(&trx_buf->hdr_buffer, strlen(auth->username), frag);
+ if (_optimized_add(&trx_buf->hdr_buffer, log_ctx, auth->username, strlen(auth->username), auth->username_free, &frag))
+ goto fail_rollback;
+ }
+
+ // [MQTT-3.1.3.6]
+ if (auth->password) {
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, 0, frag, goto fail_rollback);
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, 2, goto fail_rollback);
+ PACK_2B_INT(&trx_buf->hdr_buffer, strlen(auth->password), frag);
+ if (_optimized_add(&trx_buf->hdr_buffer, log_ctx, auth->password, strlen(auth->password), auth->password_free, &frag))
+ goto fail_rollback;
+ }
+ trx_buf->hdr_buffer.tail_frag->flags |= BUFFER_FRAG_MQTT_PACKET_TAIL;
+ transaction_buffer_transaction_commit(trx_buf);
+ return ret;
+fail_rollback:
+ transaction_buffer_transaction_rollback(trx_buf, ret);
+ return NULL;
+}
+
+int mqtt_ng_connect(struct mqtt_ng_client *client,
+ struct mqtt_auth_properties *auth,
+ struct mqtt_lwt_properties *lwt,
+ uint8_t clean_start,
+ uint16_t keep_alive)
+{
+ client->client_state = RAW;
+ client->parser.state = MQTT_PARSE_FIXED_HEADER_PACKET_TYPE;
+
+ LOCK_HDR_BUFFER(&client->main_buffer);
+ client->main_buffer.sending_frag = NULL;
+ if (clean_start)
+ buffer_purge(&client->main_buffer.hdr_buffer);
+ UNLOCK_HDR_BUFFER(&client->main_buffer);
+
+ pthread_rwlock_wrlock(&client->tx_topic_aliases.rwlock);
+ // according to MQTT spec topic aliases should not be persisted
+ // even if clean session is true
+ mqtt_ng_destroy_tx_alias_hash(client->tx_topic_aliases.stoi_dict);
+ client->tx_topic_aliases.stoi_dict = TX_ALIASES_INITIALIZE();
+ if (client->tx_topic_aliases.stoi_dict == NULL) {
+ pthread_rwlock_unlock(&client->tx_topic_aliases.rwlock);
+ return 1;
+ }
+ client->tx_topic_aliases.idx_assigned = 0;
+ pthread_rwlock_unlock(&client->tx_topic_aliases.rwlock);
+
+ mqtt_ng_destroy_rx_alias_hash(client->rx_aliases);
+ client->rx_aliases = RX_ALIASES_INITIALIZE();
+ if (client->rx_aliases == NULL)
+ return 1;
+
+ client->connect_msg = mqtt_ng_generate_connect(&client->main_buffer, client->log, auth, lwt, clean_start, keep_alive);
+ if (client->connect_msg == NULL)
+ return 1;
+
+ pthread_mutex_lock(&client->stats_mutex);
+ if (clean_start)
+ client->stats.tx_messages_queued = 1;
+ else
+ client->stats.tx_messages_queued++;
+
+ client->stats.tx_messages_sent = 0;
+ client->stats.rx_messages_rcvd = 0;
+ pthread_mutex_unlock(&client->stats_mutex);
+
+ client->client_state = CONNECT_PENDING;
+ return 0;
+}
+
+uint16_t get_unused_packet_id() {
+ static uint16_t packet_id = 0;
+ packet_id++;
+ return packet_id ? packet_id : ++packet_id;
+}
+
+static inline size_t mqtt_ng_publish_size(const char *topic,
+ size_t msg_len,
+ uint16_t topic_id)
+{
+ size_t retval = 2 /* Topic Name Length */
+ + (topic == NULL ? 0 : strlen(topic))
+ + 2 /* Packet identifier */
+ + 1 /* Properties Length TODO for now fixed to 1 property */
+ + msg_len;
+
+ if (topic_id)
+ retval += 3;
+
+ return retval;
+}
+
+int mqtt_ng_generate_publish(struct transaction_buffer *trx_buf,
+ mqtt_wss_log_ctx_t log_ctx,
+ char *topic,
+ free_fnc_t topic_free,
+ void *msg,
+ free_fnc_t msg_free,
+ size_t msg_len,
+ uint8_t publish_flags,
+ uint16_t *packet_id,
+ uint16_t topic_alias)
+{
+ // >> START THE RODEO <<
+ transaction_buffer_transaction_start(trx_buf);
+
+ // Calculate the resulting message size sans fixed MQTT header
+ size_t size = mqtt_ng_publish_size(topic, msg_len, topic_alias);
+
+ // Start generating the message
+ struct buffer_fragment *frag = NULL;
+ mqtt_msg_data mqtt_msg = NULL;
+
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, BUFFER_FRAG_MQTT_PACKET_HEAD, frag, goto fail_rollback );
+ // in case of QOS 0 we can garbage collect immediatelly after sending
+ uint8_t qos = (publish_flags >> 1) & 0x03;
+ if (!qos)
+ frag->flags |= BUFFER_FRAG_GARBAGE_COLLECT_ON_SEND;
+ mqtt_msg = frag;
+
+ // MQTT Fixed Header
+ size_t needed_bytes = 1 /* Packet type */ + MQTT_VARSIZE_INT_BYTES(size) + size - msg_len;
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, needed_bytes, goto fail_rollback);
+
+ *WRITE_POS(frag) = (MQTT_CPT_PUBLISH << 4) | (publish_flags & 0xF);
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+ DATA_ADVANCE(&trx_buf->hdr_buffer, uint32_to_mqtt_vbi(size, WRITE_POS(frag)), frag);
+
+ // MQTT Variable Header
+ // [MQTT-3.3.2.1]
+ PACK_2B_INT(&trx_buf->hdr_buffer, topic == NULL ? 0 : strlen(topic), frag);
+ if (topic != NULL) {
+ if (_optimized_add(&trx_buf->hdr_buffer, log_ctx, topic, strlen(topic), topic_free, &frag))
+ goto fail_rollback;
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, 0, frag, goto fail_rollback);
+ }
+
+ // [MQTT-3.3.2.2]
+ mqtt_msg->packet_id = get_unused_packet_id();
+ *packet_id = mqtt_msg->packet_id;
+ PACK_2B_INT(&trx_buf->hdr_buffer, mqtt_msg->packet_id, frag);
+
+ // [MQTT-3.3.2.3.1] TODO Property Length for now fixed 0
+ *WRITE_POS(frag) = topic_alias ? 3 : 0;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+
+ if(topic_alias) {
+ *WRITE_POS(frag) = MQTT_PROP_TOPIC_ALIAS;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+
+ PACK_2B_INT(&trx_buf->hdr_buffer, topic_alias, frag);
+ }
+
+ if( (frag = buffer_new_frag(&trx_buf->hdr_buffer, BUFFER_FRAG_DATA_EXTERNAL)) == NULL )
+ goto fail_rollback;
+
+ if (frag_set_external_data(log_ctx, frag, msg, msg_len, msg_free))
+ goto fail_rollback;
+
+ trx_buf->hdr_buffer.tail_frag->flags |= BUFFER_FRAG_MQTT_PACKET_TAIL;
+ if (!qos)
+ trx_buf->hdr_buffer.tail_frag->flags |= BUFFER_FRAG_GARBAGE_COLLECT_ON_SEND;
+ transaction_buffer_transaction_commit(trx_buf);
+ return MQTT_NG_MSGGEN_OK;
+fail_rollback:
+ transaction_buffer_transaction_rollback(trx_buf, mqtt_msg);
+ return MQTT_NG_MSGGEN_BUFFER_OOM;
+}
+
+#define PUBLISH_SP_SIZE 64
+int mqtt_ng_publish(struct mqtt_ng_client *client,
+ char *topic,
+ free_fnc_t topic_free,
+ void *msg,
+ free_fnc_t msg_free,
+ size_t msg_len,
+ uint8_t publish_flags,
+ uint16_t *packet_id)
+{
+ struct topic_alias_data *alias = NULL;
+ pthread_rwlock_rdlock(&client->tx_topic_aliases.rwlock);
+ c_rhash_get_ptr_by_str(client->tx_topic_aliases.stoi_dict, topic, (void**)&alias);
+ pthread_rwlock_unlock(&client->tx_topic_aliases.rwlock);
+
+ uint16_t topic_id = 0;
+
+ if (alias != NULL) {
+ topic_id = alias->idx;
+ uint32_t cnt = __atomic_fetch_add(&alias->usage_count, 1, __ATOMIC_SEQ_CST);
+ if (cnt) {
+ topic = NULL;
+ topic_free = NULL;
+ }
+ }
+
+ if (client->max_msg_size && PUBLISH_SP_SIZE + mqtt_ng_publish_size(topic, msg_len, topic_id) > client->max_msg_size) {
+ mws_error(client->log, "Message too big for server: %zu", msg_len);
+ return MQTT_NG_MSGGEN_MSG_TOO_BIG;
+ }
+
+ TRY_GENERATE_MESSAGE(mqtt_ng_generate_publish, client, topic, topic_free, msg, msg_free, msg_len, publish_flags, packet_id, topic_id);
+}
+
+static inline size_t mqtt_ng_subscribe_size(struct mqtt_sub *subs, size_t sub_count)
+{
+ size_t len = 2 /* Packet Identifier */ + 1 /* Properties Length TODO for now fixed 0 */;
+ len += sub_count * (2 /* topic filter string length */ + 1 /* [MQTT-3.8.3.1] Subscription Options Byte */);
+
+ for (size_t i = 0; i < sub_count; i++) {
+ len += strlen(subs[i].topic);
+ }
+ return len;
+}
+
+int mqtt_ng_generate_subscribe(struct transaction_buffer *trx_buf, mqtt_wss_log_ctx_t log_ctx, struct mqtt_sub *subs, size_t sub_count)
+{
+ // >> START THE RODEO <<
+ transaction_buffer_transaction_start(trx_buf);
+
+ // Calculate the resulting message size sans fixed MQTT header
+ size_t size = mqtt_ng_subscribe_size(subs, sub_count);
+
+ // Start generating the message
+ struct buffer_fragment *frag = NULL;
+ mqtt_msg_data ret = NULL;
+
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, BUFFER_FRAG_MQTT_PACKET_HEAD, frag, goto fail_rollback);
+ ret = frag;
+
+ // MQTT Fixed Header
+ size_t needed_bytes = 1 /* Packet type */ + MQTT_VARSIZE_INT_BYTES(size) + 3 /*Packet ID + Property Length*/;
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, needed_bytes, goto fail_rollback);
+
+ *WRITE_POS(frag) = (MQTT_CPT_SUBSCRIBE << 4) | 0x2 /* [MQTT-3.8.1-1] */;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+ DATA_ADVANCE(&trx_buf->hdr_buffer, uint32_to_mqtt_vbi(size, WRITE_POS(frag)), frag);
+
+ // MQTT Variable Header
+ // [MQTT-3.8.2] PacketID
+ ret->packet_id = get_unused_packet_id();
+ PACK_2B_INT(&trx_buf->hdr_buffer, ret->packet_id, frag);
+
+ // [MQTT-3.8.2.1.1] Property Length // TODO for now fixed 0
+ *WRITE_POS(frag) = 0;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+
+ for (size_t i = 0; i < sub_count; i++) {
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, 0, frag, goto fail_rollback);
+ PACK_2B_INT(&trx_buf->hdr_buffer, strlen(subs[i].topic), frag);
+ if (_optimized_add(&trx_buf->hdr_buffer, log_ctx, subs[i].topic, strlen(subs[i].topic), subs[i].topic_free, &frag))
+ goto fail_rollback;
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, 0, frag, goto fail_rollback);
+ *WRITE_POS(frag) = subs[i].options;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+ }
+
+ trx_buf->hdr_buffer.tail_frag->flags |= BUFFER_FRAG_MQTT_PACKET_TAIL;
+ transaction_buffer_transaction_commit(trx_buf);
+ return MQTT_NG_MSGGEN_OK;
+fail_rollback:
+ transaction_buffer_transaction_rollback(trx_buf, ret);
+ return MQTT_NG_MSGGEN_BUFFER_OOM;
+}
+
+int mqtt_ng_subscribe(struct mqtt_ng_client *client, struct mqtt_sub *subs, size_t sub_count)
+{
+ TRY_GENERATE_MESSAGE(mqtt_ng_generate_subscribe, client, subs, sub_count);
+}
+
+int mqtt_ng_generate_disconnect(struct transaction_buffer *trx_buf, mqtt_wss_log_ctx_t log_ctx, uint8_t reason_code)
+{
+ (void) log_ctx;
+ // >> START THE RODEO <<
+ transaction_buffer_transaction_start(trx_buf);
+
+ // Calculate the resulting message size sans fixed MQTT header
+ size_t size = reason_code ? 1 : 0;
+
+ // Start generating the message
+ struct buffer_fragment *frag = NULL;
+ mqtt_msg_data ret = NULL;
+
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, BUFFER_FRAG_MQTT_PACKET_HEAD, frag, goto fail_rollback);
+ ret = frag;
+
+ // MQTT Fixed Header
+ size_t needed_bytes = 1 /* Packet type */ + MQTT_VARSIZE_INT_BYTES(size) + (reason_code ? 1 : 0);
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, needed_bytes, goto fail_rollback);
+
+ *WRITE_POS(frag) = MQTT_CPT_DISCONNECT << 4;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+ DATA_ADVANCE(&trx_buf->hdr_buffer, uint32_to_mqtt_vbi(size, WRITE_POS(frag)), frag);
+
+ if (reason_code) {
+ // MQTT Variable Header
+ // [MQTT-3.14.2.1] PacketID
+ *WRITE_POS(frag) = reason_code;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+ }
+
+ trx_buf->hdr_buffer.tail_frag->flags |= BUFFER_FRAG_MQTT_PACKET_TAIL;
+ transaction_buffer_transaction_commit(trx_buf);
+ return MQTT_NG_MSGGEN_OK;
+fail_rollback:
+ transaction_buffer_transaction_rollback(trx_buf, ret);
+ return MQTT_NG_MSGGEN_BUFFER_OOM;
+}
+
+int mqtt_ng_disconnect(struct mqtt_ng_client *client, uint8_t reason_code)
+{
+ TRY_GENERATE_MESSAGE(mqtt_ng_generate_disconnect, client, reason_code);
+}
+
+static int mqtt_generate_puback(struct transaction_buffer *trx_buf, mqtt_wss_log_ctx_t log_ctx, uint16_t packet_id, uint8_t reason_code)
+{
+ (void) log_ctx;
+ // >> START THE RODEO <<
+ transaction_buffer_transaction_start(trx_buf);
+
+ // Calculate the resulting message size sans fixed MQTT header
+ size_t size = 2 /* Packet ID */ + (reason_code ? 1 : 0) /* reason code */;
+
+ // Start generating the message
+ struct buffer_fragment *frag = NULL;
+
+ BUFFER_TRANSACTION_NEW_FRAG(&trx_buf->hdr_buffer, BUFFER_FRAG_MQTT_PACKET_HEAD | BUFFER_FRAG_GARBAGE_COLLECT_ON_SEND, frag, goto fail_rollback);
+
+ // MQTT Fixed Header
+ size_t needed_bytes = 1 /* Packet type */ + MQTT_VARSIZE_INT_BYTES(size) + size;
+ CHECK_BYTES_AVAILABLE(&trx_buf->hdr_buffer, needed_bytes, goto fail_rollback);
+
+ *WRITE_POS(frag) = MQTT_CPT_PUBACK << 4;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+ DATA_ADVANCE(&trx_buf->hdr_buffer, uint32_to_mqtt_vbi(size, WRITE_POS(frag)), frag);
+
+ // MQTT Variable Header
+ PACK_2B_INT(&trx_buf->hdr_buffer, packet_id, frag);
+
+ if (reason_code) {
+ // MQTT Variable Header
+ // [MQTT-3.14.2.1] PacketID
+ *WRITE_POS(frag) = reason_code;
+ DATA_ADVANCE(&trx_buf->hdr_buffer, 1, frag);
+ }
+
+ trx_buf->hdr_buffer.tail_frag->flags |= BUFFER_FRAG_MQTT_PACKET_TAIL;
+ transaction_buffer_transaction_commit(trx_buf);
+ return MQTT_NG_MSGGEN_OK;
+fail_rollback:
+ transaction_buffer_transaction_rollback(trx_buf, frag);
+ return MQTT_NG_MSGGEN_BUFFER_OOM;
+}
+
+static int mqtt_ng_puback(struct mqtt_ng_client *client, uint16_t packet_id, uint8_t reason_code)
+{
+ TRY_GENERATE_MESSAGE(mqtt_generate_puback, client, packet_id, reason_code);
+}
+
+int mqtt_ng_ping(struct mqtt_ng_client *client)
+{
+ client->ping_pending = 1;
+ return MQTT_NG_MSGGEN_OK;
+}
+
+#define MQTT_NG_CLIENT_NEED_MORE_BYTES 0x10
+#define MQTT_NG_CLIENT_MQTT_PACKET_DONE 0x11
+#define MQTT_NG_CLIENT_PARSE_DONE 0x12
+#define MQTT_NG_CLIENT_WANT_WRITE 0x13
+#define MQTT_NG_CLIENT_OK_CALL_AGAIN 0
+#define MQTT_NG_CLIENT_PROTOCOL_ERROR -1
+#define MQTT_NG_CLIENT_SERVER_RETURNED_ERROR -2
+#define MQTT_NG_CLIENT_NOT_IMPL_YET -3
+#define MQTT_NG_CLIENT_OOM -4
+#define MQTT_NG_CLIENT_INTERNAL_ERROR -5
+
+#define BUF_READ_CHECK_AT_LEAST(buf, x) \
+ if (rbuf_bytes_available(buf) < (x)) \
+ return MQTT_NG_CLIENT_NEED_MORE_BYTES;
+
+#define vbi_parser_reset_ctx(ctx) memset(ctx, 0, sizeof(struct mqtt_vbi_parser_ctx))
+
+static int vbi_parser_parse(struct mqtt_vbi_parser_ctx *ctx, rbuf_t data, mqtt_wss_log_ctx_t log)
+{
+ if (ctx->bytes > MQTT_VBI_MAXBYTES - 1) {
+ mws_error(log, "MQTT Variable Byte Integer can't be longer than %d bytes", MQTT_VBI_MAXBYTES);
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ }
+ if (!ctx->bytes || ctx->data[ctx->bytes-1] & MQTT_VBI_CONTINUATION_FLAG) {
+ BUF_READ_CHECK_AT_LEAST(data, 1);
+ ctx->bytes++;
+ rbuf_pop(data, &ctx->data[ctx->bytes-1], 1);
+ if ( ctx->data[ctx->bytes-1] & MQTT_VBI_CONTINUATION_FLAG )
+ return MQTT_NG_CLIENT_OK_CALL_AGAIN;
+ }
+
+ if (mqtt_vbi_to_uint32(ctx->data, &ctx->result)) {
+ mws_error(log, "MQTT Variable Byte Integer failed to be parsed.");
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ }
+
+ return MQTT_NG_CLIENT_PARSE_DONE;
+}
+
+static void mqtt_properties_parser_ctx_reset(struct mqtt_properties_parser_ctx *ctx)
+{
+ ctx->state = PROPERTIES_LENGTH;
+ while (ctx->head) {
+ struct mqtt_property *f = ctx->head;
+ ctx->head = ctx->head->next;
+ if (f->type == MQTT_TYPE_STR || f->type == MQTT_TYPE_STR_PAIR)
+ mw_free(f->data.strings[0]);
+ if (f->type == MQTT_TYPE_STR_PAIR)
+ mw_free(f->data.strings[1]);
+ if (f->type == MQTT_TYPE_BIN)
+ mw_free(f->data.bindata);
+ mw_free(f);
+ }
+ ctx->tail = NULL;
+ ctx->properties_length = 0;
+ ctx->bytes_consumed = 0;
+ vbi_parser_reset_ctx(&ctx->vbi_parser_ctx);
+}
+
+struct mqtt_property_type {
+ uint8_t id;
+ enum mqtt_datatype datatype;
+ const char* name;
+};
+
+const struct mqtt_property_type mqtt_property_types[] = {
+ { .id = MQTT_PROP_TOPIC_ALIAS, .name = MQTT_PROP_TOPIC_ALIAS_NAME, .datatype = MQTT_TYPE_UINT_16 },
+
+ { .id = MQTT_PROP_PAYLOAD_FMT_INDICATOR, .name = MQTT_PROP_PAYLOAD_FMT_INDICATOR_NAME, .datatype = MQTT_TYPE_UINT_8 },
+ { .id = MQTT_PROP_MSG_EXPIRY_INTERVAL, .name = MQTT_PROP_MSG_EXPIRY_INTERVAL_NAME, .datatype = MQTT_TYPE_UINT_32 },
+ { .id = MQTT_PROP_CONTENT_TYPE, .name = MQTT_PROP_CONTENT_TYPE_NAME, .datatype = MQTT_TYPE_STR },
+ { .id = MQTT_PROP_RESPONSE_TOPIC, .name = MQTT_PROP_RESPONSE_TOPIC_NAME, .datatype = MQTT_TYPE_STR },
+ { .id = MQTT_PROP_CORRELATION_DATA, .name = MQTT_PROP_CORRELATION_DATA_NAME, .datatype = MQTT_TYPE_BIN },
+ { .id = MQTT_PROP_SUB_IDENTIFIER, .name = MQTT_PROP_SUB_IDENTIFIER_NAME, .datatype = MQTT_TYPE_VBI },
+ { .id = MQTT_PROP_SESSION_EXPIRY_INTERVAL, .name = MQTT_PROP_SESSION_EXPIRY_INTERVAL_NAME, .datatype = MQTT_TYPE_UINT_32 },
+ { .id = MQTT_PROP_ASSIGNED_CLIENT_ID, .name = MQTT_PROP_ASSIGNED_CLIENT_ID_NAME, .datatype = MQTT_TYPE_STR },
+ { .id = MQTT_PROP_SERVER_KEEP_ALIVE, .name = MQTT_PROP_SERVER_KEEP_ALIVE_NAME, .datatype = MQTT_TYPE_UINT_16 },
+ { .id = MQTT_PROP_AUTH_METHOD, .name = MQTT_PROP_AUTH_METHOD_NAME, .datatype = MQTT_TYPE_STR },
+ { .id = MQTT_PROP_AUTH_DATA, .name = MQTT_PROP_AUTH_DATA_NAME, .datatype = MQTT_TYPE_BIN },
+ { .id = MQTT_PROP_REQ_PROBLEM_INFO, .name = MQTT_PROP_REQ_PROBLEM_INFO_NAME, .datatype = MQTT_TYPE_UINT_8 },
+ { .id = MQTT_PROP_WILL_DELAY_INTERVAL, .name = MQTT_PROP_WIIL_DELAY_INTERVAL_NAME, .datatype = MQTT_TYPE_UINT_32 },
+ { .id = MQTT_PROP_REQ_RESP_INFORMATION, .name = MQTT_PROP_REQ_RESP_INFORMATION_NAME, .datatype = MQTT_TYPE_UINT_8 },
+ { .id = MQTT_PROP_RESP_INFORMATION, .name = MQTT_PROP_RESP_INFORMATION_NAME, .datatype = MQTT_TYPE_STR },
+ { .id = MQTT_PROP_SERVER_REF, .name = MQTT_PROP_SERVER_REF_NAME, .datatype = MQTT_TYPE_STR },
+ { .id = MQTT_PROP_REASON_STR, .name = MQTT_PROP_REASON_STR_NAME, .datatype = MQTT_TYPE_STR },
+ { .id = MQTT_PROP_RECEIVE_MAX, .name = MQTT_PROP_RECEIVE_MAX_NAME, .datatype = MQTT_TYPE_UINT_16 },
+ { .id = MQTT_PROP_TOPIC_ALIAS_MAX, .name = MQTT_PROP_TOPIC_ALIAS_MAX_NAME, .datatype = MQTT_TYPE_UINT_16 },
+ // MQTT_PROP_TOPIC_ALIAS is first as it is most often used
+ { .id = MQTT_PROP_MAX_QOS, .name = MQTT_PROP_MAX_QOS_NAME, .datatype = MQTT_TYPE_UINT_8 },
+ { .id = MQTT_PROP_RETAIN_AVAIL, .name = MQTT_PROP_RETAIN_AVAIL_NAME, .datatype = MQTT_TYPE_UINT_8 },
+ { .id = MQTT_PROP_USR, .name = MQTT_PROP_USR_NAME, .datatype = MQTT_TYPE_STR_PAIR },
+ { .id = MQTT_PROP_MAX_PKT_SIZE, .name = MQTT_PROP_MAX_PKT_SIZE_NAME, .datatype = MQTT_TYPE_UINT_32 },
+ { .id = MQTT_PROP_WILDCARD_SUB_AVAIL, .name = MQTT_PROP_WILDCARD_SUB_AVAIL_NAME, .datatype = MQTT_TYPE_UINT_8 },
+ { .id = MQTT_PROP_SUB_ID_AVAIL, .name = MQTT_PROP_SUB_ID_AVAIL_NAME, .datatype = MQTT_TYPE_UINT_8 },
+ { .id = MQTT_PROP_SHARED_SUB_AVAIL, .name = MQTT_PROP_SHARED_SUB_AVAIL_NAME, .datatype = MQTT_TYPE_UINT_8 },
+ { .id = 0, .name = NULL, .datatype = MQTT_TYPE_UNKNOWN }
+};
+
+static int get_property_type_by_id(uint8_t property_id) {
+ for (int i = 0; mqtt_property_types[i].datatype != MQTT_TYPE_UNKNOWN; i++) {
+ if (mqtt_property_types[i].id == property_id)
+ return mqtt_property_types[i].datatype;
+ }
+ return MQTT_TYPE_UNKNOWN;
+}
+
+struct mqtt_property *get_property_by_id(struct mqtt_property *props, uint8_t property_id)
+{
+ while (props) {
+ if (props->id == property_id) {
+ return props;
+ }
+ props = props->next;
+ }
+ return NULL;
+}
+
+// Parses [MQTT-2.2.2]
+static int parse_properties_array(struct mqtt_properties_parser_ctx *ctx, rbuf_t data, mqtt_wss_log_ctx_t log)
+{
+ int rc;
+ switch (ctx->state) {
+ case PROPERTIES_LENGTH:
+ rc = vbi_parser_parse(&ctx->vbi_parser_ctx, data, log);
+ if (rc == MQTT_NG_CLIENT_PARSE_DONE) {
+ ctx->properties_length = ctx->vbi_parser_ctx.result;
+ ctx->bytes_consumed += ctx->vbi_parser_ctx.bytes;
+ ctx->vbi_length = ctx->vbi_parser_ctx.bytes;
+ if (!ctx->properties_length)
+ return MQTT_NG_CLIENT_PARSE_DONE;
+ ctx->state = PROPERTY_CREATE;
+ break;
+ }
+ return rc;
+ case PROPERTY_CREATE:
+ BUF_READ_CHECK_AT_LEAST(data, 1);
+ struct mqtt_property *prop = mw_calloc(1, sizeof(struct mqtt_property));
+ if (ctx->head == NULL) {
+ ctx->head = prop;
+ ctx->tail = prop;
+ } else {
+ ctx->tail->next = prop;
+ ctx->tail = ctx->tail->next;
+ }
+ ctx->state = PROPERTY_ID;
+ /* FALLTHROUGH */
+ case PROPERTY_ID:
+ rbuf_pop(data, (char*)&ctx->tail->id, 1);
+ ctx->bytes_consumed += 1;
+ ctx->tail->type = get_property_type_by_id(ctx->tail->id);
+ switch (ctx->tail->type) {
+ case MQTT_TYPE_UINT_16:
+ ctx->state = PROPERTY_TYPE_UINT16;
+ break;
+ case MQTT_TYPE_UINT_32:
+ ctx->state = PROPERTY_TYPE_UINT32;
+ break;
+ case MQTT_TYPE_UINT_8:
+ ctx->state = PROPERTY_TYPE_UINT8;
+ break;
+ case MQTT_TYPE_VBI:
+ ctx->state = PROPERTY_TYPE_VBI;
+ vbi_parser_reset_ctx(&ctx->vbi_parser_ctx);
+ break;
+ case MQTT_TYPE_STR:
+ case MQTT_TYPE_STR_PAIR:
+ ctx->str_idx = 0;
+ /* FALLTHROUGH */
+ case MQTT_TYPE_BIN:
+ ctx->state = PROPERTY_TYPE_STR_BIN_LEN;
+ break;
+ default:
+ mws_error(log, "Unsupported property type %d for property id %d.", (int)ctx->tail->type, (int)ctx->tail->id);
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ }
+ break;
+ case PROPERTY_TYPE_STR_BIN_LEN:
+ BUF_READ_CHECK_AT_LEAST(data, sizeof(uint16_t));
+ rbuf_pop(data, (char*)&ctx->tail->bindata_len, sizeof(uint16_t));
+ ctx->tail->bindata_len = be16toh(ctx->tail->bindata_len);
+ ctx->bytes_consumed += 2;
+ switch (ctx->tail->type) {
+ case MQTT_TYPE_BIN:
+ ctx->state = PROPERTY_TYPE_BIN;
+ break;
+ case MQTT_TYPE_STR:
+ case MQTT_TYPE_STR_PAIR:
+ ctx->state = PROPERTY_TYPE_STR;
+ break;
+ default:
+ mws_error(log, "Unexpected datatype in PROPERTY_TYPE_STR_BIN_LEN %d", (int)ctx->tail->type);
+ return MQTT_NG_CLIENT_INTERNAL_ERROR;
+ }
+ break;
+ case PROPERTY_TYPE_STR:
+ BUF_READ_CHECK_AT_LEAST(data, ctx->tail->bindata_len);
+ ctx->tail->data.strings[ctx->str_idx] = mw_malloc(ctx->tail->bindata_len + 1);
+ rbuf_pop(data, ctx->tail->data.strings[ctx->str_idx], ctx->tail->bindata_len);
+ ctx->tail->data.strings[ctx->str_idx][ctx->tail->bindata_len] = 0;
+ ctx->str_idx++;
+ ctx->bytes_consumed += ctx->tail->bindata_len;
+ if (ctx->tail->type == MQTT_TYPE_STR_PAIR && ctx->str_idx < 2) {
+ ctx->state = PROPERTY_TYPE_STR_BIN_LEN;
+ break;
+ }
+ ctx->state = PROPERTY_NEXT;
+ break;
+ case PROPERTY_TYPE_BIN:
+ BUF_READ_CHECK_AT_LEAST(data, ctx->tail->bindata_len);
+ ctx->tail->data.bindata = mw_malloc(ctx->tail->bindata_len);
+ rbuf_pop(data, ctx->tail->data.bindata, ctx->tail->bindata_len);
+ ctx->bytes_consumed += ctx->tail->bindata_len;
+ ctx->state = PROPERTY_NEXT;
+ break;
+ case PROPERTY_TYPE_VBI:
+ rc = vbi_parser_parse(&ctx->vbi_parser_ctx, data, log);
+ if (rc == MQTT_NG_CLIENT_PARSE_DONE) {
+ ctx->tail->data.uint32 = ctx->vbi_parser_ctx.result;
+ ctx->bytes_consumed += ctx->vbi_parser_ctx.bytes;
+ ctx->state = PROPERTY_NEXT;
+ break;
+ }
+ return rc;
+ case PROPERTY_TYPE_UINT8:
+ BUF_READ_CHECK_AT_LEAST(data, sizeof(uint8_t));
+ rbuf_pop(data, (char*)&ctx->tail->data.uint8, sizeof(uint8_t));
+ ctx->bytes_consumed += sizeof(uint8_t);
+ ctx->state = PROPERTY_NEXT;
+ break;
+ case PROPERTY_TYPE_UINT32:
+ BUF_READ_CHECK_AT_LEAST(data, sizeof(uint32_t));
+ rbuf_pop(data, (char*)&ctx->tail->data.uint32, sizeof(uint32_t));
+ ctx->tail->data.uint32 = be32toh(ctx->tail->data.uint32);
+ ctx->bytes_consumed += sizeof(uint32_t);
+ ctx->state = PROPERTY_NEXT;
+ break;
+ case PROPERTY_TYPE_UINT16:
+ BUF_READ_CHECK_AT_LEAST(data, sizeof(uint16_t));
+ rbuf_pop(data, (char*)&ctx->tail->data.uint16, sizeof(uint16_t));
+ ctx->tail->data.uint16 = be16toh(ctx->tail->data.uint16);
+ ctx->bytes_consumed += sizeof(uint16_t);
+ ctx->state = PROPERTY_NEXT;
+ /* FALLTHROUGH */
+ case PROPERTY_NEXT:
+ if (ctx->properties_length > ctx->bytes_consumed - ctx->vbi_length) {
+ ctx->state = PROPERTY_CREATE;
+ break;
+ } else
+ return MQTT_NG_CLIENT_PARSE_DONE;
+ }
+ return MQTT_NG_CLIENT_OK_CALL_AGAIN;
+}
+
+static int parse_connack_varhdr(struct mqtt_ng_client *client)
+{
+ struct mqtt_ng_parser *parser = &client->parser;
+ switch (parser->varhdr_state) {
+ case MQTT_PARSE_VARHDR_INITIAL:
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, 2);
+ rbuf_pop(parser->received_data, (char*)&parser->mqtt_packet.connack.flags, 1);
+ rbuf_pop(parser->received_data, (char*)&parser->mqtt_packet.connack.reason_code, 1);
+ parser->varhdr_state = MQTT_PARSE_VARHDR_PROPS;
+ mqtt_properties_parser_ctx_reset(&parser->properties_parser);
+ break;
+ case MQTT_PARSE_VARHDR_PROPS:
+ return parse_properties_array(&parser->properties_parser, parser->received_data, client->log);
+ default:
+ ERROR("invalid state for connack varhdr parser");
+ return MQTT_NG_CLIENT_INTERNAL_ERROR;
+ }
+ return MQTT_NG_CLIENT_OK_CALL_AGAIN;
+}
+
+static int parse_disconnect_varhdr(struct mqtt_ng_client *client)
+{
+ struct mqtt_ng_parser *parser = &client->parser;
+ switch (parser->varhdr_state) {
+ case MQTT_PARSE_VARHDR_INITIAL:
+ if (!parser->mqtt_fixed_hdr_remaining_length) {
+ // [MQTT-3.14.2.1] if reason code omitted act same as == 0
+ parser->mqtt_packet.disconnect.reason_code = 0;
+ return MQTT_NG_CLIENT_PARSE_DONE;
+ }
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, 1);
+ rbuf_pop(parser->received_data, (char*)&parser->mqtt_packet.connack.reason_code, 1);
+ if (parser->mqtt_fixed_hdr_remaining_length == 1)
+ return MQTT_NG_CLIENT_PARSE_DONE;
+ parser->varhdr_state = MQTT_PARSE_VARHDR_PROPS;
+ mqtt_properties_parser_ctx_reset(&parser->properties_parser);
+ break;
+ case MQTT_PARSE_VARHDR_PROPS:
+ return parse_properties_array(&parser->properties_parser, parser->received_data, client->log);
+ default:
+ ERROR("invalid state for connack varhdr parser");
+ return MQTT_NG_CLIENT_INTERNAL_ERROR;
+ }
+ return MQTT_NG_CLIENT_OK_CALL_AGAIN;
+}
+
+static int parse_puback_varhdr(struct mqtt_ng_client *client)
+{
+ struct mqtt_ng_parser *parser = &client->parser;
+ switch (parser->varhdr_state) {
+ case MQTT_PARSE_VARHDR_INITIAL:
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, 2);
+ rbuf_pop(parser->received_data, (char*)&parser->mqtt_packet.puback.packet_id, 2);
+ parser->mqtt_packet.puback.packet_id = be16toh(parser->mqtt_packet.puback.packet_id);
+ if (parser->mqtt_fixed_hdr_remaining_length < 3) {
+ // [MQTT-3.4.2.1] if length is not big enough for reason code
+ // it is omitted and handled same as if it was present and == 0
+ // initially missed this detail and was wondering WTF is going on (sigh)
+ parser->mqtt_packet.puback.reason_code = 0;
+ return MQTT_NG_CLIENT_PARSE_DONE;
+ }
+ parser->varhdr_state = MQTT_PARSE_VARHDR_OPTIONAL_REASON_CODE;
+ /* FALLTHROUGH */
+ case MQTT_PARSE_VARHDR_OPTIONAL_REASON_CODE:
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, 1);
+ rbuf_pop(parser->received_data, (char*)&parser->mqtt_packet.puback.reason_code, 1);
+ // LOL so in CONNACK you have to have 0 byte to
+ // signify empty properties list
+ // but in PUBACK it can be omitted if remaining length doesn't allow it (sigh)
+ if (parser->mqtt_fixed_hdr_remaining_length < 4)
+ return MQTT_NG_CLIENT_PARSE_DONE;
+
+ parser->varhdr_state = MQTT_PARSE_VARHDR_PROPS;
+ mqtt_properties_parser_ctx_reset(&parser->properties_parser);
+ /* FALLTHROUGH */
+ case MQTT_PARSE_VARHDR_PROPS:
+ return parse_properties_array(&parser->properties_parser, parser->received_data, client->log);
+ default:
+ ERROR("invalid state for puback varhdr parser");
+ return MQTT_NG_CLIENT_INTERNAL_ERROR;
+ }
+ return MQTT_NG_CLIENT_OK_CALL_AGAIN;
+}
+
+static int parse_suback_varhdr(struct mqtt_ng_client *client)
+{
+ int rc;
+ size_t avail;
+ struct mqtt_ng_parser *parser = &client->parser;
+ struct mqtt_suback *suback = &client->parser.mqtt_packet.suback;
+ switch (parser->varhdr_state) {
+ case MQTT_PARSE_VARHDR_INITIAL:
+ suback->reason_codes = NULL;
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, 2);
+ rbuf_pop(parser->received_data, (char*)&suback->packet_id, 2);
+ suback->packet_id = be16toh(suback->packet_id);
+ parser->varhdr_state = MQTT_PARSE_VARHDR_PROPS;
+ parser->mqtt_parsed_len = 2;
+ mqtt_properties_parser_ctx_reset(&parser->properties_parser);
+ /* FALLTHROUGH */
+ case MQTT_PARSE_VARHDR_PROPS:
+ rc = parse_properties_array(&parser->properties_parser, parser->received_data, client->log);
+ if (rc != MQTT_NG_CLIENT_PARSE_DONE)
+ return rc;
+ parser->mqtt_parsed_len += parser->properties_parser.bytes_consumed;
+ suback->reason_code_count = parser->mqtt_fixed_hdr_remaining_length - parser->mqtt_parsed_len;
+ suback->reason_codes = mw_calloc(suback->reason_code_count, sizeof(*suback->reason_codes));
+ suback->reason_codes_pending = suback->reason_code_count;
+ parser->varhdr_state = MQTT_PARSE_REASONCODES;
+ /* FALLTHROUGH */
+ case MQTT_PARSE_REASONCODES:
+ avail = rbuf_bytes_available(parser->received_data);
+ if (avail < 1)
+ return MQTT_NG_CLIENT_NEED_MORE_BYTES;
+
+ suback->reason_codes_pending -= rbuf_pop(parser->received_data, (char*)suback->reason_codes, MIN(suback->reason_codes_pending, avail));
+
+ if (!suback->reason_codes_pending)
+ return MQTT_NG_CLIENT_PARSE_DONE;
+
+ return MQTT_NG_CLIENT_NEED_MORE_BYTES;
+ default:
+ ERROR("invalid state for suback varhdr parser");
+ return MQTT_NG_CLIENT_INTERNAL_ERROR;
+ }
+ return MQTT_NG_CLIENT_OK_CALL_AGAIN;
+}
+
+static int parse_publish_varhdr(struct mqtt_ng_client *client)
+{
+ int rc;
+ struct mqtt_ng_parser *parser = &client->parser;
+ struct mqtt_publish *publish = &client->parser.mqtt_packet.publish;
+ switch (parser->varhdr_state) {
+ case MQTT_PARSE_VARHDR_INITIAL:
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, 2);
+ publish->topic = NULL;
+ publish->qos = ((parser->mqtt_control_packet_type >> 1) & 0x03);
+ rbuf_pop(parser->received_data, (char*)&publish->topic_len, 2);
+ publish->topic_len = be16toh(publish->topic_len);
+ parser->mqtt_parsed_len = 2;
+ if (!publish->topic_len) {
+ parser->varhdr_state = MQTT_PARSE_VARHDR_POST_TOPICNAME;
+ break;
+ }
+ publish->topic = mw_calloc(1, publish->topic_len + 1 /* add 0x00 */);
+ if (publish->topic == NULL)
+ return MQTT_NG_CLIENT_OOM;
+ parser->varhdr_state = MQTT_PARSE_VARHDR_TOPICNAME;
+ /* FALLTHROUGH */
+ case MQTT_PARSE_VARHDR_TOPICNAME:
+ // TODO check empty topic can be valid? In which case we have to skip this step
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, publish->topic_len);
+ rbuf_pop(parser->received_data, publish->topic, publish->topic_len);
+ parser->mqtt_parsed_len += publish->topic_len;
+ parser->varhdr_state = MQTT_PARSE_VARHDR_POST_TOPICNAME;
+ /* FALLTHROUGH */
+ case MQTT_PARSE_VARHDR_POST_TOPICNAME:
+ mqtt_properties_parser_ctx_reset(&parser->properties_parser);
+ if (!publish->qos) { // PacketID present only for QOS > 0 [MQTT-3.3.2.2]
+ parser->varhdr_state = MQTT_PARSE_VARHDR_PROPS;
+ break;
+ }
+ parser->varhdr_state = MQTT_PARSE_VARHDR_PACKET_ID;
+ /* FALLTHROUGH */
+ case MQTT_PARSE_VARHDR_PACKET_ID:
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, 2);
+ rbuf_pop(parser->received_data, (char*)&publish->packet_id, 2);
+ publish->packet_id = be16toh(publish->packet_id);
+ parser->varhdr_state = MQTT_PARSE_VARHDR_PROPS;
+ parser->mqtt_parsed_len += 2;
+ /* FALLTHROUGH */
+ case MQTT_PARSE_VARHDR_PROPS:
+ rc = parse_properties_array(&parser->properties_parser, parser->received_data, client->log);
+ if (rc != MQTT_NG_CLIENT_PARSE_DONE)
+ return rc;
+ parser->mqtt_parsed_len += parser->properties_parser.bytes_consumed;
+ parser->varhdr_state = MQTT_PARSE_PAYLOAD;
+ /* FALLTHROUGH */
+ case MQTT_PARSE_PAYLOAD:
+ if (parser->mqtt_fixed_hdr_remaining_length < parser->mqtt_parsed_len) {
+ mw_free(publish->topic);
+ publish->topic = NULL;
+ ERROR("Error parsing PUBLISH message");
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ }
+ publish->data_len = parser->mqtt_fixed_hdr_remaining_length - parser->mqtt_parsed_len;
+ if (!publish->data_len) {
+ publish->data = NULL;
+ return MQTT_NG_CLIENT_PARSE_DONE; // 0 length payload is OK [MQTT-3.3.3]
+ }
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, publish->data_len);
+
+ publish->data = mw_malloc(publish->data_len);
+ if (publish->data == NULL) {
+ mw_free(publish->topic);
+ publish->topic = NULL;
+ return MQTT_NG_CLIENT_OOM;
+ }
+
+ rbuf_pop(parser->received_data, publish->data, publish->data_len);
+ parser->mqtt_parsed_len += publish->data_len;
+
+ return MQTT_NG_CLIENT_PARSE_DONE;
+ default:
+ ERROR("invalid state for publish varhdr parser");
+ return MQTT_NG_CLIENT_INTERNAL_ERROR;
+ }
+ return MQTT_NG_CLIENT_OK_CALL_AGAIN;
+}
+
+// TODO move to separate file, dont send whole client pointer just to be able
+// to access LOG context send parser only which should include log
+static int parse_data(struct mqtt_ng_client *client)
+{
+ int rc;
+ struct mqtt_ng_parser *parser = &client->parser;
+ switch(parser->state) {
+ case MQTT_PARSE_FIXED_HEADER_PACKET_TYPE:
+ BUF_READ_CHECK_AT_LEAST(parser->received_data, 1);
+ rbuf_pop(parser->received_data, (char*)&parser->mqtt_control_packet_type, 1);
+ vbi_parser_reset_ctx(&parser->vbi_parser);
+ parser->state = MQTT_PARSE_FIXED_HEADER_LEN;
+ break;
+ case MQTT_PARSE_FIXED_HEADER_LEN:
+ rc = vbi_parser_parse(&parser->vbi_parser, parser->received_data, client->log);
+ if (rc == MQTT_NG_CLIENT_PARSE_DONE) {
+ parser->mqtt_fixed_hdr_remaining_length = parser->vbi_parser.result;
+ parser->state = MQTT_PARSE_VARIABLE_HEADER;
+ parser->varhdr_state = MQTT_PARSE_VARHDR_INITIAL;
+ break;
+ }
+ return rc;
+ case MQTT_PARSE_VARIABLE_HEADER:
+ switch (get_control_packet_type(parser->mqtt_control_packet_type)) {
+ case MQTT_CPT_CONNACK:
+ rc = parse_connack_varhdr(client);
+ if (rc == MQTT_NG_CLIENT_PARSE_DONE) {
+ parser->state = MQTT_PARSE_MQTT_PACKET_DONE;
+ break;
+ }
+ return rc;
+ case MQTT_CPT_PUBACK:
+ rc = parse_puback_varhdr(client);
+ if (rc == MQTT_NG_CLIENT_PARSE_DONE) {
+ parser->state = MQTT_PARSE_MQTT_PACKET_DONE;
+ break;
+ }
+ return rc;
+ case MQTT_CPT_SUBACK:
+ rc = parse_suback_varhdr(client);
+ if (rc != MQTT_NG_CLIENT_NEED_MORE_BYTES && rc != MQTT_NG_CLIENT_OK_CALL_AGAIN) {
+ mw_free(parser->mqtt_packet.suback.reason_codes);
+ }
+ if (rc == MQTT_NG_CLIENT_PARSE_DONE) {
+ parser->state = MQTT_PARSE_MQTT_PACKET_DONE;
+ break;
+ }
+ return rc;
+ case MQTT_CPT_PUBLISH:
+ rc = parse_publish_varhdr(client);
+ if (rc == MQTT_NG_CLIENT_PARSE_DONE) {
+ parser->state = MQTT_PARSE_MQTT_PACKET_DONE;
+ break;
+ }
+ return rc;
+ case MQTT_CPT_PINGRESP:
+ if (parser->mqtt_fixed_hdr_remaining_length) {
+ ERROR ("PINGRESP has to be 0 Remaining Length."); // [MQTT-3.13.1]
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ }
+ parser->state = MQTT_PARSE_MQTT_PACKET_DONE;
+ break;
+ case MQTT_CPT_DISCONNECT:
+ rc = parse_disconnect_varhdr(client);
+ if (rc == MQTT_NG_CLIENT_PARSE_DONE) {
+ parser->state = MQTT_PARSE_MQTT_PACKET_DONE;
+ break;
+ }
+ return rc;
+ default:
+ ERROR("Parsing Control Packet Type %" PRIu8 " not implemented yet.", get_control_packet_type(parser->mqtt_control_packet_type));
+ rbuf_bump_tail(parser->received_data, parser->mqtt_fixed_hdr_remaining_length);
+ parser->state = MQTT_PARSE_MQTT_PACKET_DONE;
+ return MQTT_NG_CLIENT_NOT_IMPL_YET;
+ }
+ // we could also return MQTT_NG_CLIENT_OK_CALL_AGAIN
+ // and be called again later
+ /* FALLTHROUGH */
+ case MQTT_PARSE_MQTT_PACKET_DONE:
+ parser->state = MQTT_PARSE_FIXED_HEADER_PACKET_TYPE;
+ return MQTT_NG_CLIENT_MQTT_PACKET_DONE;
+ }
+ return MQTT_NG_CLIENT_OK_CALL_AGAIN;
+}
+
+// set next MQTT fragment to send
+// return 1 if nothing to send
+// return -1 on error
+// return 0 if there is fragment set
+static int mqtt_ng_next_to_send(struct mqtt_ng_client *client) {
+ if (client->client_state == CONNECT_PENDING) {
+ client->main_buffer.sending_frag = client->connect_msg;
+ client->client_state = CONNECTING;
+ return 0;
+ }
+ if (client->client_state != CONNECTED)
+ return -1;
+
+ struct buffer_fragment *frag = BUFFER_FIRST_FRAG(&client->main_buffer.hdr_buffer);
+ while (frag) {
+ if ( frag->sent != frag->len )
+ break;
+ frag = frag->next;
+ }
+
+ if ( client->ping_pending && (!frag || (frag->flags & BUFFER_FRAG_MQTT_PACKET_HEAD && frag->sent == 0)) ) {
+ client->ping_pending = 0;
+ ping_frag.sent = 0;
+ client->main_buffer.sending_frag = &ping_frag;
+ return 0;
+ }
+
+ client->main_buffer.sending_frag = frag;
+ return frag == NULL ? 1 : 0;
+}
+
+// send current fragment
+// return 0 if whole remaining length could be sent as a whole
+// return -1 if send buffer was filled and
+// nothing could be written anymore
+// return 1 if last fragment of a message was fully sent
+static int send_fragment(struct mqtt_ng_client *client) {
+ struct buffer_fragment *frag = client->main_buffer.sending_frag;
+
+ // for readability
+ unsigned char *ptr = frag->data + frag->sent;
+ size_t bytes = frag->len - frag->sent;
+
+ size_t processed = 0;
+
+ if (bytes)
+ processed = client->send_fnc_ptr(client->user_ctx, ptr, bytes);
+ else
+ WARN("This fragment was fully sent already. This should not happen!");
+
+ frag->sent += processed;
+ if (frag->sent != frag->len)
+ return -1;
+
+ if (frag->flags & BUFFER_FRAG_MQTT_PACKET_TAIL) {
+ client->time_of_last_send = time(NULL);
+ pthread_mutex_lock(&client->stats_mutex);
+ if (client->main_buffer.sending_frag != &ping_frag)
+ client->stats.tx_messages_queued--;
+ client->stats.tx_messages_sent++;
+ pthread_mutex_unlock(&client->stats_mutex);
+ client->main_buffer.sending_frag = NULL;
+ return 1;
+ }
+
+ client->main_buffer.sending_frag = frag->next;
+
+ return 0;
+}
+
+// attempt sending all fragments of current single MQTT packet
+static int send_all_message_fragments(struct mqtt_ng_client *client) {
+ int rc;
+ while ( !(rc = send_fragment(client)) );
+ return rc;
+}
+
+static void try_send_all(struct mqtt_ng_client *client) {
+ do {
+ if (client->main_buffer.sending_frag == NULL && mqtt_ng_next_to_send(client))
+ return;
+ } while(send_all_message_fragments(client) >= 0);
+}
+
+static inline void mark_message_for_gc(struct buffer_fragment *frag)
+{
+ while (frag) {
+ frag->flags |= BUFFER_FRAG_GARBAGE_COLLECT;
+ buffer_frag_free_data(frag);
+ if (frag->flags & BUFFER_FRAG_MQTT_PACKET_TAIL)
+ return;
+ frag = frag->next;
+ }
+}
+
+static int mark_packet_acked(struct mqtt_ng_client *client, uint16_t packet_id)
+{
+ LOCK_HDR_BUFFER(&client->main_buffer);
+ struct buffer_fragment *frag = BUFFER_FIRST_FRAG(&client->main_buffer.hdr_buffer);
+ while (frag) {
+ if ( (frag->flags & BUFFER_FRAG_MQTT_PACKET_HEAD) && frag->packet_id == packet_id) {
+ if (!frag->sent) {
+ ERROR("Received packet_id (%" PRIu16 ") belongs to MQTT packet which was not yet sent!", packet_id);
+ UNLOCK_HDR_BUFFER(&client->main_buffer);
+ return 1;
+ }
+ mark_message_for_gc(frag);
+ UNLOCK_HDR_BUFFER(&client->main_buffer);
+ return 0;
+ }
+ frag = frag->next;
+ }
+ ERROR("Received packet_id (%" PRIu16 ") is unknown!", packet_id);
+ UNLOCK_HDR_BUFFER(&client->main_buffer);
+ return 1;
+}
+
+int handle_incoming_traffic(struct mqtt_ng_client *client)
+{
+ int rc;
+ struct mqtt_publish *pub;
+ while( (rc = parse_data(client)) == MQTT_NG_CLIENT_OK_CALL_AGAIN );
+ if ( rc == MQTT_NG_CLIENT_MQTT_PACKET_DONE ) {
+ struct mqtt_property *prop;
+#ifdef MQTT_DEBUG_VERBOSE
+ DEBUG("MQTT Packet Parsed Successfully!");
+#endif
+ pthread_mutex_lock(&client->stats_mutex);
+ client->stats.rx_messages_rcvd++;
+ pthread_mutex_unlock(&client->stats_mutex);
+
+ switch (get_control_packet_type(client->parser.mqtt_control_packet_type)) {
+ case MQTT_CPT_CONNACK:
+#ifdef MQTT_DEBUG_VERBOSE
+ DEBUG("Received CONNACK");
+#endif
+ LOCK_HDR_BUFFER(&client->main_buffer);
+ mark_message_for_gc(client->connect_msg);
+ UNLOCK_HDR_BUFFER(&client->main_buffer);
+ client->connect_msg = NULL;
+ if (client->client_state != CONNECTING) {
+ ERROR("Received unexpected CONNACK");
+ client->client_state = ERROR;
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ }
+ if ((prop = get_property_by_id(client->parser.properties_parser.head, MQTT_PROP_MAX_PKT_SIZE)) != NULL) {
+ INFO("MQTT server limits message size to %" PRIu32, prop->data.uint32);
+ client->max_msg_size = prop->data.uint32;
+ }
+ if (client->connack_callback)
+ client->connack_callback(client->user_ctx, client->parser.mqtt_packet.connack.reason_code);
+ if (!client->parser.mqtt_packet.connack.reason_code) {
+ INFO("MQTT Connection Accepted By Server");
+ client->client_state = CONNECTED;
+ break;
+ }
+ client->client_state = ERROR;
+ return MQTT_NG_CLIENT_SERVER_RETURNED_ERROR;
+ case MQTT_CPT_PUBACK:
+#ifdef MQTT_DEBUG_VERBOSE
+ DEBUG("Received PUBACK %" PRIu16, client->parser.mqtt_packet.puback.packet_id);
+#endif
+ if (mark_packet_acked(client, client->parser.mqtt_packet.puback.packet_id))
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ if (client->puback_callback)
+ client->puback_callback(client->parser.mqtt_packet.puback.packet_id);
+ break;
+ case MQTT_CPT_PINGRESP:
+#ifdef MQTT_DEBUG_VERBOSE
+ DEBUG("Received PINGRESP");
+#endif
+ break;
+ case MQTT_CPT_SUBACK:
+#ifdef MQTT_DEBUG_VERBOSE
+ DEBUG("Received SUBACK %" PRIu16, client->parser.mqtt_packet.suback.packet_id);
+#endif
+ if (mark_packet_acked(client, client->parser.mqtt_packet.suback.packet_id))
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ break;
+ case MQTT_CPT_PUBLISH:
+#ifdef MQTT_DEBUG_VERBOSE
+ DEBUG("Recevied PUBLISH");
+#endif
+ pub = &client->parser.mqtt_packet.publish;
+ if (pub->qos > 1) {
+ mw_free(pub->topic);
+ mw_free(pub->data);
+ return MQTT_NG_CLIENT_NOT_IMPL_YET;
+ }
+ if ( pub->qos == 1 && (rc = mqtt_ng_puback(client, pub->packet_id, 0)) ) {
+ client->client_state = ERROR;
+ ERROR("Error generating PUBACK reply for PUBLISH");
+ return rc;
+ }
+ if ( (prop = get_property_by_id(client->parser.properties_parser.head, MQTT_PROP_TOPIC_ALIAS)) != NULL ) {
+ // Topic Alias property was sent from server
+ void *topic_ptr;
+ if (!c_rhash_get_ptr_by_uint64(client->rx_aliases, prop->data.uint8, &topic_ptr)) {
+ if (pub->topic != NULL) {
+ ERROR("We do not yet support topic alias reassignment");
+ return MQTT_NG_CLIENT_NOT_IMPL_YET;
+ }
+ pub->topic = topic_ptr;
+ } else {
+ if (pub->topic == NULL) {
+ ERROR("Topic alias with id %d unknown and topic not set by server!", prop->data.uint8);
+ return MQTT_NG_CLIENT_PROTOCOL_ERROR;
+ }
+ c_rhash_insert_uint64_ptr(client->rx_aliases, prop->data.uint8, pub->topic);
+ }
+ }
+ if (client->msg_callback)
+ client->msg_callback(pub->topic, pub->data, pub->data_len, pub->qos);
+ // in case we have property topic alias and we have topic we take over the string
+ // and add pointer to it into topic alias list
+ if (prop == NULL)
+ mw_free(pub->topic);
+ mw_free(pub->data);
+ return MQTT_NG_CLIENT_WANT_WRITE;
+ case MQTT_CPT_DISCONNECT:
+ INFO ("Got MQTT DISCONNECT control packet from server. Reason code: %d", (int)client->parser.mqtt_packet.disconnect.reason_code);
+ client->client_state = DISCONNECTED;
+ break;
+ }
+ }
+
+ return rc;
+}
+
+int mqtt_ng_sync(struct mqtt_ng_client *client)
+{
+ if (client->client_state == RAW || client->client_state == DISCONNECTED)
+ return 0;
+
+ if (client->client_state == ERROR)
+ return 1;
+
+ LOCK_HDR_BUFFER(&client->main_buffer);
+ try_send_all(client);
+ UNLOCK_HDR_BUFFER(&client->main_buffer);
+
+ int rc;
+
+ while ((rc = handle_incoming_traffic(client)) != MQTT_NG_CLIENT_NEED_MORE_BYTES) {
+ if (rc < 0)
+ break;
+ if (rc == MQTT_NG_CLIENT_WANT_WRITE) {
+ LOCK_HDR_BUFFER(&client->main_buffer);
+ try_send_all(client);
+ UNLOCK_HDR_BUFFER(&client->main_buffer);
+ }
+ }
+
+ if (rc < 0)
+ return rc;
+
+ return 0;
+}
+
+time_t mqtt_ng_last_send_time(struct mqtt_ng_client *client)
+{
+ return client->time_of_last_send;
+}
+
+void mqtt_ng_set_max_mem(struct mqtt_ng_client *client, size_t bytes)
+{
+ client->max_mem_bytes = bytes;
+}
+
+void mqtt_ng_get_stats(struct mqtt_ng_client *client, struct mqtt_ng_stats *stats)
+{
+ pthread_mutex_lock(&client->stats_mutex);
+ memcpy(stats, &client->stats, sizeof(struct mqtt_ng_stats));
+ pthread_mutex_unlock(&client->stats_mutex);
+
+ stats->tx_bytes_queued = 0;
+ stats->tx_buffer_reclaimable = 0;
+
+ LOCK_HDR_BUFFER(&client->main_buffer);
+ stats->tx_buffer_used = BUFFER_BYTES_USED(&client->main_buffer.hdr_buffer);
+ stats->tx_buffer_free = BUFFER_BYTES_AVAILABLE(&client->main_buffer.hdr_buffer);
+ stats->tx_buffer_size = client->main_buffer.hdr_buffer.size;
+ struct buffer_fragment *frag = BUFFER_FIRST_FRAG(&client->main_buffer.hdr_buffer);
+ while (frag) {
+ stats->tx_bytes_queued += frag->len - frag->sent;
+ if (frag_is_marked_for_gc(frag))
+ stats->tx_buffer_reclaimable += FRAG_SIZE_IN_BUFFER(frag);
+
+ frag = frag->next;
+ }
+ UNLOCK_HDR_BUFFER(&client->main_buffer);
+}
+
+int mqtt_ng_set_topic_alias(struct mqtt_ng_client *client, const char *topic)
+{
+ uint16_t idx;
+ pthread_rwlock_wrlock(&client->tx_topic_aliases.rwlock);
+
+ if (client->tx_topic_aliases.idx_assigned >= client->tx_topic_aliases.idx_max) {
+ pthread_rwlock_unlock(&client->tx_topic_aliases.rwlock);
+ mws_error(client->log, "Tx topic alias indexes were exhausted (current version of the library doesn't support reassigning yet. Feel free to contribute.");
+ return 0; //0 is not a valid topic alias
+ }
+
+ struct topic_alias_data *alias;
+ if (!c_rhash_get_ptr_by_str(client->tx_topic_aliases.stoi_dict, topic, (void**)&alias)) {
+ // this is not a problem for library but might be helpful to warn user
+ // as it might indicate bug in their program (but also might be expected)
+ idx = alias->idx;
+ pthread_rwlock_unlock(&client->tx_topic_aliases.rwlock);
+ mws_debug(client->log, "%s topic \"%s\" already has alias set. Ignoring.", __FUNCTION__, topic);
+ return idx;
+ }
+
+ alias = mw_malloc(sizeof(struct topic_alias_data));
+ idx = ++client->tx_topic_aliases.idx_assigned;
+ alias->idx = idx;
+ __atomic_store_n(&alias->usage_count, 0, __ATOMIC_SEQ_CST);
+
+ c_rhash_insert_str_ptr(client->tx_topic_aliases.stoi_dict, topic, (void*)alias);
+
+ pthread_rwlock_unlock(&client->tx_topic_aliases.rwlock);
+ return idx;
+}
diff --git a/src/aclk/mqtt_websockets/mqtt_ng.h b/src/aclk/mqtt_websockets/mqtt_ng.h
new file mode 100644
index 000000000..4b0584d58
--- /dev/null
+++ b/src/aclk/mqtt_websockets/mqtt_ng.h
@@ -0,0 +1,99 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include <stdint.h>
+#include <sys/types.h>
+#include <time.h>
+
+#include "c-rbuf/cringbuffer.h"
+#include "common_public.h"
+
+#define MQTT_NG_MSGGEN_OK 0
+// MQTT_NG_MSGGEN_USER_ERROR means parameters given to this function
+// do not make sense or are out of MQTT specs
+#define MQTT_NG_MSGGEN_USER_ERROR 1
+#define MQTT_NG_MSGGEN_BUFFER_OOM 2
+#define MQTT_NG_MSGGEN_MSG_TOO_BIG 3
+
+struct mqtt_ng_client;
+
+/* Converts integer to MQTT Variable Byte Integer as per 1.5.5 of MQTT 5 specs
+ * @param input value to be converted
+ * @param output pointer to memory where output will be written to. Must allow up to 4 bytes to be written.
+ * @return number of bytes written to output or <= 0 if error in which case contents of output are undefined
+ */
+int uint32_to_mqtt_vbi(uint32_t input, unsigned char *output);
+
+struct mqtt_lwt_properties {
+ char *will_topic;
+ free_fnc_t will_topic_free;
+
+ void *will_message;
+ free_fnc_t will_message_free;
+ size_t will_message_size;
+
+ int will_qos;
+ int will_retain;
+};
+
+struct mqtt_auth_properties {
+ char *client_id;
+ free_fnc_t client_id_free;
+ char *username;
+ free_fnc_t username_free;
+ char *password;
+ free_fnc_t password_free;
+};
+
+int mqtt_ng_connect(struct mqtt_ng_client *client,
+ struct mqtt_auth_properties *auth,
+ struct mqtt_lwt_properties *lwt,
+ uint8_t clean_start,
+ uint16_t keep_alive);
+
+int mqtt_ng_publish(struct mqtt_ng_client *client,
+ char *topic,
+ free_fnc_t topic_free,
+ void *msg,
+ free_fnc_t msg_free,
+ size_t msg_len,
+ uint8_t publish_flags,
+ uint16_t *packet_id);
+
+struct mqtt_sub {
+ char *topic;
+ free_fnc_t topic_free;
+ uint8_t options;
+};
+
+int mqtt_ng_subscribe(struct mqtt_ng_client *client, struct mqtt_sub *subscriptions, size_t subscription_count);
+
+int mqtt_ng_ping(struct mqtt_ng_client *client);
+
+typedef ssize_t (*mqtt_ng_send_fnc_t)(void *user_ctx, const void* buf, size_t len);
+
+struct mqtt_ng_init {
+ mqtt_wss_log_ctx_t log;
+ rbuf_t data_in;
+ mqtt_ng_send_fnc_t data_out_fnc;
+ void *user_ctx;
+
+ void (*puback_callback)(uint16_t packet_id);
+ void (*connack_callback)(void* user_ctx, int connack_reply);
+ void (*msg_callback)(const char *topic, const void *msg, size_t msglen, int qos);
+};
+
+struct mqtt_ng_client *mqtt_ng_init(struct mqtt_ng_init *settings);
+
+void mqtt_ng_destroy(struct mqtt_ng_client *client);
+
+int mqtt_ng_disconnect(struct mqtt_ng_client *client, uint8_t reason_code);
+
+int mqtt_ng_sync(struct mqtt_ng_client *client);
+
+time_t mqtt_ng_last_send_time(struct mqtt_ng_client *client);
+
+void mqtt_ng_set_max_mem(struct mqtt_ng_client *client, size_t bytes);
+
+void mqtt_ng_get_stats(struct mqtt_ng_client *client, struct mqtt_ng_stats *stats);
+
+int mqtt_ng_set_topic_alias(struct mqtt_ng_client *client, const char *topic);
diff --git a/src/aclk/mqtt_websockets/mqtt_wss_client.c b/src/aclk/mqtt_websockets/mqtt_wss_client.c
new file mode 100644
index 000000000..f5b4025d7
--- /dev/null
+++ b/src/aclk/mqtt_websockets/mqtt_wss_client.c
@@ -0,0 +1,1136 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// Copyright (C) 2020 Timotej Šiškovič
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include "mqtt_wss_client.h"
+#include "mqtt_ng.h"
+#include "ws_client.h"
+#include "common_internal.h"
+
+#include <stdlib.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <poll.h>
+#include <string.h>
+#include <time.h>
+
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <netinet/tcp.h> //TCP_NODELAY
+#include <netdb.h>
+
+#include <openssl/err.h>
+#include <openssl/ssl.h>
+
+#define PIPE_READ_END 0
+#define PIPE_WRITE_END 1
+#define POLLFD_SOCKET 0
+#define POLLFD_PIPE 1
+
+#if (OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_110) && (SSLEAY_VERSION_NUMBER >= OPENSSL_VERSION_097)
+#include <openssl/conf.h>
+#endif
+
+//TODO MQTT_PUBLISH_RETAIN should not be needed anymore
+#define MQTT_PUBLISH_RETAIN 0x01
+#define MQTT_CONNECT_CLEAN_SESSION 0x02
+#define MQTT_CONNECT_WILL_RETAIN 0x20
+
+char *util_openssl_ret_err(int err)
+{
+ switch(err){
+ case SSL_ERROR_WANT_READ:
+ return "SSL_ERROR_WANT_READ";
+ case SSL_ERROR_WANT_WRITE:
+ return "SSL_ERROR_WANT_WRITE";
+ case SSL_ERROR_NONE:
+ return "SSL_ERROR_NONE";
+ case SSL_ERROR_ZERO_RETURN:
+ return "SSL_ERROR_ZERO_RETURN";
+ case SSL_ERROR_WANT_CONNECT:
+ return "SSL_ERROR_WANT_CONNECT";
+ case SSL_ERROR_WANT_ACCEPT:
+ return "SSL_ERROR_WANT_ACCEPT";
+ case SSL_ERROR_WANT_X509_LOOKUP:
+ return "SSL_ERROR_WANT_X509_LOOKUP";
+#ifdef SSL_ERROR_WANT_ASYNC
+ case SSL_ERROR_WANT_ASYNC:
+ return "SSL_ERROR_WANT_ASYNC";
+#endif
+#ifdef SSL_ERROR_WANT_ASYNC_JOB
+ case SSL_ERROR_WANT_ASYNC_JOB:
+ return "SSL_ERROR_WANT_ASYNC_JOB";
+#endif
+#ifdef SSL_ERROR_WANT_CLIENT_HELLO_CB
+ case SSL_ERROR_WANT_CLIENT_HELLO_CB:
+ return "SSL_ERROR_WANT_CLIENT_HELLO_CB";
+#endif
+ case SSL_ERROR_SYSCALL:
+ return "SSL_ERROR_SYSCALL";
+ case SSL_ERROR_SSL:
+ return "SSL_ERROR_SSL";
+ }
+ return "UNKNOWN";
+}
+
+struct mqtt_wss_client_struct {
+ ws_client *ws_client;
+
+ mqtt_wss_log_ctx_t log;
+
+// immediate connection (e.g. proxy server)
+ char *host;
+ int port;
+
+// target of connection (e.g. where we want to connect to)
+ char *target_host;
+ int target_port;
+
+ enum mqtt_wss_proxy_type proxy_type;
+ char *proxy_uname;
+ char *proxy_passwd;
+
+// nonblock IO related
+ int sockfd;
+ int write_notif_pipe[2];
+ struct pollfd poll_fds[2];
+
+ SSL_CTX *ssl_ctx;
+ SSL *ssl;
+ int ssl_flags;
+
+ struct mqtt_ng_client *mqtt;
+
+ int mqtt_keepalive;
+
+ pthread_mutex_t pub_lock;
+
+// signifies that we didn't write all MQTT wanted
+// us to write during last cycle (e.g. due to buffer
+// size) and thus we should arm POLLOUT
+ unsigned int mqtt_didnt_finish_write:1;
+
+ unsigned int mqtt_connected:1;
+ unsigned int mqtt_disconnecting:1;
+
+// Application layer callback pointers
+ void (*msg_callback)(const char *, const void *, size_t, int);
+ void (*puback_callback)(uint16_t packet_id);
+
+ pthread_mutex_t stat_lock;
+ struct mqtt_wss_stats stats;
+
+#ifdef MQTT_WSS_DEBUG
+ void (*ssl_ctx_keylog_cb)(const SSL *ssl, const char *line);
+#endif
+};
+
+static void mws_connack_callback_ng(void *user_ctx, int code)
+{
+ mqtt_wss_client client = user_ctx;
+ switch(code) {
+ case 0:
+ client->mqtt_connected = 1;
+ return;
+//TODO manual labor: all the CONNACK error codes with some nice error message
+ default:
+ mws_error(client->log, "MQTT CONNACK returned error %d", code);
+ return;
+ }
+}
+
+static ssize_t mqtt_send_cb(void *user_ctx, const void* buf, size_t len)
+{
+ mqtt_wss_client client = user_ctx;
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "mqtt_pal_sendall(len=%d)", len);
+#endif
+ int ret = ws_client_send(client->ws_client, WS_OP_BINARY_FRAME, buf, len);
+ if (ret >= 0 && (size_t)ret != len) {
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "Not complete message sent (Msg=%d,Sent=%d). Need to arm POLLOUT!", len, ret);
+#endif
+ client->mqtt_didnt_finish_write = 1;
+ }
+ return ret;
+}
+
+mqtt_wss_client mqtt_wss_new(const char *log_prefix,
+ mqtt_wss_log_callback_t log_callback,
+ msg_callback_fnc_t msg_callback,
+ void (*puback_callback)(uint16_t packet_id))
+{
+ mqtt_wss_log_ctx_t log;
+
+ log = mqtt_wss_log_ctx_create(log_prefix, log_callback);
+ if(!log)
+ return NULL;
+
+ SSL_library_init();
+ SSL_load_error_strings();
+
+ mqtt_wss_client client = mw_calloc(1, sizeof(struct mqtt_wss_client_struct));
+ if (!client) {
+ mws_error(log, "OOM alocating mqtt_wss_client");
+ goto fail;
+ }
+
+ pthread_mutex_init(&client->pub_lock, NULL);
+ pthread_mutex_init(&client->stat_lock, NULL);
+
+ client->msg_callback = msg_callback;
+ client->puback_callback = puback_callback;
+
+ client->ws_client = ws_client_new(0, &client->target_host, log);
+ if (!client->ws_client) {
+ mws_error(log, "Error creating ws_client");
+ goto fail_1;
+ }
+
+ client->log = log;
+
+#ifdef __APPLE__
+ if (pipe(client->write_notif_pipe)) {
+#else
+ if (pipe2(client->write_notif_pipe, O_CLOEXEC /*| O_DIRECT*/)) {
+#endif
+ mws_error(log, "Couldn't create pipe");
+ goto fail_2;
+ }
+
+ client->poll_fds[POLLFD_PIPE].fd = client->write_notif_pipe[PIPE_READ_END];
+ client->poll_fds[POLLFD_PIPE].events = POLLIN;
+
+ client->poll_fds[POLLFD_SOCKET].events = POLLIN;
+
+ struct mqtt_ng_init settings = {
+ .log = log,
+ .data_in = client->ws_client->buf_to_mqtt,
+ .data_out_fnc = &mqtt_send_cb,
+ .user_ctx = client,
+ .connack_callback = &mws_connack_callback_ng,
+ .puback_callback = puback_callback,
+ .msg_callback = msg_callback
+ };
+ if ( (client->mqtt = mqtt_ng_init(&settings)) == NULL ) {
+ mws_error(log, "Error initializing internal MQTT client");
+ goto fail_3;
+ }
+
+ return client;
+
+fail_3:
+ close(client->write_notif_pipe[PIPE_WRITE_END]);
+ close(client->write_notif_pipe[PIPE_READ_END]);
+fail_2:
+ ws_client_destroy(client->ws_client);
+fail_1:
+ mw_free(client);
+fail:
+ mqtt_wss_log_ctx_destroy(log);
+ return NULL;
+}
+
+void mqtt_wss_set_max_buf_size(mqtt_wss_client client, size_t size)
+{
+ mqtt_ng_set_max_mem(client->mqtt, size);
+}
+
+void mqtt_wss_destroy(mqtt_wss_client client)
+{
+ mqtt_ng_destroy(client->mqtt);
+
+ close(client->write_notif_pipe[PIPE_WRITE_END]);
+ close(client->write_notif_pipe[PIPE_READ_END]);
+
+ ws_client_destroy(client->ws_client);
+
+ // deleted after client->ws_client
+ // as it "borrows" this pointer and might use it
+ if (client->target_host == client->host)
+ client->target_host = NULL;
+ if (client->target_host)
+ mw_free(client->target_host);
+ if (client->host)
+ mw_free(client->host);
+ mw_free(client->proxy_passwd);
+ mw_free(client->proxy_uname);
+
+ if (client->ssl)
+ SSL_free(client->ssl);
+
+ if (client->ssl_ctx)
+ SSL_CTX_free(client->ssl_ctx);
+
+ if (client->sockfd > 0)
+ close(client->sockfd);
+
+ pthread_mutex_destroy(&client->pub_lock);
+ pthread_mutex_destroy(&client->stat_lock);
+
+ mqtt_wss_log_ctx_destroy(client->log);
+ mw_free(client);
+}
+
+static int cert_verify_callback(int preverify_ok, X509_STORE_CTX *ctx)
+{
+ SSL *ssl;
+ X509 *err_cert;
+ mqtt_wss_client client;
+ int err = 0, depth;
+ char *err_str;
+
+ ssl = X509_STORE_CTX_get_ex_data(ctx, SSL_get_ex_data_X509_STORE_CTX_idx());
+ client = SSL_get_ex_data(ssl, 0);
+
+ // TODO handle depth as per https://www.openssl.org/docs/man1.0.2/man3/SSL_CTX_set_verify.html
+
+ if (!preverify_ok) {
+ err = X509_STORE_CTX_get_error(ctx);
+ depth = X509_STORE_CTX_get_error_depth(ctx);
+ err_cert = X509_STORE_CTX_get_current_cert(ctx);
+ err_str = X509_NAME_oneline(X509_get_subject_name(err_cert), NULL, 0);
+
+ mws_error(client->log, "verify error:num=%d:%s:depth=%d:%s", err,
+ X509_verify_cert_error_string(err), depth, err_str);
+
+ mw_free(err_str);
+ }
+
+ if (!preverify_ok && err == X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT &&
+ client->ssl_flags & MQTT_WSS_SSL_ALLOW_SELF_SIGNED)
+ {
+ preverify_ok = 1;
+ mws_error(client->log, "Self Signed Certificate Accepted as the connection was "
+ "requested with MQTT_WSS_SSL_ALLOW_SELF_SIGNED");
+ }
+
+ return preverify_ok;
+}
+
+#define PROXY_CONNECT "CONNECT"
+#define PROXY_HTTP "HTTP/1.1"
+#define PROXY_HTTP10 "HTTP/1.0"
+#define HTTP_ENDLINE "\x0D\x0A"
+#define HTTP_HDR_TERMINATOR "\x0D\x0A\x0D\x0A"
+#define HTTP_CODE_LEN 4
+#define HTTP_REASON_MAX_LEN 512
+static int http_parse_reply(mqtt_wss_client client, rbuf_t buf)
+{
+ char *ptr;
+ char http_code_s[4];
+ int http_code;
+ int idx;
+
+ if (rbuf_memcmp_n(buf, PROXY_HTTP, strlen(PROXY_HTTP))) {
+ if (rbuf_memcmp_n(buf, PROXY_HTTP10, strlen(PROXY_HTTP10))) {
+ mws_error(client->log, "http_proxy expected reply with \"" PROXY_HTTP "\" or \"" PROXY_HTTP10 "\"");
+ return 1;
+ }
+ }
+
+ rbuf_bump_tail(buf, strlen(PROXY_HTTP));
+
+ if (!rbuf_pop(buf, http_code_s, 1) || http_code_s[0] != 0x20) {
+ mws_error(client->log, "http_proxy missing space after \"" PROXY_HTTP "\" or \"" PROXY_HTTP10 "\"");
+ return 2;
+ }
+
+ if (!rbuf_pop(buf, http_code_s, HTTP_CODE_LEN)) {
+ mws_error(client->log, "http_proxy missing HTTP code");
+ return 3;
+ }
+
+ for (int i = 0; i < HTTP_CODE_LEN - 1; i++)
+ if (http_code_s[i] > 0x39 || http_code_s[i] < 0x30) {
+ mws_error(client->log, "http_proxy HTTP code non numeric");
+ return 4;
+ }
+
+ http_code_s[HTTP_CODE_LEN - 1] = 0;
+ http_code = atoi(http_code_s);
+
+ // TODO check if we ever have more headers here
+ rbuf_find_bytes(buf, HTTP_ENDLINE, strlen(HTTP_ENDLINE), &idx);
+ if (idx >= HTTP_REASON_MAX_LEN) {
+ mws_error(client->log, "http_proxy returned reason that is too long");
+ return 5;
+ }
+
+ if (http_code != 200) {
+ ptr = mw_malloc(idx + 1);
+ if (!ptr)
+ return 6;
+ rbuf_pop(buf, ptr, idx);
+ ptr[idx] = 0;
+
+ mws_error(client->log, "http_proxy returned error code %d \"%s\"", http_code, ptr);
+ mw_free(ptr);
+ return 7;
+ }/* else
+ rbuf_bump_tail(buf, idx);*/
+
+ rbuf_find_bytes(buf, HTTP_HDR_TERMINATOR, strlen(HTTP_HDR_TERMINATOR), &idx);
+ if (idx)
+ rbuf_bump_tail(buf, idx);
+
+ rbuf_bump_tail(buf, strlen(HTTP_HDR_TERMINATOR));
+
+ if (rbuf_bytes_available(buf)) {
+ mws_error(client->log, "http_proxy unexpected trailing bytes after end of HTTP hdr");
+ return 8;
+ }
+
+ mws_debug(client->log, "http_proxy CONNECT succeeded");
+ return 0;
+}
+
+#if defined(OPENSSL_VERSION_NUMBER) && OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_110
+static EVP_ENCODE_CTX *EVP_ENCODE_CTX_new(void)
+{
+ EVP_ENCODE_CTX *ctx = OPENSSL_malloc(sizeof(*ctx));
+
+ if (ctx != NULL) {
+ memset(ctx, 0, sizeof(*ctx));
+ }
+ return ctx;
+}
+static void EVP_ENCODE_CTX_free(EVP_ENCODE_CTX *ctx)
+{
+ OPENSSL_free(ctx);
+ return;
+}
+#endif
+
+inline static int base64_encode_helper(unsigned char *out, int *outl, const unsigned char *in, int in_len)
+{
+ int len;
+ unsigned char *str = out;
+ EVP_ENCODE_CTX *ctx = EVP_ENCODE_CTX_new();
+ EVP_EncodeInit(ctx);
+ EVP_EncodeUpdate(ctx, str, outl, in, in_len);
+ str += *outl;
+ EVP_EncodeFinal(ctx, str, &len);
+ *outl += len;
+
+ str = out;
+ while(*str) {
+ if (*str != 0x0D && *str != 0x0A)
+ *out++ = *str++;
+ else
+ str++;
+ }
+ *out = 0;
+
+ EVP_ENCODE_CTX_free(ctx);
+ return 0;
+}
+
+static int http_proxy_connect(mqtt_wss_client client)
+{
+ int rc;
+ struct pollfd poll_fd;
+ rbuf_t r_buf = rbuf_create(4096);
+ if (!r_buf)
+ return 1;
+ char *r_buf_ptr;
+ size_t r_buf_linear_insert_capacity;
+
+ poll_fd.fd = client->sockfd;
+ poll_fd.events = POLLIN;
+
+ r_buf_ptr = rbuf_get_linear_insert_range(r_buf, &r_buf_linear_insert_capacity);
+ snprintf(r_buf_ptr, r_buf_linear_insert_capacity,"%s %s:%d %s" HTTP_ENDLINE "Host: %s" HTTP_ENDLINE, PROXY_CONNECT,
+ client->target_host, client->target_port, PROXY_HTTP, client->target_host);
+ write(client->sockfd, r_buf_ptr, strlen(r_buf_ptr));
+
+ if (client->proxy_uname) {
+ size_t creds_plain_len = strlen(client->proxy_uname) + strlen(client->proxy_passwd) + 2;
+ char *creds_plain = mw_malloc(creds_plain_len);
+ if (!creds_plain) {
+ mws_error(client->log, "OOM creds_plain");
+ rc = 6;
+ goto cleanup;
+ }
+ int creds_base64_len = (((4 * creds_plain_len / 3) + 3) & ~3);
+ // OpenSSL encoder puts newline every 64 output bytes
+ // we remove those but during encoding we need that space in the buffer
+ creds_base64_len += (1+(creds_base64_len/64)) * strlen("\n");
+ char *creds_base64 = mw_malloc(creds_base64_len + 1);
+ if (!creds_base64) {
+ mw_free(creds_plain);
+ mws_error(client->log, "OOM creds_base64");
+ rc = 6;
+ goto cleanup;
+ }
+ char *ptr = creds_plain;
+ strcpy(ptr, client->proxy_uname);
+ ptr += strlen(client->proxy_uname);
+ *ptr++ = ':';
+ strcpy(ptr, client->proxy_passwd);
+
+ int b64_len;
+ base64_encode_helper((unsigned char*)creds_base64, &b64_len, (unsigned char*)creds_plain, strlen(creds_plain));
+ mw_free(creds_plain);
+
+ r_buf_ptr = rbuf_get_linear_insert_range(r_buf, &r_buf_linear_insert_capacity);
+ snprintf(r_buf_ptr, r_buf_linear_insert_capacity,"Proxy-Authorization: Basic %s" HTTP_ENDLINE, creds_base64);
+ write(client->sockfd, r_buf_ptr, strlen(r_buf_ptr));
+ mw_free(creds_base64);
+ }
+ write(client->sockfd, HTTP_ENDLINE, strlen(HTTP_ENDLINE));
+
+ // read until you find CRLF, CRLF (HTTP HDR end)
+ // or ring buffer is full
+ // or timeout
+ while ((rc = poll(&poll_fd, 1, 1000)) >= 0) {
+ if (!rc) {
+ mws_error(client->log, "http_proxy timeout waiting reply from proxy server");
+ rc = 2;
+ goto cleanup;
+ }
+ r_buf_ptr = rbuf_get_linear_insert_range(r_buf, &r_buf_linear_insert_capacity);
+ if (!r_buf_ptr) {
+ mws_error(client->log, "http_proxy read ring buffer full");
+ rc = 3;
+ goto cleanup;
+ }
+ if ((rc = read(client->sockfd, r_buf_ptr, r_buf_linear_insert_capacity)) < 0) {
+ if (errno == EWOULDBLOCK || errno == EAGAIN) {
+ continue;
+ }
+ mws_error(client->log, "http_proxy error reading from socket \"%s\"", strerror(errno));
+ rc = 4;
+ goto cleanup;
+ }
+ rbuf_bump_head(r_buf, rc);
+ if (rbuf_find_bytes(r_buf, HTTP_HDR_TERMINATOR, strlen(HTTP_HDR_TERMINATOR), &rc)) {
+ rc = 0;
+ if (http_parse_reply(client, r_buf))
+ rc = 5;
+
+ goto cleanup;
+ }
+ }
+ mws_error(client->log, "proxy negotiation poll error \"%s\"", strerror(errno));
+ rc = 5;
+cleanup:
+ rbuf_free(r_buf);
+ return rc;
+}
+
+int mqtt_wss_connect(mqtt_wss_client client, char *host, int port, struct mqtt_connect_params *mqtt_params, int ssl_flags, struct mqtt_wss_proxy *proxy)
+{
+ struct sockaddr_in addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sin_family = AF_INET;
+
+ struct hostent *he;
+ struct in_addr **addr_list;
+
+ if (!mqtt_params) {
+ mws_error(client->log, "mqtt_params can't be null!");
+ return -1;
+ }
+
+ // reset state in case this is reconnect
+ client->mqtt_didnt_finish_write = 0;
+ client->mqtt_connected = 0;
+ client->mqtt_disconnecting = 0;
+ ws_client_reset(client->ws_client);
+
+ if (client->target_host == client->host)
+ client->target_host = NULL;
+ if (client->target_host)
+ mw_free(client->target_host);
+ if (client->host)
+ mw_free(client->host);
+
+ if (proxy && proxy->type != MQTT_WSS_DIRECT) {
+ client->host = mw_strdup(proxy->host);
+ client->port = proxy->port;
+ client->target_host = mw_strdup(host);
+ client->target_port = port;
+ client->proxy_type = proxy->type;
+ if (proxy->username)
+ client->proxy_uname = mw_strdup(proxy->username);
+ if (proxy->password)
+ client->proxy_passwd = mw_strdup(proxy->password);
+ } else {
+ client->host = mw_strdup(host);
+ client->port = port;
+ client->target_host = client->host;
+ client->target_port = port;
+ }
+
+ client->ssl_flags = ssl_flags;
+
+ //TODO gethostbyname -> getaddinfo
+ // hstrerror -> gai_strerror
+ if ((he = gethostbyname(client->host)) == NULL) {
+ mws_error(client->log, "gethostbyname() error \"%s\"", hstrerror(h_errno));
+ return -1;
+ }
+
+ addr_list = (struct in_addr **)he->h_addr_list;
+ if(!addr_list[0]) {
+ mws_error(client->log, "No IP addr resolved");
+ return -1;
+ }
+ mws_debug(client->log, "Resolved IP: %s", inet_ntoa(*addr_list[0]));
+ addr.sin_addr = *addr_list[0];
+ addr.sin_port = htons(client->port);
+
+ if (client->sockfd > 0)
+ close(client->sockfd);
+ client->sockfd = socket(AF_INET, SOCK_STREAM | DEFAULT_SOCKET_FLAGS, 0);
+ if (client->sockfd < 0) {
+ mws_error(client->log, "Couldn't create socket()");
+ return -1;
+ }
+
+#ifndef SOCK_CLOEXEC
+ int flags = fcntl(client->sockfd, F_GETFD);
+ if (flags != -1)
+ (void) fcntl(client->sockfd, F_SETFD, flags| FD_CLOEXEC);
+#endif
+
+ int flag = 1;
+ int result = setsockopt(client->sockfd,
+ IPPROTO_TCP,
+ TCP_NODELAY,
+ &flag,
+ sizeof(int));
+ if (result < 0)
+ mws_error(client->log, "Could not dissable NAGLE");
+
+ if (connect(client->sockfd, (struct sockaddr *)&addr, sizeof(addr)) < 0) {
+ mws_error(client->log, "Could not connect to remote endpoint \"%s\", port %d.\n", client->host, client->port);
+ return -3;
+ }
+
+ client->poll_fds[POLLFD_SOCKET].fd = client->sockfd;
+
+ if (fcntl(client->sockfd, F_SETFL, fcntl(client->sockfd, F_GETFL, 0) | O_NONBLOCK) == -1) {
+ mws_error(client->log, "Error setting O_NONBLOCK to TCP socket. \"%s\"", strerror(errno));
+ return -8;
+ }
+
+ if (client->proxy_type != MQTT_WSS_DIRECT)
+ if (http_proxy_connect(client))
+ return -4;
+
+#if OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_110
+#if (SSLEAY_VERSION_NUMBER >= OPENSSL_VERSION_097)
+ OPENSSL_config(NULL);
+#endif
+ SSL_load_error_strings();
+ SSL_library_init();
+#else
+ if (OPENSSL_init_ssl(OPENSSL_INIT_LOAD_CONFIG, NULL) != 1) {
+ mws_error(client->log, "Failed to initialize SSL");
+ return -1;
+ };
+#endif
+
+ // free SSL structs from possible previous connections
+ if (client->ssl)
+ SSL_free(client->ssl);
+ if (client->ssl_ctx)
+ SSL_CTX_free(client->ssl_ctx);
+
+ client->ssl_ctx = SSL_CTX_new(SSLv23_client_method());
+ if (!(client->ssl_flags & MQTT_WSS_SSL_DONT_CHECK_CERTS)) {
+ SSL_CTX_set_default_verify_paths(client->ssl_ctx);
+ SSL_CTX_set_verify(client->ssl_ctx, SSL_VERIFY_PEER | SSL_VERIFY_CLIENT_ONCE, cert_verify_callback);
+ } else
+ mws_error(client->log, "SSL Certificate checking completely disabled!!!");
+
+#ifdef MQTT_WSS_DEBUG
+ if(client->ssl_ctx_keylog_cb)
+ SSL_CTX_set_keylog_callback(client->ssl_ctx, client->ssl_ctx_keylog_cb);
+#endif
+
+ client->ssl = SSL_new(client->ssl_ctx);
+ if (!(client->ssl_flags & MQTT_WSS_SSL_DONT_CHECK_CERTS)) {
+ if (!SSL_set_ex_data(client->ssl, 0, client)) {
+ mws_error(client->log, "Could not SSL_set_ex_data");
+ return -4;
+ }
+ }
+ SSL_set_fd(client->ssl, client->sockfd);
+ SSL_set_connect_state(client->ssl);
+
+ if (!SSL_set_tlsext_host_name(client->ssl, client->target_host)) {
+ mws_error(client->log, "Error setting TLS SNI host");
+ return -7;
+ }
+
+ result = SSL_connect(client->ssl);
+ if (result != -1 && result != 1) {
+ mws_error(client->log, "SSL could not connect");
+ return -5;
+ }
+ if (result == -1) {
+ int ec = SSL_get_error(client->ssl, result);
+ if (ec != SSL_ERROR_WANT_READ && ec != SSL_ERROR_WANT_WRITE) {
+ mws_error(client->log, "Failed to start SSL connection");
+ return -6;
+ }
+ }
+
+ client->mqtt_keepalive = (mqtt_params->keep_alive ? mqtt_params->keep_alive : 400);
+
+ mws_info(client->log, "Going to connect using internal MQTT 5 implementation");
+ struct mqtt_auth_properties auth;
+ auth.client_id = (char*)mqtt_params->clientid;
+ auth.client_id_free = NULL;
+ auth.username = (char*)mqtt_params->username;
+ auth.username_free = NULL;
+ auth.password = (char*)mqtt_params->password;
+ auth.password_free = NULL;
+ struct mqtt_lwt_properties lwt;
+ lwt.will_topic = (char*)mqtt_params->will_topic;
+ lwt.will_topic_free = NULL;
+ lwt.will_message = (void*)mqtt_params->will_msg;
+ lwt.will_message_free = NULL; // TODO expose no copy version to API
+ lwt.will_message_size = mqtt_params->will_msg_len;
+ lwt.will_qos = (mqtt_params->will_flags & MQTT_WSS_PUB_QOSMASK);
+ lwt.will_retain = mqtt_params->will_flags & MQTT_WSS_PUB_RETAIN;
+ int ret = mqtt_ng_connect(client->mqtt, &auth, mqtt_params->will_msg ? &lwt : NULL, 1, client->mqtt_keepalive);
+ if (ret) {
+ mws_error(client->log, "Error generating MQTT connect");
+ return 1;
+ }
+
+ client->poll_fds[POLLFD_PIPE].events = POLLIN;
+ client->poll_fds[POLLFD_SOCKET].events = POLLIN;
+ // wait till MQTT connection is established
+ while (!client->mqtt_connected) {
+ if(mqtt_wss_service(client, -1)) {
+ mws_error(client->log, "Error connecting to MQTT WSS server \"%s\", port %d.", host, port);
+ return 2;
+ }
+ }
+
+ return 0;
+}
+
+#define NSEC_PER_USEC 1000ULL
+#define USEC_PER_SEC 1000000ULL
+#define NSEC_PER_MSEC 1000000ULL
+#define NSEC_PER_SEC 1000000000ULL
+
+static inline uint64_t boottime_usec(mqtt_wss_client client) {
+ struct timespec ts;
+#if defined(__APPLE__) || defined(__FreeBSD__)
+ if (clock_gettime(CLOCK_MONOTONIC, &ts) == -1) {
+#else
+ if (clock_gettime(CLOCK_BOOTTIME, &ts) == -1) {
+#endif
+ mws_error(client->log, "clock_gettimte failed");
+ return 0;
+ }
+ return (uint64_t)ts.tv_sec * USEC_PER_SEC + (ts.tv_nsec % NSEC_PER_SEC) / NSEC_PER_USEC;
+}
+
+#define MWS_TIMED_OUT 1
+#define MWS_ERROR 2
+#define MWS_OK 0
+static inline const char *mqtt_wss_error_tos(int ec)
+{
+ switch(ec) {
+ case MWS_TIMED_OUT:
+ return "Error: Operation was not able to finish in time";
+ case MWS_ERROR:
+ return "Unspecified Error";
+ default:
+ return "Unknown Error Code!";
+ }
+
+}
+
+static inline int mqtt_wss_service_all(mqtt_wss_client client, int timeout_ms)
+{
+ uint64_t exit_by = boottime_usec(client) + (timeout_ms * NSEC_PER_MSEC);
+ uint64_t now;
+ client->poll_fds[POLLFD_SOCKET].events |= POLLOUT; // TODO when entering mwtt_wss_service use out buffer size to arm POLLOUT
+ while (rbuf_bytes_available(client->ws_client->buf_write)) {
+ now = boottime_usec(client);
+ if (now >= exit_by)
+ return MWS_TIMED_OUT;
+ if (mqtt_wss_service(client, exit_by - now))
+ return MWS_ERROR;
+ }
+ return MWS_OK;
+}
+
+void mqtt_wss_disconnect(mqtt_wss_client client, int timeout_ms)
+{
+ int ret;
+
+ // block application from sending more MQTT messages
+ client->mqtt_disconnecting = 1;
+
+ // send whatever was left at the time of calling this function
+ ret = mqtt_wss_service_all(client, timeout_ms / 4);
+ if(ret)
+ mws_error(client->log,
+ "Error while trying to send all remaining data in an attempt "
+ "to gracefully disconnect! EC=%d Desc:\"%s\"",
+ ret,
+ mqtt_wss_error_tos(ret));
+
+ // schedule and send MQTT disconnect
+ mqtt_ng_disconnect(client->mqtt, 0);
+ mqtt_ng_sync(client->mqtt);
+
+ ret = mqtt_wss_service_all(client, timeout_ms / 4);
+ if(ret)
+ mws_error(client->log,
+ "Error while trying to send MQTT disconnect message in an attempt "
+ "to gracefully disconnect! EC=%d Desc:\"%s\"",
+ ret,
+ mqtt_wss_error_tos(ret));
+
+ // send WebSockets close message
+ uint16_t ws_rc = htobe16(1000);
+ ws_client_send(client->ws_client, WS_OP_CONNECTION_CLOSE, (const char*)&ws_rc, sizeof(ws_rc));
+ ret = mqtt_wss_service_all(client, timeout_ms / 4);
+ if(ret) {
+ // Some MQTT/WSS servers will close socket on receipt of MQTT disconnect and
+ // do not wait for WebSocket to be closed properly
+ mws_warn(client->log,
+ "Error while trying to send WebSocket disconnect message in an attempt "
+ "to gracefully disconnect! EC=%d Desc:\"%s\".",
+ ret,
+ mqtt_wss_error_tos(ret));
+ }
+
+ // Service WSS connection until remote closes connection (usual)
+ // or timeout happens (unusual) in which case we close
+ mqtt_wss_service_all(client, timeout_ms / 4);
+
+ close(client->sockfd);
+ client->sockfd = -1;
+}
+
+static inline void mqtt_wss_wakeup(mqtt_wss_client client)
+{
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "mqtt_wss_wakup - forcing wake up of main loop");
+#endif
+ write(client->write_notif_pipe[PIPE_WRITE_END], " ", 1);
+}
+
+#define THROWAWAY_BUF_SIZE 32
+char throwaway[THROWAWAY_BUF_SIZE];
+static inline void util_clear_pipe(int fd)
+{
+ (void)read(fd, throwaway, THROWAWAY_BUF_SIZE);
+}
+
+static inline void set_socket_pollfds(mqtt_wss_client client, int ssl_ret) {
+ if (ssl_ret == SSL_ERROR_WANT_WRITE)
+ client->poll_fds[POLLFD_SOCKET].events |= POLLOUT;
+ if (ssl_ret == SSL_ERROR_WANT_READ)
+ client->poll_fds[POLLFD_SOCKET].events |= POLLIN;
+}
+
+static int handle_mqtt_internal(mqtt_wss_client client)
+{
+ int rc = mqtt_ng_sync(client->mqtt);
+ if (rc) {
+ mws_error(client->log, "mqtt_ng_sync returned %d != 0", rc);
+ client->mqtt_connected = 0;
+ return 1;
+ }
+ return 0;
+}
+
+#define SEC_TO_MSEC 1000
+static inline long long int t_till_next_keepalive_ms(mqtt_wss_client client)
+{
+ time_t last_send = mqtt_ng_last_send_time(client->mqtt);
+ long long int next_mqtt_keep_alive = (last_send * SEC_TO_MSEC)
+ + (client->mqtt_keepalive * (SEC_TO_MSEC * 0.75 /* SEND IN ADVANCE */));
+ return(next_mqtt_keep_alive - (time(NULL) * SEC_TO_MSEC));
+}
+
+#ifdef MQTT_WSS_CPUSTATS
+static inline uint64_t mqtt_wss_now_usec(mqtt_wss_client client) {
+ struct timespec ts;
+ if(clock_gettime(CLOCK_MONOTONIC, &ts) == -1) {
+ mws_error(client->log, "clock_gettime(CLOCK_MONOTONIC, &timespec) failed.");
+ return 0;
+ }
+ return (uint64_t)ts.tv_sec * USEC_PER_SEC + (ts.tv_nsec % NSEC_PER_SEC) / NSEC_PER_USEC;
+}
+#endif
+
+int mqtt_wss_service(mqtt_wss_client client, int timeout_ms)
+{
+ char *ptr;
+ size_t size;
+ int ret;
+ int send_keepalive = 0;
+
+#ifdef MQTT_WSS_CPUSTATS
+ uint64_t t1,t2;
+ t1 = mqtt_wss_now_usec(client);
+#endif
+
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, ">>>>> mqtt_wss_service <<<<<");
+ mws_debug(client->log, "Waiting for events: %s%s%s",
+ (client->poll_fds[POLLFD_SOCKET].events & POLLIN) ? "SOCKET_POLLIN " : "",
+ (client->poll_fds[POLLFD_SOCKET].events & POLLOUT) ? "SOCKET_POLLOUT " : "",
+ (client->poll_fds[POLLFD_PIPE].events & POLLIN) ? "PIPE_POLLIN" : "" );
+#endif
+
+ // Check user requested TO doesn't interfere with MQTT keep alives
+ long long int till_next_keep_alive = t_till_next_keepalive_ms(client);
+ if (client->mqtt_connected && (timeout_ms < 0 || timeout_ms >= till_next_keep_alive)) {
+ #ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "Shortening Timeout requested %d to %lld to ensure keep-alive can be sent", timeout_ms, till_next_keep_alive);
+ #endif
+ timeout_ms = till_next_keep_alive;
+ send_keepalive = 1;
+ }
+
+#ifdef MQTT_WSS_CPUSTATS
+ t2 = mqtt_wss_now_usec(client);
+ client->stats.time_keepalive += t2 - t1;
+#endif
+
+ if ((ret = poll(client->poll_fds, 2, timeout_ms >= 0 ? timeout_ms : -1)) < 0) {
+ if (errno == EINTR) {
+ mws_warn(client->log, "poll interrupted by EINTR");
+ return 0;
+ }
+ mws_error(client->log, "poll error \"%s\"", strerror(errno));
+ return -2;
+ }
+
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "Poll events happened: %s%s%s%s",
+ (client->poll_fds[POLLFD_SOCKET].revents & POLLIN) ? "SOCKET_POLLIN " : "",
+ (client->poll_fds[POLLFD_SOCKET].revents & POLLOUT) ? "SOCKET_POLLOUT " : "",
+ (client->poll_fds[POLLFD_PIPE].revents & POLLIN) ? "PIPE_POLLIN " : "",
+ (!ret) ? "POLL_TIMEOUT" : "");
+#endif
+
+#ifdef MQTT_WSS_CPUSTATS
+ t1 = mqtt_wss_now_usec(client);
+#endif
+
+ if (ret == 0) {
+ if (send_keepalive) {
+ // otherwise we shortened the timeout ourselves to take care of
+ // MQTT keep alives
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "Forcing MQTT Ping/keep-alive");
+#endif
+ mqtt_ng_ping(client->mqtt);
+ } else {
+ // if poll timed out and user requested timeout was being used
+ // return here let user do his work and he will call us back soon
+ return 0;
+ }
+ }
+
+#ifdef MQTT_WSS_CPUSTATS
+ t2 = mqtt_wss_now_usec(client);
+ client->stats.time_keepalive += t2 - t1;
+#endif
+
+ client->poll_fds[POLLFD_SOCKET].events = 0;
+
+ if ((ptr = rbuf_get_linear_insert_range(client->ws_client->buf_read, &size))) {
+ if((ret = SSL_read(client->ssl, ptr, size)) > 0) {
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "SSL_Read: Read %d.", ret);
+#endif
+ pthread_mutex_lock(&client->stat_lock);
+ client->stats.bytes_rx += ret;
+ pthread_mutex_unlock(&client->stat_lock);
+ rbuf_bump_head(client->ws_client->buf_read, ret);
+ } else {
+ int errnobkp = errno;
+ ret = SSL_get_error(client->ssl, ret);
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "Read Err: %s", util_openssl_ret_err(ret));
+#endif
+ set_socket_pollfds(client, ret);
+ if (ret != SSL_ERROR_WANT_READ &&
+ ret != SSL_ERROR_WANT_WRITE) {
+ mws_error(client->log, "SSL_read error: %d %s", ret, util_openssl_ret_err(ret));
+ if (ret == SSL_ERROR_SYSCALL)
+ mws_error(client->log, "SSL_read SYSCALL errno: %d %s", errnobkp, strerror(errnobkp));
+ return MQTT_WSS_ERR_CONN_DROP;
+ }
+ }
+ }
+
+#ifdef MQTT_WSS_CPUSTATS
+ t1 = mqtt_wss_now_usec(client);
+ client->stats.time_read_socket += t1 - t2;
+#endif
+
+ ret = ws_client_process(client->ws_client);
+ switch(ret) {
+ case WS_CLIENT_PROTOCOL_ERROR:
+ return MQTT_WSS_ERR_PROTO_WS;
+ case WS_CLIENT_NEED_MORE_BYTES:
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "WSCLIENT WANT READ");
+#endif
+ client->poll_fds[POLLFD_SOCKET].events |= POLLIN;
+ break;
+ case WS_CLIENT_CONNECTION_CLOSED:
+ return MQTT_WSS_ERR_CONN_DROP;
+ }
+
+#ifdef MQTT_WSS_CPUSTATS
+ t2 = mqtt_wss_now_usec(client);
+ client->stats.time_process_websocket += t2 - t1;
+#endif
+
+ // process MQTT stuff
+ if(client->ws_client->state == WS_ESTABLISHED)
+ if (handle_mqtt_internal(client))
+ return MQTT_WSS_ERR_PROTO_MQTT;
+
+ if (client->mqtt_didnt_finish_write) {
+ client->mqtt_didnt_finish_write = 0;
+ client->poll_fds[POLLFD_SOCKET].events |= POLLOUT;
+ }
+
+#ifdef MQTT_WSS_CPUSTATS
+ t1 = mqtt_wss_now_usec(client);
+ client->stats.time_process_mqtt += t1 - t2;
+#endif
+
+ if ((ptr = rbuf_get_linear_read_range(client->ws_client->buf_write, &size))) {
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "Have data to write to SSL");
+#endif
+ if ((ret = SSL_write(client->ssl, ptr, size)) > 0) {
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "SSL_Write: Written %d of avail %d.", ret, size);
+#endif
+ pthread_mutex_lock(&client->stat_lock);
+ client->stats.bytes_tx += ret;
+ pthread_mutex_unlock(&client->stat_lock);
+ rbuf_bump_tail(client->ws_client->buf_write, ret);
+ } else {
+ int errnobkp = errno;
+ ret = SSL_get_error(client->ssl, ret);
+#ifdef DEBUG_ULTRA_VERBOSE
+ mws_debug(client->log, "Write Err: %s", util_openssl_ret_err(ret));
+#endif
+ set_socket_pollfds(client, ret);
+ if (ret != SSL_ERROR_WANT_READ &&
+ ret != SSL_ERROR_WANT_WRITE) {
+ mws_error(client->log, "SSL_write error: %d %s", ret, util_openssl_ret_err(ret));
+ if (ret == SSL_ERROR_SYSCALL)
+ mws_error(client->log, "SSL_write SYSCALL errno: %d %s", errnobkp, strerror(errnobkp));
+ return MQTT_WSS_ERR_CONN_DROP;
+ }
+ }
+ }
+
+ if(client->poll_fds[POLLFD_PIPE].revents & POLLIN)
+ util_clear_pipe(client->write_notif_pipe[PIPE_READ_END]);
+
+#ifdef MQTT_WSS_CPUSTATS
+ t2 = mqtt_wss_now_usec(client);
+ client->stats.time_write_socket += t2 - t1;
+#endif
+
+ return MQTT_WSS_OK;
+}
+
+int mqtt_wss_publish5(mqtt_wss_client client,
+ char *topic,
+ free_fnc_t topic_free,
+ void *msg,
+ free_fnc_t msg_free,
+ size_t msg_len,
+ uint8_t publish_flags,
+ uint16_t *packet_id)
+{
+ if (client->mqtt_disconnecting) {
+ mws_error(client->log, "mqtt_wss is disconnecting can't publish");
+ return 1;
+ }
+
+ if (!client->mqtt_connected) {
+ mws_error(client->log, "MQTT is offline. Can't send message.");
+ return 1;
+ }
+ uint8_t mqtt_flags = 0;
+
+ mqtt_flags = (publish_flags & MQTT_WSS_PUB_QOSMASK) << 1;
+ if (publish_flags & MQTT_WSS_PUB_RETAIN)
+ mqtt_flags |= MQTT_PUBLISH_RETAIN;
+
+ int rc = mqtt_ng_publish(client->mqtt, topic, topic_free, msg, msg_free, msg_len, mqtt_flags, packet_id);
+ if (rc == MQTT_NG_MSGGEN_MSG_TOO_BIG)
+ return MQTT_WSS_ERR_TOO_BIG_FOR_SERVER;
+
+ mqtt_wss_wakeup(client);
+
+ return rc;
+}
+
+int mqtt_wss_subscribe(mqtt_wss_client client, char *topic, int max_qos_level)
+{
+ (void)max_qos_level; //TODO now hardcoded
+ if (!client->mqtt_connected) {
+ mws_error(client->log, "MQTT is offline. Can't subscribe.");
+ return 1;
+ }
+
+ if (client->mqtt_disconnecting) {
+ mws_error(client->log, "mqtt_wss is disconnecting can't subscribe");
+ return 1;
+ }
+
+ struct mqtt_sub sub = {
+ .topic = topic,
+ .topic_free = NULL,
+ .options = /* max_qos_level & 0x3 TODO when QOS > 1 implemented */ 0x01 | (0x01 << 3)
+ };
+ mqtt_ng_subscribe(client->mqtt, &sub, 1);
+
+ mqtt_wss_wakeup(client);
+ return 0;
+}
+
+struct mqtt_wss_stats mqtt_wss_get_stats(mqtt_wss_client client)
+{
+ struct mqtt_wss_stats current;
+ pthread_mutex_lock(&client->stat_lock);
+ current = client->stats;
+ memset(&client->stats, 0, sizeof(client->stats));
+ pthread_mutex_unlock(&client->stat_lock);
+ mqtt_ng_get_stats(client->mqtt, &current.mqtt);
+ return current;
+}
+
+int mqtt_wss_set_topic_alias(mqtt_wss_client client, const char *topic)
+{
+ return mqtt_ng_set_topic_alias(client->mqtt, topic);
+}
+
+#ifdef MQTT_WSS_DEBUG
+void mqtt_wss_set_SSL_CTX_keylog_cb(mqtt_wss_client client, void (*ssl_ctx_keylog_cb)(const SSL *ssl, const char *line))
+{
+ client->ssl_ctx_keylog_cb = ssl_ctx_keylog_cb;
+}
+#endif
diff --git a/src/aclk/mqtt_websockets/mqtt_wss_client.h b/src/aclk/mqtt_websockets/mqtt_wss_client.h
new file mode 100644
index 000000000..4bdea4db9
--- /dev/null
+++ b/src/aclk/mqtt_websockets/mqtt_wss_client.h
@@ -0,0 +1,162 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// Copyright (C) 2020 Timotej Šiškovič
+
+#ifndef MQTT_WSS_CLIENT_H
+#define MQTT_WSS_CLIENT_H
+
+#include <stdint.h>
+#include <stddef.h> //size_t
+
+#include "mqtt_wss_log.h"
+#include "common_public.h"
+
+// All OK call me at your earliest convinience
+#define MQTT_WSS_OK 0
+/* All OK, poll timeout you requested when calling mqtt_wss_service expired - you might want to know if timeout
+ * happened or we got some data or handle same as MQTT_WSS_OK
+ */
+#define MQTT_WSS_OK_TO 1
+// Connection was closed by remote
+#define MQTT_WSS_ERR_CONN_DROP -1
+// Error in MQTT protocol (e.g. malformed packet)
+#define MQTT_WSS_ERR_PROTO_MQTT -2
+// Error in WebSocket protocol (e.g. malformed packet)
+#define MQTT_WSS_ERR_PROTO_WS -3
+
+#define MQTT_WSS_ERR_TX_BUF_TOO_SMALL -4
+#define MQTT_WSS_ERR_RX_BUF_TOO_SMALL -5
+
+#define MQTT_WSS_ERR_TOO_BIG_FOR_SERVER -6
+// if client was initialized with MQTT 3 but MQTT 5 feature
+// was requested by user of library
+#define MQTT_WSS_ERR_CANT_DO -8
+
+typedef struct mqtt_wss_client_struct *mqtt_wss_client;
+
+typedef void (*msg_callback_fnc_t)(const char *topic, const void *msg, size_t msglen, int qos);
+/* Creates new instance of MQTT over WSS. Doesn't start connection.
+ * @param log_prefix this is prefix to be used when logging to discern between multiple
+ * mqtt_wss instances. Can be NULL.
+ * @param log_callback is function pointer to fnc to be called when mqtt_wss wants
+ * to log. This allows plugging this library into your own logging system/solution.
+ * If NULL STDOUT/STDERR will be used.
+ * @param msg_callback is function pointer to function which will be called
+ * when application level message arrives from broker (for subscribed topics).
+ * Can be NULL if you are not interested about incoming messages.
+ * @param puback_callback is function pointer to function to be called when QOS1 Publish
+ * is acknowledged by server
+ */
+mqtt_wss_client mqtt_wss_new(const char *log_prefix,
+ mqtt_wss_log_callback_t log_callback,
+ msg_callback_fnc_t msg_callback,
+ void (*puback_callback)(uint16_t packet_id));
+
+void mqtt_wss_set_max_buf_size(mqtt_wss_client client, size_t size);
+
+void mqtt_wss_destroy(mqtt_wss_client client);
+
+struct mqtt_connect_params;
+struct mqtt_wss_proxy;
+
+#define MQTT_WSS_SSL_CERT_CHECK_FULL 0x00
+#define MQTT_WSS_SSL_ALLOW_SELF_SIGNED 0x01
+#define MQTT_WSS_SSL_DONT_CHECK_CERTS 0x08
+
+/* Will block until the MQTT over WSS connection is established or return error
+ * @param client mqtt_wss_client which should connect
+ * @param host to connect to (where MQTT over WSS server is listening)
+ * @param port to connect to (where MQTT over WSS server is listening)
+ * @param mqtt_params pointer to mqtt_connect_params structure which contains MQTT credentials and settings
+ * @param ssl_flags parameters for OpenSSL, 0=MQTT_WSS_SSL_CERT_CHECK_FULL
+ */
+int mqtt_wss_connect(mqtt_wss_client client, char *host, int port, struct mqtt_connect_params *mqtt_params, int ssl_flags, struct mqtt_wss_proxy *proxy);
+int mqtt_wss_service(mqtt_wss_client client, int timeout_ms);
+void mqtt_wss_disconnect(mqtt_wss_client client, int timeout_ms);
+
+// we redefine this instead of using MQTT-C flags as in future
+// we want to support different MQTT implementations if needed
+enum mqtt_wss_publish_flags {
+ MQTT_WSS_PUB_QOS0 = 0x0,
+ MQTT_WSS_PUB_QOS1 = 0x1,
+ MQTT_WSS_PUB_QOS2 = 0x2,
+ MQTT_WSS_PUB_QOSMASK = 0x3,
+ MQTT_WSS_PUB_RETAIN = 0x4
+};
+
+struct mqtt_connect_params {
+ const char *clientid;
+ const char *username;
+ const char *password;
+ const char *will_topic;
+ const void *will_msg;
+ enum mqtt_wss_publish_flags will_flags;
+ size_t will_msg_len;
+ int keep_alive;
+ int drop_on_publish_fail;
+};
+
+enum mqtt_wss_proxy_type {
+ MQTT_WSS_DIRECT = 0,
+ MQTT_WSS_PROXY_HTTP
+};
+
+struct mqtt_wss_proxy {
+ enum mqtt_wss_proxy_type type;
+ const char *host;
+ int port;
+ const char *username;
+ const char *password;
+};
+
+/* TODO!!! update the description
+ * Publishes MQTT message and gets message id
+ * @param client mqtt_wss_client which should transfer the message
+ * @param topic MQTT topic to publish message to (0 terminated C string)
+ * @param msg Message to be published (no need for 0 termination)
+ * @param msg_len Length of the message to be published
+ * @param publish_flags see enum mqtt_wss_publish_flags e.g. (MQTT_WSS_PUB_QOS1 | MQTT_WSS_PUB_RETAIN)
+ * @param packet_id is 16 bit unsigned int representing ID that can be used to pair with PUBACK callback
+ * for usages where application layer wants to know which messages are delivered when
+ * @return Returns 0 on success
+ */
+int mqtt_wss_publish5(mqtt_wss_client client,
+ char *topic,
+ free_fnc_t topic_free,
+ void *msg,
+ free_fnc_t msg_free,
+ size_t msg_len,
+ uint8_t publish_flags,
+ uint16_t *packet_id);
+
+int mqtt_wss_set_topic_alias(mqtt_wss_client client, const char *topic);
+
+/* Subscribes to MQTT topic
+ * @param client mqtt_wss_client which should do the subscription
+ * @param topic MQTT topic to subscribe to
+ * @param max_qos_level maximum QOS level that broker can send to us on this subscription
+ * @return Returns 0 on success
+ */
+int mqtt_wss_subscribe(mqtt_wss_client client, char *topic, int max_qos_level);
+
+
+struct mqtt_wss_stats {
+ uint64_t bytes_tx;
+ uint64_t bytes_rx;
+#ifdef MQTT_WSS_CPUSTATS
+ uint64_t time_keepalive;
+ uint64_t time_read_socket;
+ uint64_t time_write_socket;
+ uint64_t time_process_websocket;
+ uint64_t time_process_mqtt;
+#endif
+ struct mqtt_ng_stats mqtt;
+};
+
+struct mqtt_wss_stats mqtt_wss_get_stats(mqtt_wss_client client);
+
+#ifdef MQTT_WSS_DEBUG
+#include <openssl/ssl.h>
+void mqtt_wss_set_SSL_CTX_keylog_cb(mqtt_wss_client client, void (*ssl_ctx_keylog_cb)(const SSL *ssl, const char *line));
+#endif
+
+#endif /* MQTT_WSS_CLIENT_H */
diff --git a/src/aclk/mqtt_websockets/mqtt_wss_log.c b/src/aclk/mqtt_websockets/mqtt_wss_log.c
new file mode 100644
index 000000000..5e606c12b
--- /dev/null
+++ b/src/aclk/mqtt_websockets/mqtt_wss_log.c
@@ -0,0 +1,130 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#include <stdlib.h>
+#include <stdarg.h>
+#include <string.h>
+#include <stdio.h>
+
+#include "mqtt_wss_log.h"
+#include "common_internal.h"
+
+struct mqtt_wss_log_ctx {
+ mqtt_wss_log_callback_t extern_log_fnc;
+ char *ctx_prefix;
+ char *buffer;
+ char *buffer_w_ptr;
+ size_t buffer_bytes_avail;
+};
+
+#define LOG_BUFFER_SIZE 1024 * 4
+#define LOG_CTX_PREFIX_SEV_STR " : "
+#define LOG_CTX_PREFIX_LIMIT 15
+#define LOG_CTX_PREFIX_LIMIT_STR (LOG_CTX_PREFIX_LIMIT - (2 + strlen(LOG_CTX_PREFIX_SEV_STR))) // with [] characters and affixed ' ' it is total 15 chars
+#if (LOG_CTX_PREFIX_LIMIT * 10) > LOG_BUFFER_SIZE
+#error "LOG_BUFFER_SIZE too small"
+#endif
+mqtt_wss_log_ctx_t mqtt_wss_log_ctx_create(const char *ctx_prefix, mqtt_wss_log_callback_t log_callback)
+{
+ mqtt_wss_log_ctx_t ctx = mw_calloc(1, sizeof(struct mqtt_wss_log_ctx));
+ if(!ctx)
+ return NULL;
+
+ if(log_callback) {
+ ctx->extern_log_fnc = log_callback;
+ ctx->buffer = mw_calloc(1, LOG_BUFFER_SIZE);
+ if(!ctx->buffer)
+ goto cleanup;
+
+ ctx->buffer_w_ptr = ctx->buffer;
+ if(ctx_prefix) {
+ *(ctx->buffer_w_ptr++) = '[';
+ strncpy(ctx->buffer_w_ptr, ctx_prefix, LOG_CTX_PREFIX_LIMIT_STR);
+ ctx->buffer_w_ptr += strnlen(ctx_prefix, LOG_CTX_PREFIX_LIMIT_STR);
+ *(ctx->buffer_w_ptr++) = ']';
+ }
+ strcpy(ctx->buffer_w_ptr, LOG_CTX_PREFIX_SEV_STR);
+ ctx->buffer_w_ptr += strlen(LOG_CTX_PREFIX_SEV_STR);
+ // no term '\0' -> calloc is used
+
+ ctx->buffer_bytes_avail = LOG_BUFFER_SIZE - strlen(ctx->buffer);
+
+ return ctx;
+ }
+
+ if(ctx_prefix) {
+ ctx->ctx_prefix = strndup(ctx_prefix, LOG_CTX_PREFIX_LIMIT_STR);
+ if(!ctx->ctx_prefix)
+ goto cleanup;
+ }
+
+ return ctx;
+
+cleanup:
+ mw_free(ctx);
+ return NULL;
+}
+
+void mqtt_wss_log_ctx_destroy(mqtt_wss_log_ctx_t ctx)
+{
+ mw_free(ctx->ctx_prefix);
+ mw_free(ctx->buffer);
+ mw_free(ctx);
+}
+
+static inline char severity_to_c(int severity)
+{
+ switch (severity) {
+ case MQTT_WSS_LOG_FATAL:
+ return 'F';
+ case MQTT_WSS_LOG_ERROR:
+ return 'E';
+ case MQTT_WSS_LOG_WARN:
+ return 'W';
+ case MQTT_WSS_LOG_INFO:
+ return 'I';
+ case MQTT_WSS_LOG_DEBUG:
+ return 'D';
+ default:
+ return '?';
+ }
+}
+
+void mws_log(int severity, mqtt_wss_log_ctx_t ctx, const char *fmt, va_list args)
+{
+ size_t size;
+
+ if(ctx->extern_log_fnc) {
+ size = vsnprintf(ctx->buffer_w_ptr, ctx->buffer_bytes_avail, fmt, args);
+ *(ctx->buffer_w_ptr - 3) = severity_to_c(severity);
+
+ ctx->extern_log_fnc(severity, ctx->buffer);
+
+ if(size >= ctx->buffer_bytes_avail)
+ mws_error(ctx, "Last message of this type was truncated! Consider what you log or increase LOG_BUFFER_SIZE if really needed.");
+
+ return;
+ }
+
+ if(ctx->ctx_prefix)
+ printf("[%s] ", ctx->ctx_prefix);
+
+ printf("%c: ", severity_to_c(severity));
+
+ vprintf(fmt, args);
+ putchar('\n');
+}
+
+#define DEFINE_MWS_SEV_FNC(severity_fncname, severity) \
+void mws_ ## severity_fncname(mqtt_wss_log_ctx_t ctx, const char *fmt, ...) \
+{ \
+ va_list args; \
+ va_start(args, fmt); \
+ mws_log(severity, ctx, fmt, args); \
+ va_end(args); \
+}
+
+DEFINE_MWS_SEV_FNC(fatal, MQTT_WSS_LOG_FATAL)
+DEFINE_MWS_SEV_FNC(error, MQTT_WSS_LOG_ERROR)
+DEFINE_MWS_SEV_FNC(warn, MQTT_WSS_LOG_WARN )
+DEFINE_MWS_SEV_FNC(info, MQTT_WSS_LOG_INFO )
+DEFINE_MWS_SEV_FNC(debug, MQTT_WSS_LOG_DEBUG)
diff --git a/src/aclk/mqtt_websockets/mqtt_wss_log.h b/src/aclk/mqtt_websockets/mqtt_wss_log.h
new file mode 100644
index 000000000..6ae60d870
--- /dev/null
+++ b/src/aclk/mqtt_websockets/mqtt_wss_log.h
@@ -0,0 +1,39 @@
+// Copyright: SPDX-License-Identifier: GPL-3.0-only
+
+#ifndef MQTT_WSS_LOG_H
+#define MQTT_WSS_LOG_H
+
+typedef enum mqtt_wss_log_type {
+ MQTT_WSS_LOG_DEBUG = 0x01,
+ MQTT_WSS_LOG_INFO = 0x02,
+ MQTT_WSS_LOG_WARN = 0x03,
+ MQTT_WSS_LOG_ERROR = 0x81,
+ MQTT_WSS_LOG_FATAL = 0x88
+} mqtt_wss_log_type_t;
+
+typedef void (*mqtt_wss_log_callback_t)(mqtt_wss_log_type_t, const char*);
+
+typedef struct mqtt_wss_log_ctx *mqtt_wss_log_ctx_t;
+
+/** Creates logging context with optional prefix and optional callback
+ * @param ctx_prefix String to be prefixed to every log message.
+ * This is useful if multiple clients are instantiated to be able to
+ * know which one this message belongs to. Can be `NULL` for no prefix.
+ * @param log_callback Callback to be called instead of logging to
+ * `STDOUT` or `STDERR` (if debug enabled otherwise silent). Callback has to be
+ * pointer to function of `void function(mqtt_wss_log_type_t, const char*)` type.
+ * If `NULL` default will be used (silent or STDERR/STDOUT).
+ * @return mqtt_wss_log_ctx_t or `NULL` on error */
+mqtt_wss_log_ctx_t mqtt_wss_log_ctx_create(const char *ctx_prefix, mqtt_wss_log_callback_t log_callback);
+
+/** Destroys logging context and cleans up the memory
+ * @param ctx Context to destroy */
+void mqtt_wss_log_ctx_destroy(mqtt_wss_log_ctx_t ctx);
+
+void mws_fatal(mqtt_wss_log_ctx_t ctx, const char *fmt, ...);
+void mws_error(mqtt_wss_log_ctx_t ctx, const char *fmt, ...);
+void mws_warn (mqtt_wss_log_ctx_t ctx, const char *fmt, ...);
+void mws_info (mqtt_wss_log_ctx_t ctx, const char *fmt, ...);
+void mws_debug(mqtt_wss_log_ctx_t ctx, const char *fmt, ...);
+
+#endif /* MQTT_WSS_LOG_H */
diff --git a/src/aclk/mqtt_websockets/test.c b/src/aclk/mqtt_websockets/test.c
new file mode 100644
index 000000000..59a9f3474
--- /dev/null
+++ b/src/aclk/mqtt_websockets/test.c
@@ -0,0 +1,90 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// Copyright (C) 2020 Timotej Šiškovič
+
+#include <unistd.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+
+#include "mqtt_wss_client.h"
+
+int test_exit = 0;
+int port = 0;
+
+void mqtt_wss_log_cb(mqtt_wss_log_type_t log_type, const char* str)
+{
+ (void)log_type;
+ puts(str);
+}
+
+#define TEST_MSGLEN_MAX 512
+void msg_callback(const char *topic, const void *msg, size_t msglen, int qos)
+{
+ char cmsg[TEST_MSGLEN_MAX];
+ size_t len = (msglen < TEST_MSGLEN_MAX - 1) ? msglen : (TEST_MSGLEN_MAX - 1);
+ memcpy(cmsg,
+ msg,
+ len);
+ cmsg[len] = 0;
+
+ if (!strcmp(cmsg, "shutdown"))
+ test_exit = 1;
+
+ printf("Got Message From Broker Topic \"%s\" QOS %d MSG: \"%s\"\n", topic, qos, cmsg);
+}
+
+#define TESTMSG "Hello World!"
+int client_handle(mqtt_wss_client client)
+{
+ struct mqtt_connect_params params = {
+ .clientid = "test",
+ .username = "anon",
+ .password = "anon",
+ .keep_alive = 10
+ };
+
+/* struct mqtt_wss_proxy proxy = {
+ .host = "127.0.0.1",
+ .port = 3128,
+ .type = MQTT_WSS_PROXY_HTTP
+ };*/
+
+ while (mqtt_wss_connect(client, "127.0.0.1", port, &params, MQTT_WSS_SSL_ALLOW_SELF_SIGNED, NULL /*&proxy*/)) {
+ printf("Connect failed\n");
+ sleep(1);
+ printf("Attempting Reconnect\n");
+ }
+ printf("Connection succeeded\n");
+
+ mqtt_wss_subscribe(client, "test", 1);
+
+ while (!test_exit) {
+ if(mqtt_wss_service(client, -1) < 0)
+ return 1;
+ }
+ return 0;
+}
+
+int main(int argc, char **argv)
+{
+ if (argc >= 2)
+ port = atoi(argv[1]);
+ if (!port)
+ port = 9002;
+ printf("Using port %d\n", port);
+
+ mqtt_wss_client client = mqtt_wss_new("main", mqtt_wss_log_cb, msg_callback, NULL);
+ if (!client) {
+ printf("Couldn't initialize mqtt_wss\n");
+ return 1;
+ }
+ while (!test_exit) {
+ printf("client_handle = %d\n", client_handle(client));
+ }
+ if (test_exit) {
+ mqtt_wss_disconnect(client, 2000);
+ }
+
+ mqtt_wss_destroy(client);
+ return 0;
+}
diff --git a/src/aclk/mqtt_websockets/ws_client.c b/src/aclk/mqtt_websockets/ws_client.c
new file mode 100644
index 000000000..240e889ca
--- /dev/null
+++ b/src/aclk/mqtt_websockets/ws_client.c
@@ -0,0 +1,744 @@
+// Copyright (C) 2020 Timotej Šiškovič
+// SPDX-License-Identifier: GPL-3.0-only
+//
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free Software Foundation, version 3.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
+// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+// See the GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along with this program.
+// If not, see <https://www.gnu.org/licenses/>.
+
+#include <fcntl.h>
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+
+#include <openssl/evp.h>
+
+#include "ws_client.h"
+#include "common_internal.h"
+
+#ifdef MQTT_WEBSOCKETS_DEBUG
+#include "../c-rbuf/src/ringbuffer_internal.h"
+#endif
+
+#define UNIT_LOG_PREFIX "ws_client: "
+#define FATAL(fmt, ...) mws_fatal(client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+#define ERROR(fmt, ...) mws_error(client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+#define WARN(fmt, ...) mws_warn (client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+#define INFO(fmt, ...) mws_info (client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+#define DEBUG(fmt, ...) mws_debug(client->log, UNIT_LOG_PREFIX fmt, ##__VA_ARGS__)
+
+const char *websocket_upgrage_hdr = "GET /mqtt HTTP/1.1\x0D\x0A"
+ "Host: %s\x0D\x0A"
+ "Upgrade: websocket\x0D\x0A"
+ "Connection: Upgrade\x0D\x0A"
+ "Sec-WebSocket-Key: %s\x0D\x0A"
+ "Origin: http://example.com\x0D\x0A"
+ "Sec-WebSocket-Protocol: mqtt\x0D\x0A"
+ "Sec-WebSocket-Version: 13\x0D\x0A\x0D\x0A";
+
+const char *mqtt_protoid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+#define DEFAULT_RINGBUFFER_SIZE (1024*128)
+#define ENTROPY_SOURCE "/dev/urandom"
+ws_client *ws_client_new(size_t buf_size, char **host, mqtt_wss_log_ctx_t log)
+{
+ ws_client *client;
+
+ if(!host)
+ return NULL;
+
+ client = mw_calloc(1, sizeof(ws_client));
+ if (!client)
+ return NULL;
+
+ client->host = host;
+ client->log = log;
+
+ client->buf_read = rbuf_create(buf_size ? buf_size : DEFAULT_RINGBUFFER_SIZE);
+ if (!client->buf_read)
+ goto cleanup;
+
+ client->buf_write = rbuf_create(buf_size ? buf_size : DEFAULT_RINGBUFFER_SIZE);
+ if (!client->buf_write)
+ goto cleanup_1;
+
+ client->buf_to_mqtt = rbuf_create(buf_size ? buf_size : DEFAULT_RINGBUFFER_SIZE);
+ if (!client->buf_to_mqtt)
+ goto cleanup_2;
+
+ client->entropy_fd = open(ENTROPY_SOURCE, O_RDONLY | O_CLOEXEC);
+ if (client->entropy_fd < 1) {
+ ERROR("Error opening entropy source \"" ENTROPY_SOURCE "\". Reason: \"%s\"", strerror(errno));
+ goto cleanup_3;
+ }
+
+ return client;
+
+cleanup_3:
+ rbuf_free(client->buf_to_mqtt);
+cleanup_2:
+ rbuf_free(client->buf_write);
+cleanup_1:
+ rbuf_free(client->buf_read);
+cleanup:
+ mw_free(client);
+ return NULL;
+}
+
+void ws_client_free_headers(ws_client *client)
+{
+ struct http_header *ptr = client->hs.headers;
+ struct http_header *tmp;
+
+ while (ptr) {
+ tmp = ptr;
+ ptr = ptr->next;
+ mw_free(tmp);
+ }
+
+ client->hs.headers = NULL;
+ client->hs.headers_tail = NULL;
+ client->hs.hdr_count = 0;
+}
+
+void ws_client_destroy(ws_client *client)
+{
+ ws_client_free_headers(client);
+ mw_free(client->hs.nonce_reply);
+ mw_free(client->hs.http_reply_msg);
+ close(client->entropy_fd);
+ rbuf_free(client->buf_read);
+ rbuf_free(client->buf_write);
+ rbuf_free(client->buf_to_mqtt);
+ mw_free(client);
+}
+
+void ws_client_reset(ws_client *client)
+{
+ ws_client_free_headers(client);
+ mw_free(client->hs.nonce_reply);
+ client->hs.nonce_reply = NULL;
+ mw_free(client->hs.http_reply_msg);
+ client->hs.http_reply_msg = NULL;
+ rbuf_flush(client->buf_read);
+ rbuf_flush(client->buf_write);
+ rbuf_flush(client->buf_to_mqtt);
+ client->state = WS_RAW;
+ client->hs.hdr_state = WS_HDR_HTTP;
+ client->rx.parse_state = WS_FIRST_2BYTES;
+}
+
+#define MAX_HTTP_HDR_COUNT 128
+int ws_client_add_http_header(ws_client *client, struct http_header *hdr)
+{
+ if (client->hs.hdr_count > MAX_HTTP_HDR_COUNT) {
+ ERROR("Too many HTTP response header fields");
+ return -1;
+ }
+
+ if (client->hs.headers)
+ client->hs.headers_tail->next = hdr;
+ else
+ client->hs.headers = hdr;
+
+ client->hs.headers_tail = hdr;
+ client->hs.hdr_count++;
+
+ return 0;
+}
+
+int ws_client_want_write(ws_client *client)
+{
+ return rbuf_bytes_available(client->buf_write);
+}
+
+#define RAND_SRC "/dev/urandom"
+static int ws_client_get_nonce(ws_client *client, char *dest, unsigned int size)
+{
+ // we do not need crypto secure random here
+ // it's just used for protocol negotiation
+ int rd;
+ int f = open(RAND_SRC, O_RDONLY | O_CLOEXEC);
+ if (f < 0) {
+ ERROR("Error opening \"%s\". Err: \"%s\"", RAND_SRC, strerror(errno));
+ return -2;
+ }
+
+ if ((rd = read(f, dest, size)) > 0) {
+ close(f);
+ return rd;
+ }
+ close(f);
+ return -1;
+}
+
+#define WEBSOCKET_NONCE_SIZE 16
+#define TEMP_BUF_SIZE 4096
+int ws_client_start_handshake(ws_client *client)
+{
+ char nonce[WEBSOCKET_NONCE_SIZE];
+ char nonce_b64[256];
+ char second[TEMP_BUF_SIZE];
+ unsigned int md_len;
+ unsigned char *digest;
+ EVP_MD_CTX *md_ctx;
+ const EVP_MD *md;
+
+ if(!*client->host) {
+ ERROR("Hostname has not been set. We should not be able to come here!");
+ return 1;
+ }
+
+ ws_client_get_nonce(client, nonce, WEBSOCKET_NONCE_SIZE);
+ EVP_EncodeBlock((unsigned char *)nonce_b64, (const unsigned char *)nonce, WEBSOCKET_NONCE_SIZE);
+ snprintf(second, TEMP_BUF_SIZE, websocket_upgrage_hdr,
+ *client->host,
+ nonce_b64);
+ if(rbuf_bytes_free(client->buf_write) < strlen(second)) {
+ ERROR("Write buffer capacity too low.");
+ return 1;
+ }
+
+ rbuf_push(client->buf_write, second, strlen(second));
+ client->state = WS_HANDSHAKE;
+
+ //Calculating expected Sec-WebSocket-Accept reply
+ snprintf(second, TEMP_BUF_SIZE, "%s%s", nonce_b64, mqtt_protoid);
+
+#if (OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_110)
+ md_ctx = EVP_MD_CTX_create();
+#else
+ md_ctx = EVP_MD_CTX_new();
+#endif
+ if (md_ctx == NULL) {
+ ERROR("Cant create EVP_MD Context");
+ return 1;
+ }
+
+ md = EVP_get_digestbyname("sha1");
+ if (!md) {
+ ERROR("Unknown message digest");
+ return 1;
+ }
+
+ if ((digest = (unsigned char *)OPENSSL_malloc(EVP_MD_size(EVP_sha256()))) == NULL) {
+ ERROR("Cant alloc digest");
+ return 1;
+ }
+
+ EVP_DigestInit_ex(md_ctx, md, NULL);
+ EVP_DigestUpdate(md_ctx, second, strlen(second));
+ EVP_DigestFinal_ex(md_ctx, digest, &md_len);
+
+ EVP_EncodeBlock((unsigned char *)nonce_b64, digest, md_len);
+
+ mw_free(client->hs.nonce_reply);
+ client->hs.nonce_reply = mw_strdup(nonce_b64);
+
+ OPENSSL_free(digest);
+
+#if (OPENSSL_VERSION_NUMBER < OPENSSL_VERSION_110)
+ EVP_MD_CTX_destroy(md_ctx);
+#else
+ EVP_MD_CTX_free(md_ctx);
+#endif
+
+ return 0;
+}
+
+#define BUF_READ_MEMCMP_CONST(const, err) \
+ if (rbuf_memcmp_n(client->buf_read, const, strlen(const))) { \
+ ERROR(err); \
+ rbuf_flush(client->buf_read); \
+ return WS_CLIENT_PROTOCOL_ERROR; \
+ }
+
+#define BUF_READ_CHECK_AT_LEAST(x) \
+ if (rbuf_bytes_available(client->buf_read) < x) \
+ return WS_CLIENT_NEED_MORE_BYTES;
+
+#define MAX_HTTP_LINE_LENGTH 1024*4
+#define HTTP_SC_LENGTH 4 // "XXX " http status code as C string
+#define WS_CLIENT_HTTP_HDR "HTTP/1.1 "
+#define WS_CONN_ACCEPT "sec-websocket-accept"
+#define HTTP_HDR_SEPARATOR ": "
+#define WS_NONCE_STRLEN_B64 28
+#define WS_HTTP_NEWLINE "\r\n"
+#define HTTP_HEADER_NAME_MAX_LEN 256
+#if HTTP_HEADER_NAME_MAX_LEN > MAX_HTTP_LINE_LENGTH
+#error "Buffer too small"
+#endif
+#if WS_NONCE_STRLEN_B64 > MAX_HTTP_LINE_LENGTH
+#error "Buffer too small"
+#endif
+
+#define HTTP_HDR_LINE_CHECK_LIMIT(x) if ((x) >= MAX_HTTP_LINE_LENGTH) \
+{ \
+ ERROR("HTTP line received is too long. Maximum is %d", MAX_HTTP_LINE_LENGTH); \
+ return WS_CLIENT_PROTOCOL_ERROR; \
+}
+
+int ws_client_parse_handshake_resp(ws_client *client)
+{
+ char buf[HTTP_SC_LENGTH];
+ int idx_crlf, idx_sep;
+ char *ptr;
+ size_t bytes;
+ switch (client->hs.hdr_state) {
+ case WS_HDR_HTTP:
+ BUF_READ_CHECK_AT_LEAST(strlen(WS_CLIENT_HTTP_HDR))
+ BUF_READ_MEMCMP_CONST(WS_CLIENT_HTTP_HDR, "Expected \"HTTP1.1\" header");
+ rbuf_bump_tail(client->buf_read, strlen(WS_CLIENT_HTTP_HDR));
+ client->hs.hdr_state = WS_HDR_RC;
+ break;
+ case WS_HDR_RC:
+ BUF_READ_CHECK_AT_LEAST(HTTP_SC_LENGTH); // "XXX " http return code
+ rbuf_pop(client->buf_read, buf, HTTP_SC_LENGTH);
+ if (buf[HTTP_SC_LENGTH - 1] != 0x20) {
+ ERROR("HTTP status code received is not terminated by space (0x20)");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+ buf[HTTP_SC_LENGTH - 1] = 0;
+ client->hs.http_code = atoi(buf);
+ if (client->hs.http_code < 100 || client->hs.http_code >= 600) {
+ ERROR("HTTP status code received not in valid range 100-600");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+ client->hs.hdr_state = WS_HDR_ENDLINE;
+ break;
+ case WS_HDR_ENDLINE:
+ ptr = rbuf_find_bytes(client->buf_read, WS_HTTP_NEWLINE, strlen(WS_HTTP_NEWLINE), &idx_crlf);
+ if (!ptr) {
+ bytes = rbuf_bytes_available(client->buf_read);
+ HTTP_HDR_LINE_CHECK_LIMIT(bytes);
+ return WS_CLIENT_NEED_MORE_BYTES;
+ }
+ HTTP_HDR_LINE_CHECK_LIMIT(idx_crlf);
+
+ client->hs.http_reply_msg = mw_malloc(idx_crlf+1);
+ rbuf_pop(client->buf_read, client->hs.http_reply_msg, idx_crlf);
+ client->hs.http_reply_msg[idx_crlf] = 0;
+ rbuf_bump_tail(client->buf_read, strlen(WS_HTTP_NEWLINE));
+ client->hs.hdr_state = WS_HDR_PARSE_HEADERS;
+ break;
+ case WS_HDR_PARSE_HEADERS:
+ ptr = rbuf_find_bytes(client->buf_read, WS_HTTP_NEWLINE, strlen(WS_HTTP_NEWLINE), &idx_crlf);
+ if (!ptr) {
+ bytes = rbuf_bytes_available(client->buf_read);
+ HTTP_HDR_LINE_CHECK_LIMIT(bytes);
+ return WS_CLIENT_NEED_MORE_BYTES;
+ }
+ HTTP_HDR_LINE_CHECK_LIMIT(idx_crlf);
+
+ if (!idx_crlf) { // empty line, header end
+ rbuf_bump_tail(client->buf_read, strlen(WS_HTTP_NEWLINE));
+ client->hs.hdr_state = WS_HDR_PARSE_DONE;
+ return 0;
+ }
+
+ ptr = rbuf_find_bytes(client->buf_read, HTTP_HDR_SEPARATOR, strlen(HTTP_HDR_SEPARATOR), &idx_sep);
+ if (!ptr || idx_sep > idx_crlf) {
+ ERROR("Expected HTTP hdr field key/value separator \": \" before endline in non empty HTTP header line");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+ if (idx_crlf == idx_sep + (int)strlen(HTTP_HDR_SEPARATOR)) {
+ ERROR("HTTP Header value cannot be empty");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+
+ if (idx_sep > HTTP_HEADER_NAME_MAX_LEN) {
+ ERROR("HTTP header too long (%d)", idx_sep);
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+
+ struct http_header *hdr = mw_calloc(1, sizeof(struct http_header) + idx_crlf); //idx_crlf includes ": " that will be used as 2 \0 bytes
+ hdr->key = ((char*)hdr) + sizeof(struct http_header);
+ hdr->value = hdr->key + idx_sep + 1;
+
+ bytes = rbuf_pop(client->buf_read, hdr->key, idx_sep);
+ rbuf_bump_tail(client->buf_read, strlen(HTTP_HDR_SEPARATOR));
+
+ bytes = rbuf_pop(client->buf_read, hdr->value, idx_crlf - idx_sep - strlen(HTTP_HDR_SEPARATOR));
+ rbuf_bump_tail(client->buf_read, strlen(WS_HTTP_NEWLINE));
+
+ for (int i = 0; hdr->key[i]; i++)
+ hdr->key[i] = tolower(hdr->key[i]);
+
+// DEBUG("HTTP header \"%s\" received. Value \"%s\"", hdr->key, hdr->value);
+
+ if (ws_client_add_http_header(client, hdr))
+ return WS_CLIENT_PROTOCOL_ERROR;
+
+ if (!strcmp(hdr->key, WS_CONN_ACCEPT)) {
+ if (strcmp(client->hs.nonce_reply, hdr->value)) {
+ ERROR("Received NONCE \"%s\" does not match expected nonce of \"%s\"", hdr->value, client->hs.nonce_reply);
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+ client->hs.nonce_matched = 1;
+ }
+
+ break;
+ case WS_HDR_PARSE_DONE:
+ if (!client->hs.nonce_matched) {
+ ERROR("Missing " WS_CONN_ACCEPT " header");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+ if (client->hs.http_code != 101) {
+ ERROR("HTTP return code not 101. Received %d with msg \"%s\".", client->hs.http_code, client->hs.http_reply_msg);
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+
+ client->state = WS_ESTABLISHED;
+ client->hs.hdr_state = WS_HDR_ALL_DONE;
+ INFO("Websocket Connection Accepted By Server");
+ return WS_CLIENT_PARSING_DONE;
+ case WS_HDR_ALL_DONE:
+ FATAL("This is error we should never come here!");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+ return 0;
+}
+
+#define BYTE_MSB 0x80
+#define WS_FINAL_FRAG BYTE_MSB
+#define WS_PAYLOAD_MASKED BYTE_MSB
+
+static inline size_t get_ws_hdr_size(size_t payload_size)
+{
+ size_t hdr_len = 2 + 4 /*mask*/;
+ if(payload_size > 125)
+ hdr_len += 2;
+ if(payload_size > 65535)
+ hdr_len += 6;
+ return hdr_len;
+}
+
+#define MAX_POSSIBLE_HDR_LEN 14
+int ws_client_send(ws_client *client, enum websocket_opcode frame_type, const char *data, size_t size)
+{
+ // TODO maybe? implement fragmenting, it is not necessary though
+ // as both tested MQTT brokers have no reuirement of one MQTT envelope
+ // be equal to one WebSockets envelope. Therefore there is no need to send
+ // one big MQTT message as single fragmented WebSocket envelope
+ char hdr[MAX_POSSIBLE_HDR_LEN];
+ char *ptr = hdr;
+ char *mask;
+ int size_written = 0;
+ size_t j = 0;
+
+ size_t w_buff_free = rbuf_bytes_free(client->buf_write);
+ size_t hdr_len = get_ws_hdr_size(size);
+
+ if (w_buff_free < hdr_len * 2) {
+#ifdef DEBUG_ULTRA_VERBOSE
+ DEBUG("Write buffer full. Can't write requested %d size.", size);
+#endif
+ return 0;
+ }
+
+ if (w_buff_free < (hdr_len + size)) {
+#ifdef DEBUG_ULTRA_VERBOSE
+ DEBUG("Can't write whole MQTT packet of %d bytes into the buffer. Will do partial send of %d.", size, w_buff_free - hdr_len);
+#endif
+ size = w_buff_free - hdr_len;
+ hdr_len = get_ws_hdr_size(size);
+ // the actual needed header size might decrease if we cut number of bytes
+ // if decrease of size crosses 65535 or 125 boundary
+ // but I can live with that at least for now
+ // worst case is we have 6 more bytes we could have written
+ // no bigus dealus
+ }
+
+ *ptr++ = frame_type | WS_FINAL_FRAG;
+
+ //generate length
+ *ptr = WS_PAYLOAD_MASKED;
+ if (size > 65535) {
+ *ptr++ |= 0x7f;
+ uint64_t be = htobe64(size);
+ memcpy(ptr, (void *)&be, sizeof(be));
+ ptr += sizeof(be);
+ } else if (size > 125) {
+ *ptr++ |= 0x7e;
+ uint16_t be = htobe16(size);
+ memcpy(ptr, (void *)&be, sizeof(be));
+ ptr += sizeof(be);
+ } else
+ *ptr++ |= size;
+
+ mask = ptr;
+ if (read(client->entropy_fd, mask, sizeof(uint32_t)) < (ssize_t)sizeof(uint32_t)) {
+ ERROR("Unable to get mask from \"" ENTROPY_SOURCE "\"");
+ return -2;
+ }
+
+ rbuf_push(client->buf_write, hdr, hdr_len);
+
+ if (!size)
+ return 0;
+
+ // copy and mask data in the write ringbuffer
+ while (size - size_written) {
+ size_t writable_bytes;
+ char *w_ptr = rbuf_get_linear_insert_range(client->buf_write, &writable_bytes);
+ if(!writable_bytes)
+ break;
+
+ writable_bytes = (writable_bytes > size) ? (size - size_written) : writable_bytes;
+
+ memcpy(w_ptr, &data[size_written], writable_bytes);
+ rbuf_bump_head(client->buf_write, writable_bytes);
+
+ for (size_t i = 0; i < writable_bytes; i++, j++)
+ w_ptr[i] ^= mask[j % 4];
+ size_written += writable_bytes;
+ }
+ return size_written;
+}
+
+static int check_opcode(ws_client *client,enum websocket_opcode oc)
+{
+ switch(oc) {
+ case WS_OP_BINARY_FRAME:
+ case WS_OP_CONNECTION_CLOSE:
+ case WS_OP_PING:
+ return 0;
+ case WS_OP_CONTINUATION_FRAME:
+ FATAL("WS_OP_CONTINUATION_FRAME NOT IMPLEMENTED YET!!!!");
+ return 0;
+ case WS_OP_TEXT_FRAME:
+ FATAL("WS_OP_TEXT_FRAME NOT IMPLEMENTED YET!!!!");
+ return 0;
+ case WS_OP_PONG:
+ FATAL("WS_OP_PONG NOT IMPLEMENTED YET!!!!");
+ return 0;
+ default:
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+}
+
+static inline void ws_client_rx_post_hdr_state(ws_client *client)
+{
+ switch(client->rx.opcode) {
+ case WS_OP_BINARY_FRAME:
+ client->rx.parse_state = WS_PAYLOAD_DATA;
+ return;
+ case WS_OP_CONNECTION_CLOSE:
+ client->rx.parse_state = WS_PAYLOAD_CONNECTION_CLOSE;
+ return;
+ case WS_OP_PING:
+ client->rx.parse_state = WS_PAYLOAD_PING_REQ_PAYLOAD;
+ return;
+ default:
+ client->rx.parse_state = WS_PAYLOAD_SKIP_UNKNOWN_PAYLOAD;
+ return;
+ }
+}
+
+#define LONGEST_POSSIBLE_HDR_PART 8
+int ws_client_process_rx_ws(ws_client *client)
+{
+ char buf[LONGEST_POSSIBLE_HDR_PART];
+ size_t size;
+ switch (client->rx.parse_state) {
+ case WS_FIRST_2BYTES:
+ BUF_READ_CHECK_AT_LEAST(2);
+ rbuf_pop(client->buf_read, buf, 2);
+ client->rx.opcode = buf[0] & (char)~BYTE_MSB;
+
+ if (!(buf[0] & (char)~WS_FINAL_FRAG)) {
+ ERROR("Not supporting fragmented messages yet!");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+
+ if (check_opcode(client, client->rx.opcode) == WS_CLIENT_PROTOCOL_ERROR)
+ return WS_CLIENT_PROTOCOL_ERROR;
+
+ if (buf[1] & (char)WS_PAYLOAD_MASKED) {
+ ERROR("Mask is not allowed in Server->Client Websocket direction.");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+
+ switch (buf[1]) {
+ case 127:
+ client->rx.parse_state = WS_PAYLOAD_EXTENDED_64;
+ break;
+ case 126:
+ client->rx.parse_state = WS_PAYLOAD_EXTENDED_16;
+ break;
+ default:
+ client->rx.payload_length = buf[1];
+ ws_client_rx_post_hdr_state(client);
+ }
+ break;
+ case WS_PAYLOAD_EXTENDED_16:
+ BUF_READ_CHECK_AT_LEAST(2);
+ rbuf_pop(client->buf_read, buf, 2);
+ client->rx.payload_length = be16toh(*((uint16_t *)buf));
+ ws_client_rx_post_hdr_state(client);
+ break;
+ case WS_PAYLOAD_EXTENDED_64:
+ BUF_READ_CHECK_AT_LEAST(LONGEST_POSSIBLE_HDR_PART);
+ rbuf_pop(client->buf_read, buf, LONGEST_POSSIBLE_HDR_PART);
+ client->rx.payload_length = be64toh(*((uint64_t *)buf));
+ ws_client_rx_post_hdr_state(client);
+ break;
+ case WS_PAYLOAD_DATA:
+ // TODO not pretty?
+ while (client->rx.payload_processed < client->rx.payload_length) {
+ size_t remaining = client->rx.payload_length - client->rx.payload_processed;
+ if (!rbuf_bytes_available(client->buf_read))
+ return WS_CLIENT_NEED_MORE_BYTES;
+ char *insert = rbuf_get_linear_insert_range(client->buf_to_mqtt, &size);
+ if (!insert) {
+#ifdef DEBUG_ULTRA_VERBOSE
+ DEBUG("BUFFER TOO FULL. Avail %d req %d", (int)size, (int)remaining);
+#endif
+ return WS_CLIENT_BUFFER_FULL;
+ }
+ size = (size > remaining) ? remaining : size;
+ size = rbuf_pop(client->buf_read, insert, size);
+ rbuf_bump_head(client->buf_to_mqtt, size);
+ client->rx.payload_processed += size;
+ }
+ client->rx.parse_state = WS_PACKET_DONE;
+ break;
+ case WS_PAYLOAD_CONNECTION_CLOSE:
+ // for WS_OP_CONNECTION_CLOSE allowed is
+ // a) empty payload
+ // b) 2byte reason code
+ // c) 2byte reason code followed by message
+ if (client->rx.payload_length == 1) {
+ ERROR("WebScoket CONNECTION_CLOSE can't have payload of size 1");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+ if (!client->rx.payload_length) {
+ INFO("WebSocket server closed the connection without giving reason.");
+ client->rx.parse_state = WS_PACKET_DONE;
+ break;
+ }
+ client->rx.parse_state = WS_PAYLOAD_CONNECTION_CLOSE_EC;
+ break;
+ case WS_PAYLOAD_CONNECTION_CLOSE_EC:
+ BUF_READ_CHECK_AT_LEAST(sizeof(uint16_t));
+
+ rbuf_pop(client->buf_read, buf, sizeof(uint16_t));
+ client->rx.specific_data.op_close.ec = be16toh(*((uint16_t *)buf));
+ client->rx.payload_processed += sizeof(uint16_t);
+
+ if(client->rx.payload_processed == client->rx.payload_length) {
+ INFO("WebSocket server closed the connection with EC=%d. Without message.",
+ client->rx.specific_data.op_close.ec);
+ client->rx.parse_state = WS_PACKET_DONE;
+ break;
+ }
+ client->rx.parse_state = WS_PAYLOAD_CONNECTION_CLOSE_MSG;
+ break;
+ case WS_PAYLOAD_CONNECTION_CLOSE_MSG:
+ if (!client->rx.specific_data.op_close.reason)
+ client->rx.specific_data.op_close.reason = mw_malloc(client->rx.payload_length + 1);
+
+ while (client->rx.payload_processed < client->rx.payload_length) {
+ if (!rbuf_bytes_available(client->buf_read))
+ return WS_CLIENT_NEED_MORE_BYTES;
+ client->rx.payload_processed += rbuf_pop(client->buf_read,
+ &client->rx.specific_data.op_close.reason[client->rx.payload_processed - sizeof(uint16_t)],
+ client->rx.payload_length - client->rx.payload_processed);
+ }
+ client->rx.specific_data.op_close.reason[client->rx.payload_length] = 0;
+ INFO("WebSocket server closed the connection with EC=%d and reason \"%s\"",
+ client->rx.specific_data.op_close.ec,
+ client->rx.specific_data.op_close.reason);
+ mw_free(client->rx.specific_data.op_close.reason);
+ client->rx.specific_data.op_close.reason = NULL;
+ client->rx.parse_state = WS_PACKET_DONE;
+ break;
+ case WS_PAYLOAD_SKIP_UNKNOWN_PAYLOAD:
+ BUF_READ_CHECK_AT_LEAST(client->rx.payload_length);
+ WARN("Skipping Websocket Packet of unsupported/unknown type");
+ if (client->rx.payload_length)
+ rbuf_bump_tail(client->buf_read, client->rx.payload_length);
+ client->rx.parse_state = WS_PACKET_DONE;
+ return WS_CLIENT_PARSING_DONE;
+ case WS_PAYLOAD_PING_REQ_PAYLOAD:
+ if (client->rx.payload_length > rbuf_get_capacity(client->buf_read) / 2) {
+ ERROR("Ping arrived with payload which is too big!");
+ return WS_CLIENT_INTERNAL_ERROR;
+ }
+ BUF_READ_CHECK_AT_LEAST(client->rx.payload_length);
+ client->rx.specific_data.ping_msg = mw_malloc(client->rx.payload_length);
+ rbuf_pop(client->buf_read, client->rx.specific_data.ping_msg, client->rx.payload_length);
+ // TODO schedule this instead of sending right away
+ // then attempt to send as soon as buffer space clears up
+ size = ws_client_send(client, WS_OP_PONG, client->rx.specific_data.ping_msg, client->rx.payload_length);
+ if (size != client->rx.payload_length) {
+ ERROR("Unable to send the PONG as one packet back. Closing connection.");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ }
+ client->rx.parse_state = WS_PACKET_DONE;
+ return WS_CLIENT_PARSING_DONE;
+ case WS_PACKET_DONE:
+ client->rx.parse_state = WS_FIRST_2BYTES;
+ client->rx.payload_processed = 0;
+ if (client->rx.opcode == WS_OP_CONNECTION_CLOSE)
+ return WS_CLIENT_CONNECTION_CLOSED;
+ return WS_CLIENT_PARSING_DONE;
+ default:
+ FATAL("Unknown parse state");
+ return WS_CLIENT_INTERNAL_ERROR;
+ }
+ return 0;
+}
+
+int ws_client_process(ws_client *client)
+{
+ int ret;
+ switch(client->state) {
+ case WS_RAW:
+ if (ws_client_start_handshake(client))
+ return WS_CLIENT_INTERNAL_ERROR;
+ return WS_CLIENT_NEED_MORE_BYTES;
+ case WS_HANDSHAKE:
+ do {
+ ret = ws_client_parse_handshake_resp(client);
+ if (ret == WS_CLIENT_PROTOCOL_ERROR)
+ client->state = WS_ERROR;
+ if (ret == WS_CLIENT_PARSING_DONE && client->state == WS_ESTABLISHED)
+ ret = WS_CLIENT_NEED_MORE_BYTES;
+ } while (!ret);
+ break;
+ case WS_ESTABLISHED:
+ do {
+ ret = ws_client_process_rx_ws(client);
+ switch(ret) {
+ case WS_CLIENT_PROTOCOL_ERROR:
+ client->state = WS_ERROR;
+ break;
+ case WS_CLIENT_CONNECTION_CLOSED:
+ client->state = WS_CONN_CLOSED_GRACEFUL;
+ break;
+ }
+ // if ret == 0 we can continue parsing
+ // if ret == WS_CLIENT_PARSING_DONE we processed
+ // one websocket packet and attempt processing
+ // next one if data available in the buffer
+ } while (!ret || ret == WS_CLIENT_PARSING_DONE);
+ break;
+ case WS_ERROR:
+ ERROR("ws_client is in error state. Restart the connection!");
+ return WS_CLIENT_PROTOCOL_ERROR;
+ case WS_CONN_CLOSED_GRACEFUL:
+ ERROR("Connection has been gracefully closed. Calling this is useless (and probably bug) until you reconnect again.");
+ return WS_CLIENT_CONNECTION_CLOSED;
+ default:
+ FATAL("Unknown connection state! Probably memory corruption.");
+ return WS_CLIENT_INTERNAL_ERROR;
+ }
+ return ret;
+}
diff --git a/src/aclk/mqtt_websockets/ws_client.h b/src/aclk/mqtt_websockets/ws_client.h
new file mode 100644
index 000000000..0ccbd29a8
--- /dev/null
+++ b/src/aclk/mqtt_websockets/ws_client.h
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// Copyright (C) 2020 Timotej Šiškovič
+
+#ifndef WS_CLIENT_H
+#define WS_CLIENT_H
+
+#include "c-rbuf/cringbuffer.h"
+#include "mqtt_wss_log.h"
+
+#include <stdint.h>
+
+#define WS_CLIENT_NEED_MORE_BYTES 0x10
+#define WS_CLIENT_PARSING_DONE 0x11
+#define WS_CLIENT_CONNECTION_CLOSED 0x12
+#define WS_CLIENT_PROTOCOL_ERROR -0x10
+#define WS_CLIENT_BUFFER_FULL -0x11
+#define WS_CLIENT_INTERNAL_ERROR -0x12
+
+enum websocket_client_conn_state {
+ WS_RAW = 0,
+ WS_HANDSHAKE,
+ WS_ESTABLISHED,
+ WS_ERROR, // connection has to be restarted if this is reached
+ WS_CONN_CLOSED_GRACEFUL
+};
+
+enum websocket_client_hdr_parse_state {
+ WS_HDR_HTTP = 0, // need to check HTTP/1.1
+ WS_HDR_RC, // need to read HTTP code
+ WS_HDR_ENDLINE, // need to read rest of the first line
+ WS_HDR_PARSE_HEADERS, // rest of the header until CRLF CRLF
+ WS_HDR_PARSE_DONE,
+ WS_HDR_ALL_DONE
+};
+
+enum websocket_client_rx_ws_parse_state {
+ WS_FIRST_2BYTES = 0,
+ WS_PAYLOAD_EXTENDED_16,
+ WS_PAYLOAD_EXTENDED_64,
+ WS_PAYLOAD_DATA, // BINARY payload to be passed to MQTT
+ WS_PAYLOAD_CONNECTION_CLOSE,
+ WS_PAYLOAD_CONNECTION_CLOSE_EC,
+ WS_PAYLOAD_CONNECTION_CLOSE_MSG,
+ WS_PAYLOAD_SKIP_UNKNOWN_PAYLOAD,
+ WS_PAYLOAD_PING_REQ_PAYLOAD, // PING payload to be sent back as PONG
+ WS_PACKET_DONE
+};
+
+enum websocket_opcode {
+ WS_OP_CONTINUATION_FRAME = 0x0,
+ WS_OP_TEXT_FRAME = 0x1,
+ WS_OP_BINARY_FRAME = 0x2,
+ WS_OP_CONNECTION_CLOSE = 0x8,
+ WS_OP_PING = 0x9,
+ WS_OP_PONG = 0xA
+};
+
+struct ws_op_close_payload {
+ uint16_t ec;
+ char *reason;
+};
+
+struct http_header {
+ char *key;
+ char *value;
+ struct http_header *next;
+};
+
+typedef struct websocket_client {
+ enum websocket_client_conn_state state;
+
+ struct ws_handshake {
+ enum websocket_client_hdr_parse_state hdr_state;
+ char *nonce_reply;
+ int nonce_matched;
+ int http_code;
+ char *http_reply_msg;
+ struct http_header *headers;
+ struct http_header *headers_tail;
+ int hdr_count;
+ } hs;
+
+ struct ws_rx {
+ enum websocket_client_rx_ws_parse_state parse_state;
+ enum websocket_opcode opcode;
+ uint64_t payload_length;
+ uint64_t payload_processed;
+ union {
+ struct ws_op_close_payload op_close;
+ char *ping_msg;
+ } specific_data;
+ } rx;
+
+ rbuf_t buf_read; // from SSL
+ rbuf_t buf_write; // to SSL and then to socket
+ // TODO if ringbuffer gets multiple tail support
+ // we can work without buf_to_mqtt and thus reduce
+ // memory usage and remove one more memcpy buf_read->buf_to_mqtt
+ rbuf_t buf_to_mqtt; // RAW data for MQTT lib
+
+ int entropy_fd;
+
+ // careful host is borrowed, don't free
+ char **host;
+ mqtt_wss_log_ctx_t log;
+} ws_client;
+
+ws_client *ws_client_new(size_t buf_size, char **host, mqtt_wss_log_ctx_t log);
+void ws_client_destroy(ws_client *client);
+void ws_client_reset(ws_client *client);
+
+int ws_client_start_handshake(ws_client *client);
+
+int ws_client_want_write(ws_client *client);
+
+int ws_client_process(ws_client *client);
+
+int ws_client_send(ws_client *client, enum websocket_opcode frame_type, const char *data, size_t size);
+
+#endif /* WS_CLIENT_H */
diff --git a/src/aclk/schema-wrappers/agent_cmds.cc b/src/aclk/schema-wrappers/agent_cmds.cc
new file mode 100644
index 000000000..6950f4025
--- /dev/null
+++ b/src/aclk/schema-wrappers/agent_cmds.cc
@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "proto/agent/v1/cmds.pb.h"
+
+#include "agent_cmds.h"
+
+#include "schema_wrapper_utils.h"
+
+using namespace agent::v1;
+
+int parse_cancel_pending_req(const char *msg, size_t msg_len, struct aclk_cancel_pending_req *req)
+{
+ CancelPendingRequest msg_parsed;
+
+ if (!msg_parsed.ParseFromArray(msg, msg_len)) {
+ error_report("Failed to parse CancelPendingRequest message");
+ return 1;
+ }
+
+ if (msg_parsed.request_id().c_str() == NULL) {
+ error_report("CancelPendingRequest message missing request_id");
+ return 1;
+ }
+ req->request_id = strdupz(msg_parsed.request_id().c_str());
+
+ if (msg_parsed.trace_id().c_str())
+ req->trace_id = strdupz(msg_parsed.trace_id().c_str());
+
+ set_timeval_from_google_timestamp(msg_parsed.timestamp(), &req->timestamp);
+
+ return 0;
+}
+
+void free_cancel_pending_req(struct aclk_cancel_pending_req *req)
+{
+ freez(req->request_id);
+ freez(req->trace_id);
+}
diff --git a/src/aclk/schema-wrappers/agent_cmds.h b/src/aclk/schema-wrappers/agent_cmds.h
new file mode 100644
index 000000000..7e01f86c5
--- /dev/null
+++ b/src/aclk/schema-wrappers/agent_cmds.h
@@ -0,0 +1,27 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPERS_AGENT_CMDS_H
+#define ACLK_SCHEMA_WRAPPERS_AGENT_CMDS_H
+
+#include "libnetdata/libnetdata.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct aclk_cancel_pending_req {
+ char *request_id;
+
+ struct timeval timestamp;
+
+ char *trace_id;
+};
+
+int parse_cancel_pending_req(const char *msg, size_t msg_len, struct aclk_cancel_pending_req *req);
+void free_cancel_pending_req(struct aclk_cancel_pending_req *req);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPERS_AGENT_CMDS_H */
diff --git a/src/aclk/schema-wrappers/alarm_config.cc b/src/aclk/schema-wrappers/alarm_config.cc
new file mode 100644
index 000000000..64d28f324
--- /dev/null
+++ b/src/aclk/schema-wrappers/alarm_config.cc
@@ -0,0 +1,140 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "alarm_config.h"
+
+#include "proto/alarm/v1/config.pb.h"
+
+#include "libnetdata/libnetdata.h"
+
+#include "schema_wrapper_utils.h"
+
+using namespace alarms::v1;
+
+void destroy_aclk_alarm_configuration(struct aclk_alarm_configuration *cfg)
+{
+ freez(cfg->alarm);
+ freez(cfg->tmpl);
+ freez(cfg->on_chart);
+ freez(cfg->classification);
+ freez(cfg->type);
+ freez(cfg->component);
+ freez(cfg->os);
+ freez(cfg->hosts);
+ freez(cfg->plugin);
+ freez(cfg->module);
+ freez(cfg->charts);
+ freez(cfg->lookup);
+ freez(cfg->every);
+ freez(cfg->units);
+ freez(cfg->green);
+ freez(cfg->red);
+ freez(cfg->calculation_expr);
+ freez(cfg->warning_expr);
+ freez(cfg->critical_expr);
+ freez(cfg->recipient);
+ freez(cfg->exec);
+ freez(cfg->delay);
+ freez(cfg->repeat);
+ freez(cfg->info);
+ freez(cfg->options);
+ freez(cfg->host_labels);
+ freez(cfg->p_db_lookup_dimensions);
+ freez(cfg->p_db_lookup_method);
+ freez(cfg->p_db_lookup_options);
+ freez(cfg->chart_labels);
+ freez(cfg->summary);
+}
+
+char *generate_provide_alarm_configuration(size_t *len, struct provide_alarm_configuration *data)
+{
+ ProvideAlarmConfiguration msg;
+ AlarmConfiguration *cfg = msg.mutable_config();
+
+ msg.set_config_hash(data->cfg_hash);
+
+ if (data->cfg.alarm)
+ cfg->set_alarm(data->cfg.alarm);
+ if (data->cfg.tmpl)
+ cfg->set_template_(data->cfg.tmpl);
+ if(data->cfg.on_chart)
+ cfg->set_on_chart(data->cfg.on_chart);
+ if (data->cfg.classification)
+ cfg->set_classification(data->cfg.classification);
+ if (data->cfg.type)
+ cfg->set_type(data->cfg.type);
+ if (data->cfg.component)
+ cfg->set_component(data->cfg.component);
+ if (data->cfg.os)
+ cfg->set_os(data->cfg.os);
+ if (data->cfg.hosts)
+ cfg->set_hosts(data->cfg.hosts);
+ if (data->cfg.plugin)
+ cfg->set_plugin(data->cfg.plugin);
+ if(data->cfg.module)
+ cfg->set_module(data->cfg.module);
+ if(data->cfg.charts)
+ cfg->set_charts(data->cfg.charts);
+ if(data->cfg.lookup)
+ cfg->set_lookup(data->cfg.lookup);
+ if(data->cfg.every)
+ cfg->set_every(data->cfg.every);
+ if(data->cfg.units)
+ cfg->set_units(data->cfg.units);
+ if (data->cfg.green)
+ cfg->set_green(data->cfg.green);
+ if (data->cfg.red)
+ cfg->set_red(data->cfg.red);
+ if (data->cfg.calculation_expr)
+ cfg->set_calculation_expr(data->cfg.calculation_expr);
+ if (data->cfg.warning_expr)
+ cfg->set_warning_expr(data->cfg.warning_expr);
+ if (data->cfg.critical_expr)
+ cfg->set_critical_expr(data->cfg.critical_expr);
+ if (data->cfg.recipient)
+ cfg->set_recipient(data->cfg.recipient);
+ if (data->cfg.exec)
+ cfg->set_exec(data->cfg.exec);
+ if (data->cfg.delay)
+ cfg->set_delay(data->cfg.delay);
+ if (data->cfg.repeat)
+ cfg->set_repeat(data->cfg.repeat);
+ if (data->cfg.info)
+ cfg->set_info(data->cfg.info);
+ if (data->cfg.options)
+ cfg->set_options(data->cfg.options);
+ if (data->cfg.host_labels)
+ cfg->set_host_labels(data->cfg.host_labels);
+
+ cfg->set_p_db_lookup_after(data->cfg.p_db_lookup_after);
+ cfg->set_p_db_lookup_before(data->cfg.p_db_lookup_before);
+ if (data->cfg.p_db_lookup_dimensions)
+ cfg->set_p_db_lookup_dimensions(data->cfg.p_db_lookup_dimensions);
+ if (data->cfg.p_db_lookup_method)
+ cfg->set_p_db_lookup_method(data->cfg.p_db_lookup_method);
+ if (data->cfg.p_db_lookup_options)
+ cfg->set_p_db_lookup_options(data->cfg.p_db_lookup_options);
+ cfg->set_p_update_every(data->cfg.p_update_every);
+
+ if (data->cfg.chart_labels)
+ cfg->set_chart_labels(data->cfg.chart_labels);
+ if (data->cfg.summary)
+ cfg->set_summary(data->cfg.summary);
+
+ *len = PROTO_COMPAT_MSG_SIZE(msg);
+ char *bin = (char*)mallocz(*len);
+ if (!msg.SerializeToArray(bin, *len))
+ return NULL;
+
+ return bin;
+}
+
+char *parse_send_alarm_configuration(const char *data, size_t len)
+{
+ SendAlarmConfiguration msg;
+ if (!msg.ParseFromArray(data, len))
+ return NULL;
+ if (!msg.config_hash().c_str())
+ return NULL;
+ return strdupz(msg.config_hash().c_str());
+}
+
diff --git a/src/aclk/schema-wrappers/alarm_config.h b/src/aclk/schema-wrappers/alarm_config.h
new file mode 100644
index 000000000..3c9a5d9a8
--- /dev/null
+++ b/src/aclk/schema-wrappers/alarm_config.h
@@ -0,0 +1,71 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPER_ALARM_CONFIG_H
+#define ACLK_SCHEMA_WRAPPER_ALARM_CONFIG_H
+
+#include <stdlib.h>
+#include <stdint.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct aclk_alarm_configuration {
+ char *alarm;
+ char *tmpl;
+ char *on_chart;
+
+ char *classification;
+ char *type;
+ char *component;
+
+ char *os;
+ char *hosts;
+ char *plugin;
+ char *module;
+ char *charts;
+ char *lookup;
+ char *every;
+ char *units;
+
+ char *green;
+ char *red;
+
+ char *calculation_expr;
+ char *warning_expr;
+ char *critical_expr;
+
+ char *recipient;
+ char *exec;
+ char *delay;
+ char *repeat;
+ char *info;
+ char *options;
+ char *host_labels;
+
+ int32_t p_db_lookup_after;
+ int32_t p_db_lookup_before;
+ char *p_db_lookup_dimensions;
+ char *p_db_lookup_method;
+ char *p_db_lookup_options;
+ int32_t p_update_every;
+
+ char *chart_labels;
+ char *summary;
+};
+
+void destroy_aclk_alarm_configuration(struct aclk_alarm_configuration *cfg);
+
+struct provide_alarm_configuration {
+ char *cfg_hash;
+ struct aclk_alarm_configuration cfg;
+};
+
+char *generate_provide_alarm_configuration(size_t *len, struct provide_alarm_configuration *data);
+char *parse_send_alarm_configuration(const char *data, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPER_ALARM_CONFIG_H */
diff --git a/src/aclk/schema-wrappers/alarm_stream.cc b/src/aclk/schema-wrappers/alarm_stream.cc
new file mode 100644
index 000000000..29d80e39e
--- /dev/null
+++ b/src/aclk/schema-wrappers/alarm_stream.cc
@@ -0,0 +1,221 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "alarm_stream.h"
+
+#include "proto/alarm/v1/stream.pb.h"
+
+#include "libnetdata/libnetdata.h"
+
+#include "schema_wrapper_utils.h"
+
+using namespace alarms::v1;
+
+struct start_alarm_streaming parse_start_alarm_streaming(const char *data, size_t len)
+{
+ struct start_alarm_streaming ret;
+ memset(&ret, 0, sizeof(ret));
+
+ StartAlarmStreaming msg;
+
+ if (!msg.ParseFromArray(data, len))
+ return ret;
+
+ ret.node_id = strdupz(msg.node_id().c_str());
+ ret.resets = msg.resets();
+
+ return ret;
+}
+
+struct send_alarm_checkpoint parse_send_alarm_checkpoint(const char *data, size_t len)
+{
+ struct send_alarm_checkpoint ret;
+ memset(&ret, 0, sizeof(ret));
+
+ SendAlarmCheckpoint msg;
+ if (!msg.ParseFromArray(data, len))
+ return ret;
+
+ ret.node_id = strdupz(msg.node_id().c_str());
+ ret.claim_id = strdupz(msg.claim_id().c_str());
+
+ return ret;
+}
+
+static alarms::v1::AlarmStatus aclk_alarm_status_to_proto(enum aclk_alarm_status status)
+{
+ switch (status) {
+ case aclk_alarm_status::ALARM_STATUS_NULL:
+ return alarms::v1::ALARM_STATUS_NULL;
+ case aclk_alarm_status::ALARM_STATUS_UNKNOWN:
+ return alarms::v1::ALARM_STATUS_UNKNOWN;
+ case aclk_alarm_status::ALARM_STATUS_REMOVED:
+ return alarms::v1::ALARM_STATUS_REMOVED;
+ case aclk_alarm_status::ALARM_STATUS_NOT_A_NUMBER:
+ return alarms::v1::ALARM_STATUS_NOT_A_NUMBER;
+ case aclk_alarm_status::ALARM_STATUS_CLEAR:
+ return alarms::v1::ALARM_STATUS_CLEAR;
+ case aclk_alarm_status::ALARM_STATUS_WARNING:
+ return alarms::v1::ALARM_STATUS_WARNING;
+ case aclk_alarm_status::ALARM_STATUS_CRITICAL:
+ return alarms::v1::ALARM_STATUS_CRITICAL;
+ default:
+ netdata_log_error("Unknown alarm status");
+ return alarms::v1::ALARM_STATUS_UNKNOWN;
+ }
+}
+
+void destroy_alarm_log_entry(struct alarm_log_entry *entry)
+{
+ freez(entry->chart);
+ freez(entry->name);
+ freez(entry->config_hash);
+ freez(entry->timezone);
+ freez(entry->exec_path);
+ freez(entry->conf_source);
+ freez(entry->command);
+ freez(entry->value_string);
+ freez(entry->old_value_string);
+ freez(entry->rendered_info);
+ freez(entry->chart_context);
+ freez(entry->transition_id);
+ freez(entry->chart_name);
+ freez(entry->summary);
+}
+
+static void fill_alarm_log_entry(struct alarm_log_entry *data, AlarmLogEntry *proto)
+{
+ proto->set_node_id(data->node_id);
+ proto->set_claim_id(data->claim_id);
+ proto->set_chart(data->chart);
+ proto->set_name(data->name);
+ proto->set_when(data->when);
+ proto->set_config_hash(data->config_hash);
+ proto->set_utc_offset(data->utc_offset);
+ proto->set_timezone(data->timezone);
+ proto->set_exec_path(data->exec_path);
+ proto->set_conf_source(data->conf_source);
+ proto->set_command(data->command);
+ proto->set_duration(data->duration);
+ proto->set_non_clear_duration(data->non_clear_duration);
+ proto->set_status(aclk_alarm_status_to_proto(data->status));
+ proto->set_old_status(aclk_alarm_status_to_proto(data->old_status));
+ proto->set_delay(data->delay);
+ proto->set_delay_up_to_timestamp(data->delay_up_to_timestamp);
+ proto->set_last_repeat(data->last_repeat);
+ proto->set_silenced(data->silenced);
+
+ if (data->value_string)
+ proto->set_value_string(data->value_string);
+ if (data->old_value_string)
+ proto->set_old_value_string(data->old_value_string);
+
+ proto->set_value(data->value);
+ proto->set_old_value(data->old_value);
+ proto->set_updated(data->updated);
+ proto->set_rendered_info(data->rendered_info);
+ proto->set_chart_context(data->chart_context);
+ proto->set_event_id(data->event_id);
+ proto->set_transition_id(data->transition_id);
+ proto->set_chart_name(data->chart_name);
+ proto->set_summary(data->summary);
+}
+
+char *generate_alarm_log_entry(size_t *len, struct alarm_log_entry *data)
+{
+ AlarmLogEntry le;
+
+ fill_alarm_log_entry(data, &le);
+
+ *len = PROTO_COMPAT_MSG_SIZE(le);
+ char *bin = (char*)mallocz(*len);
+ if (!le.SerializeToArray(bin, *len)) {
+ freez(bin);
+ return NULL;
+ }
+
+ return bin;
+}
+
+char *generate_alarm_checkpoint(size_t *len, struct alarm_checkpoint *data)
+{
+ AlarmCheckpoint msg;
+
+ msg.set_claim_id(data->claim_id);
+ msg.set_node_id(data->node_id);
+ msg.set_checksum(data->checksum);
+
+ *len = PROTO_COMPAT_MSG_SIZE(msg);
+ char *bin = (char*)mallocz(*len);
+ if (!msg.SerializeToArray(bin, *len)) {
+ freez(bin);
+ return NULL;
+ }
+
+ return bin;
+}
+
+struct send_alarm_snapshot *parse_send_alarm_snapshot(const char *data, size_t len)
+{
+ SendAlarmSnapshot msg;
+ if (!msg.ParseFromArray(data, len))
+ return NULL;
+
+ struct send_alarm_snapshot *ret = (struct send_alarm_snapshot*)callocz(1, sizeof(struct send_alarm_snapshot));
+ if (msg.claim_id().c_str())
+ ret->claim_id = strdupz(msg.claim_id().c_str());
+ if (msg.node_id().c_str())
+ ret->node_id = strdupz(msg.node_id().c_str());
+ if (msg.snapshot_uuid().c_str())
+ ret->snapshot_uuid = strdupz(msg.snapshot_uuid().c_str());
+
+ return ret;
+}
+
+void destroy_send_alarm_snapshot(struct send_alarm_snapshot *ptr)
+{
+ freez(ptr->claim_id);
+ freez(ptr->node_id);
+ freez(ptr->snapshot_uuid);
+ freez(ptr);
+}
+
+alarm_snapshot_proto_ptr_t generate_alarm_snapshot_proto(struct alarm_snapshot *data)
+{
+ AlarmSnapshot *msg = new AlarmSnapshot;
+ if (unlikely(!msg)) fatal("Cannot allocate memory for AlarmSnapshot");
+
+ msg->set_node_id(data->node_id);
+ msg->set_claim_id(data->claim_id);
+ msg->set_snapshot_uuid(data->snapshot_uuid);
+ msg->set_chunks(data->chunks);
+ msg->set_chunk(data->chunk);
+
+ // this is handled automatically by add_alarm_log_entry2snapshot function
+ msg->set_chunk_size(0);
+
+ return msg;
+}
+
+void add_alarm_log_entry2snapshot(alarm_snapshot_proto_ptr_t snapshot, struct alarm_log_entry *data)
+{
+ AlarmSnapshot *alarm_snapshot = (AlarmSnapshot *)snapshot;
+ AlarmLogEntry *alarm_log_entry = alarm_snapshot->add_alarms();
+
+ fill_alarm_log_entry(data, alarm_log_entry);
+
+ alarm_snapshot->set_chunk_size(alarm_snapshot->chunk_size() + 1);
+}
+
+char *generate_alarm_snapshot_bin(size_t *len, alarm_snapshot_proto_ptr_t snapshot)
+{
+ AlarmSnapshot *alarm_snapshot = (AlarmSnapshot *)snapshot;
+ *len = PROTO_COMPAT_MSG_SIZE_PTR(alarm_snapshot);
+ char *bin = (char*)mallocz(*len);
+ if (!alarm_snapshot->SerializeToArray(bin, *len)) {
+ delete alarm_snapshot;
+ return NULL;
+ }
+
+ delete alarm_snapshot;
+ return bin;
+}
diff --git a/src/aclk/schema-wrappers/alarm_stream.h b/src/aclk/schema-wrappers/alarm_stream.h
new file mode 100644
index 000000000..3c81ff445
--- /dev/null
+++ b/src/aclk/schema-wrappers/alarm_stream.h
@@ -0,0 +1,128 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPER_ALARM_STREAM_H
+#define ACLK_SCHEMA_WRAPPER_ALARM_STREAM_H
+
+#include <stdlib.h>
+
+#include "database/rrd.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct start_alarm_streaming {
+ char *node_id;
+ bool resets;
+};
+
+struct start_alarm_streaming parse_start_alarm_streaming(const char *data, size_t len);
+
+enum aclk_alarm_status {
+ ALARM_STATUS_NULL = 0,
+ ALARM_STATUS_UNKNOWN = 1,
+ ALARM_STATUS_REMOVED = 2,
+ ALARM_STATUS_NOT_A_NUMBER = 3,
+ ALARM_STATUS_CLEAR = 4,
+ ALARM_STATUS_WARNING = 5,
+ ALARM_STATUS_CRITICAL = 6
+};
+
+struct alarm_log_entry {
+ char *node_id;
+ char *claim_id;
+
+ char *chart;
+ char *name;
+ char *family;
+
+ uint64_t batch_id;
+ uint64_t sequence_id;
+ uint64_t when;
+
+ char *config_hash;
+
+ int32_t utc_offset;
+ char *timezone;
+
+ char *exec_path;
+ char *conf_source;
+ char *command;
+
+ uint32_t duration;
+ uint32_t non_clear_duration;
+
+ enum aclk_alarm_status status;
+ enum aclk_alarm_status old_status;
+ uint64_t delay;
+ uint64_t delay_up_to_timestamp;
+
+ uint64_t last_repeat;
+ int silenced;
+
+ char *value_string;
+ char *old_value_string;
+
+ double value;
+ double old_value;
+
+ // updated alarm entry, when the status of the alarm has been updated by a later entry
+ int updated;
+
+ // rendered_info
+ char *rendered_info;
+
+ char *chart_context;
+ char *chart_name;
+
+ uint64_t event_id;
+ char *transition_id;
+ char *summary;
+};
+
+struct send_alarm_checkpoint {
+ char *node_id;
+ char *claim_id;
+};
+
+struct alarm_checkpoint {
+ char *node_id;
+ char *claim_id;
+ char *checksum;
+};
+
+struct send_alarm_snapshot {
+ char *node_id;
+ char *claim_id;
+ char *snapshot_uuid;
+};
+
+struct alarm_snapshot {
+ char *node_id;
+ char *claim_id;
+ char *snapshot_uuid;
+ uint32_t chunks;
+ uint32_t chunk;
+};
+
+typedef void* alarm_snapshot_proto_ptr_t;
+
+void destroy_alarm_log_entry(struct alarm_log_entry *entry);
+
+char *generate_alarm_log_entry(size_t *len, struct alarm_log_entry *data);
+
+struct send_alarm_snapshot *parse_send_alarm_snapshot(const char *data, size_t len);
+void destroy_send_alarm_snapshot(struct send_alarm_snapshot *ptr);
+
+struct send_alarm_checkpoint parse_send_alarm_checkpoint(const char *data, size_t len);
+char *generate_alarm_checkpoint(size_t *len, struct alarm_checkpoint *data);
+
+alarm_snapshot_proto_ptr_t generate_alarm_snapshot_proto(struct alarm_snapshot *data);
+void add_alarm_log_entry2snapshot(alarm_snapshot_proto_ptr_t snapshot, struct alarm_log_entry *data);
+char *generate_alarm_snapshot_bin(size_t *len, alarm_snapshot_proto_ptr_t snapshot);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPER_ALARM_STREAM_H */
diff --git a/src/aclk/schema-wrappers/capability.cc b/src/aclk/schema-wrappers/capability.cc
new file mode 100644
index 000000000..af45740a9
--- /dev/null
+++ b/src/aclk/schema-wrappers/capability.cc
@@ -0,0 +1,11 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "proto/aclk/v1/lib.pb.h"
+
+#include "capability.h"
+
+void capability_set(aclk_lib::v1::Capability *proto_capa, const struct capability *c_capa) {
+ proto_capa->set_name(c_capa->name);
+ proto_capa->set_enabled(c_capa->enabled);
+ proto_capa->set_version(c_capa->version);
+}
diff --git a/src/aclk/schema-wrappers/capability.h b/src/aclk/schema-wrappers/capability.h
new file mode 100644
index 000000000..c6085a44b
--- /dev/null
+++ b/src/aclk/schema-wrappers/capability.h
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_CAPABILITY_H
+#define ACLK_SCHEMA_CAPABILITY_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct capability {
+ const char *name;
+ uint32_t version;
+ int enabled;
+};
+
+#ifdef __cplusplus
+}
+
+#include "proto/aclk/v1/lib.pb.h"
+
+void capability_set(aclk_lib::v1::Capability *proto_capa, const struct capability *c_capa);
+#endif
+
+#endif /* ACLK_SCHEMA_CAPABILITY_H */
diff --git a/src/aclk/schema-wrappers/connection.cc b/src/aclk/schema-wrappers/connection.cc
new file mode 100644
index 000000000..20b40ece2
--- /dev/null
+++ b/src/aclk/schema-wrappers/connection.cc
@@ -0,0 +1,72 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "proto/agent/v1/connection.pb.h"
+#include "proto/agent/v1/disconnect.pb.h"
+#include "connection.h"
+
+#include "schema_wrapper_utils.h"
+
+#include <sys/time.h>
+#include <stdlib.h>
+
+using namespace agent::v1;
+
+char *generate_update_agent_connection(size_t *len, const update_agent_connection_t *data)
+{
+ UpdateAgentConnection connupd;
+
+ connupd.set_claim_id(data->claim_id);
+ connupd.set_reachable(data->reachable);
+ connupd.set_session_id(data->session_id);
+
+ connupd.set_update_source((data->lwt) ? CONNECTION_UPDATE_SOURCE_LWT : CONNECTION_UPDATE_SOURCE_AGENT);
+
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+
+ google::protobuf::Timestamp *timestamp = connupd.mutable_updated_at();
+ timestamp->set_seconds(tv.tv_sec);
+ timestamp->set_nanos(tv.tv_usec * 1000);
+
+ if (data->capabilities) {
+ const struct capability *capa = data->capabilities;
+ while (capa->name) {
+ aclk_lib::v1::Capability *proto_capa = connupd.add_capabilities();
+ capability_set(proto_capa, capa);
+ capa++;
+ }
+ }
+
+ *len = PROTO_COMPAT_MSG_SIZE(connupd);
+ char *msg = (char*)mallocz(*len);
+ if (msg)
+ connupd.SerializeToArray(msg, *len);
+
+ return msg;
+}
+
+struct disconnect_cmd *parse_disconnect_cmd(const char *data, size_t len) {
+ DisconnectReq req;
+ struct disconnect_cmd *res;
+
+ if (!req.ParseFromArray(data, len))
+ return NULL;
+
+ res = (struct disconnect_cmd *)callocz(1, sizeof(struct disconnect_cmd));
+
+ if (!res)
+ return NULL;
+
+ res->reconnect_after_s = req.reconnect_after_seconds();
+ res->permaban = req.permaban();
+ res->error_code = req.error_code();
+ if (req.error_description().c_str()) {
+ res->error_description = strdupz(req.error_description().c_str());
+ if (!res->error_description) {
+ freez(res);
+ return NULL;
+ }
+ }
+
+ return res;
+}
diff --git a/src/aclk/schema-wrappers/connection.h b/src/aclk/schema-wrappers/connection.h
new file mode 100644
index 000000000..0356c7d78
--- /dev/null
+++ b/src/aclk/schema-wrappers/connection.h
@@ -0,0 +1,47 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPER_CONNECTION_H
+#define ACLK_SCHEMA_WRAPPER_CONNECTION_H
+
+#include "capability.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+ const char *claim_id;
+ unsigned int reachable:1;
+
+ int64_t session_id;
+
+ unsigned int lwt:1;
+
+ const struct capability *capabilities;
+
+// TODO in future optional fields
+// > 15 optional fields:
+// How long the system was running until connection (only applicable when reachable=true)
+// google.protobuf.Duration system_uptime = 15;
+// How long the netdata agent was running until connection (only applicable when reachable=true)
+// google.protobuf.Duration agent_uptime = 16;
+
+
+} update_agent_connection_t;
+
+char *generate_update_agent_connection(size_t *len, const update_agent_connection_t *data);
+
+struct disconnect_cmd {
+ uint64_t reconnect_after_s;
+ int permaban;
+ uint32_t error_code;
+ char *error_description;
+};
+
+struct disconnect_cmd *parse_disconnect_cmd(const char *data, size_t len);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPER_CONNECTION_H */
diff --git a/src/aclk/schema-wrappers/context.cc b/src/aclk/schema-wrappers/context.cc
new file mode 100644
index 000000000..b04c9d20c
--- /dev/null
+++ b/src/aclk/schema-wrappers/context.cc
@@ -0,0 +1,125 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "proto/context/v1/context.pb.h"
+
+#include "libnetdata/libnetdata.h"
+
+#include "schema_wrapper_utils.h"
+
+#include "context.h"
+
+using namespace context::v1;
+
+// ContextsSnapshot
+contexts_snapshot_t contexts_snapshot_new(const char *claim_id, const char *node_id, uint64_t version)
+{
+ ContextsSnapshot *ctxs_snap = new ContextsSnapshot;
+
+ if (ctxs_snap == NULL)
+ fatal("Cannot allocate ContextsSnapshot object. OOM");
+
+ ctxs_snap->set_claim_id(claim_id);
+ ctxs_snap->set_node_id(node_id);
+ ctxs_snap->set_version(version);
+
+ return ctxs_snap;
+}
+
+void contexts_snapshot_delete(contexts_snapshot_t snapshot)
+{
+ delete (ContextsSnapshot *)snapshot;
+}
+
+void contexts_snapshot_set_version(contexts_snapshot_t ctxs_snapshot, uint64_t version)
+{
+ ((ContextsSnapshot *)ctxs_snapshot)->set_version(version);
+}
+
+static void fill_ctx_updated(ContextUpdated *ctx, struct context_updated *c_ctx)
+{
+ ctx->set_id(c_ctx->id);
+ ctx->set_version(c_ctx->version);
+ ctx->set_first_entry(c_ctx->first_entry);
+ ctx->set_last_entry(c_ctx->last_entry);
+ ctx->set_deleted(c_ctx->deleted);
+ ctx->set_title(c_ctx->title);
+ ctx->set_priority(c_ctx->priority);
+ ctx->set_chart_type(c_ctx->chart_type);
+ ctx->set_units(c_ctx->units);
+ ctx->set_family(c_ctx->family);
+}
+
+void contexts_snapshot_add_ctx_update(contexts_snapshot_t ctxs_snapshot, struct context_updated *ctx_update)
+{
+ ContextsSnapshot *ctxs_snap = (ContextsSnapshot *)ctxs_snapshot;
+ ContextUpdated *ctx = ctxs_snap->add_contexts();
+
+ fill_ctx_updated(ctx, ctx_update);
+}
+
+char *contexts_snapshot_2bin(contexts_snapshot_t ctxs_snapshot, size_t *len)
+{
+ ContextsSnapshot *ctxs_snap = (ContextsSnapshot *)ctxs_snapshot;
+ *len = PROTO_COMPAT_MSG_SIZE_PTR(ctxs_snap);
+ char *bin = (char*)mallocz(*len);
+ if (!ctxs_snap->SerializeToArray(bin, *len)) {
+ freez(bin);
+ delete ctxs_snap;
+ return NULL;
+ }
+
+ delete ctxs_snap;
+ return bin;
+}
+
+// ContextsUpdated
+contexts_updated_t contexts_updated_new(const char *claim_id, const char *node_id, uint64_t version_hash, uint64_t created_at)
+{
+ ContextsUpdated *ctxs_updated = new ContextsUpdated;
+
+ if (ctxs_updated == NULL)
+ fatal("Cannot allocate ContextsUpdated object. OOM");
+
+ ctxs_updated->set_claim_id(claim_id);
+ ctxs_updated->set_node_id(node_id);
+ ctxs_updated->set_version_hash(version_hash);
+ ctxs_updated->set_created_at(created_at);
+
+ return ctxs_updated;
+}
+
+void contexts_updated_delete(contexts_updated_t ctxs_updated)
+{
+ delete (ContextsUpdated *)ctxs_updated;
+}
+
+void contexts_updated_update_version_hash(contexts_updated_t ctxs_updated, uint64_t version_hash)
+{
+ ((ContextsUpdated *)ctxs_updated)->set_version_hash(version_hash);
+}
+
+void contexts_updated_add_ctx_update(contexts_updated_t ctxs_updated, struct context_updated *ctx_update)
+{
+ ContextsUpdated *ctxs_update = (ContextsUpdated *)ctxs_updated;
+ ContextUpdated *ctx = ctxs_update->add_contextupdates();
+
+ if (ctx == NULL)
+ fatal("Cannot allocate ContextUpdated object. OOM");
+
+ fill_ctx_updated(ctx, ctx_update);
+}
+
+char *contexts_updated_2bin(contexts_updated_t ctxs_updated, size_t *len)
+{
+ ContextsUpdated *ctxs_update = (ContextsUpdated *)ctxs_updated;
+ *len = PROTO_COMPAT_MSG_SIZE_PTR(ctxs_update);
+ char *bin = (char*)mallocz(*len);
+ if (!ctxs_update->SerializeToArray(bin, *len)) {
+ freez(bin);
+ delete ctxs_update;
+ return NULL;
+ }
+
+ delete ctxs_update;
+ return bin;
+}
diff --git a/src/aclk/schema-wrappers/context.h b/src/aclk/schema-wrappers/context.h
new file mode 100644
index 000000000..cbb7701a8
--- /dev/null
+++ b/src/aclk/schema-wrappers/context.h
@@ -0,0 +1,53 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPER_CONTEXT_H
+#define ACLK_SCHEMA_WRAPPER_CONTEXT_H
+
+#include <stdint.h>
+#include <sys/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef void* contexts_updated_t;
+typedef void* contexts_snapshot_t;
+
+struct context_updated {
+ // context id
+ const char *id;
+
+ uint64_t version;
+
+ uint64_t first_entry;
+ uint64_t last_entry;
+
+ int deleted;
+
+ const char *title;
+ uint64_t priority;
+ const char *chart_type;
+ const char *units;
+ const char *family;
+};
+
+// ContextS Snapshot related
+contexts_snapshot_t contexts_snapshot_new(const char *claim_id, const char *node_id, uint64_t version);
+void contexts_snapshot_delete(contexts_snapshot_t ctxs_snapshot);
+void contexts_snapshot_set_version(contexts_snapshot_t ctxs_snapshot, uint64_t version);
+void contexts_snapshot_add_ctx_update(contexts_snapshot_t ctxs_snapshot, struct context_updated *ctx_update);
+char *contexts_snapshot_2bin(contexts_snapshot_t ctxs_snapshot, size_t *len);
+
+// ContextS Updated related
+contexts_updated_t contexts_updated_new(const char *claim_id, const char *node_id, uint64_t version_hash, uint64_t created_at);
+void contexts_updated_delete(contexts_updated_t ctxs_updated);
+void contexts_updated_update_version_hash(contexts_updated_t ctxs_updated, uint64_t version_hash);
+void contexts_updated_add_ctx_update(contexts_updated_t ctxs_updated, struct context_updated *ctx_update);
+char *contexts_updated_2bin(contexts_updated_t ctxs_updated, size_t *len);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPER_CONTEXT_H */
diff --git a/src/aclk/schema-wrappers/context_stream.cc b/src/aclk/schema-wrappers/context_stream.cc
new file mode 100644
index 000000000..3bb1956cb
--- /dev/null
+++ b/src/aclk/schema-wrappers/context_stream.cc
@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "proto/context/v1/stream.pb.h"
+
+#include "context_stream.h"
+
+#include "libnetdata/libnetdata.h"
+
+struct stop_streaming_ctxs *parse_stop_streaming_ctxs(const char *data, size_t len)
+{
+ context::v1::StopStreamingContexts msg;
+
+ struct stop_streaming_ctxs *res;
+
+ if (!msg.ParseFromArray(data, len))
+ return NULL;
+
+ res = (struct stop_streaming_ctxs *)callocz(1, sizeof(struct stop_streaming_ctxs));
+
+ res->claim_id = strdupz(msg.claim_id().c_str());
+ res->node_id = strdupz(msg.node_id().c_str());
+
+ return res;
+}
+
+struct ctxs_checkpoint *parse_ctxs_checkpoint(const char *data, size_t len)
+{
+ context::v1::ContextsCheckpoint msg;
+
+ struct ctxs_checkpoint *res;
+
+ if (!msg.ParseFromArray(data, len))
+ return NULL;
+
+ res = (struct ctxs_checkpoint *)callocz(1, sizeof(struct ctxs_checkpoint));
+
+ res->claim_id = strdupz(msg.claim_id().c_str());
+ res->node_id = strdupz(msg.node_id().c_str());
+ res->version_hash = msg.version_hash();
+
+ return res;
+}
diff --git a/src/aclk/schema-wrappers/context_stream.h b/src/aclk/schema-wrappers/context_stream.h
new file mode 100644
index 000000000..8c691d2cc
--- /dev/null
+++ b/src/aclk/schema-wrappers/context_stream.h
@@ -0,0 +1,36 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPER_CONTEXT_STREAM_H
+#define ACLK_SCHEMA_WRAPPER_CONTEXT_STREAM_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct stop_streaming_ctxs {
+ char *claim_id;
+ char *node_id;
+ // we omit reason as there is only one defined at this point
+ // as soon as there is more than one defined in StopStreaminContextsReason
+ // we should add it
+ // 0 - RATE_LIMIT_EXCEEDED
+};
+
+struct stop_streaming_ctxs *parse_stop_streaming_ctxs(const char *data, size_t len);
+
+struct ctxs_checkpoint {
+ char *claim_id;
+ char *node_id;
+
+ uint64_t version_hash;
+};
+
+struct ctxs_checkpoint *parse_ctxs_checkpoint(const char *data, size_t len);
+
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPER_CONTEXT_STREAM_H */
diff --git a/src/aclk/schema-wrappers/node_connection.cc b/src/aclk/schema-wrappers/node_connection.cc
new file mode 100644
index 000000000..db1fa6449
--- /dev/null
+++ b/src/aclk/schema-wrappers/node_connection.cc
@@ -0,0 +1,46 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "proto/nodeinstance/connection/v1/connection.pb.h"
+#include "node_connection.h"
+
+#include "schema_wrapper_utils.h"
+
+#include <sys/time.h>
+#include <stdlib.h>
+
+char *generate_node_instance_connection(size_t *len, const node_instance_connection_t *data) {
+ nodeinstance::v1::UpdateNodeInstanceConnection msg;
+
+ if(data->claim_id)
+ msg.set_claim_id(data->claim_id);
+ msg.set_node_id(data->node_id);
+
+ msg.set_liveness(data->live);
+ msg.set_queryable(data->queryable);
+
+ msg.set_session_id(data->session_id);
+ msg.set_hops(data->hops);
+
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+
+ google::protobuf::Timestamp *timestamp = msg.mutable_updated_at();
+ timestamp->set_seconds(tv.tv_sec);
+ timestamp->set_nanos(tv.tv_usec * 1000);
+
+ if (data->capabilities) {
+ const struct capability *capa = data->capabilities;
+ while (capa->name) {
+ aclk_lib::v1::Capability *proto_capa = msg.add_capabilities();
+ capability_set(proto_capa, capa);
+ capa++;
+ }
+ }
+
+ *len = PROTO_COMPAT_MSG_SIZE(msg);
+ char *bin = (char*)mallocz(*len);
+ if (bin)
+ msg.SerializeToArray(bin, *len);
+
+ return bin;
+}
diff --git a/src/aclk/schema-wrappers/node_connection.h b/src/aclk/schema-wrappers/node_connection.h
new file mode 100644
index 000000000..dac0d8fe0
--- /dev/null
+++ b/src/aclk/schema-wrappers/node_connection.h
@@ -0,0 +1,32 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPER_NODE_CONNECTION_H
+#define ACLK_SCHEMA_WRAPPER_NODE_CONNECTION_H
+
+#include "capability.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+ const char* claim_id;
+ const char* node_id;
+
+ unsigned int live:1;
+ unsigned int queryable:1;
+
+ int64_t session_id;
+
+ int32_t hops;
+ const struct capability *capabilities;
+} node_instance_connection_t;
+
+char *generate_node_instance_connection(size_t *len, const node_instance_connection_t *data);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPER_NODE_CONNECTION_H */
diff --git a/src/aclk/schema-wrappers/node_creation.cc b/src/aclk/schema-wrappers/node_creation.cc
new file mode 100644
index 000000000..5ad25b7e5
--- /dev/null
+++ b/src/aclk/schema-wrappers/node_creation.cc
@@ -0,0 +1,39 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "proto/nodeinstance/create/v1/creation.pb.h"
+#include "node_creation.h"
+
+#include "schema_wrapper_utils.h"
+
+#include <stdlib.h>
+
+char *generate_node_instance_creation(size_t *len, const node_instance_creation_t *data)
+{
+ nodeinstance::create::v1::CreateNodeInstance msg;
+
+ if (data->claim_id)
+ msg.set_claim_id(data->claim_id);
+ msg.set_machine_guid(data->machine_guid);
+ msg.set_hostname(data->hostname);
+ msg.set_hops(data->hops);
+
+ *len = PROTO_COMPAT_MSG_SIZE(msg);
+ char *bin = (char*)mallocz(*len);
+ if (bin)
+ msg.SerializeToArray(bin, *len);
+
+ return bin;
+}
+
+node_instance_creation_result_t parse_create_node_instance_result(const char *data, size_t len)
+{
+ nodeinstance::create::v1::CreateNodeInstanceResult msg;
+ node_instance_creation_result_t res = { .node_id = NULL, .machine_guid = NULL };
+
+ if (!msg.ParseFromArray(data, len))
+ return res;
+
+ res.node_id = strdupz(msg.node_id().c_str());
+ res.machine_guid = strdupz(msg.machine_guid().c_str());
+ return res;
+}
diff --git a/src/aclk/schema-wrappers/node_creation.h b/src/aclk/schema-wrappers/node_creation.h
new file mode 100644
index 000000000..7a8c7f7c7
--- /dev/null
+++ b/src/aclk/schema-wrappers/node_creation.h
@@ -0,0 +1,31 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPER_NODE_CREATION_H
+#define ACLK_SCHEMA_WRAPPER_NODE_CREATION_H
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+typedef struct {
+ const char *claim_id;
+ const char *machine_guid;
+ const char *hostname;
+
+ int32_t hops;
+} node_instance_creation_t;
+
+typedef struct {
+ char *node_id;
+ char *machine_guid;
+} node_instance_creation_result_t;
+
+char *generate_node_instance_creation(size_t *len, const node_instance_creation_t *data);
+node_instance_creation_result_t parse_create_node_instance_result(const char *data, size_t len);
+
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPER_NODE_CREATION_H */
diff --git a/src/aclk/schema-wrappers/node_info.cc b/src/aclk/schema-wrappers/node_info.cc
new file mode 100644
index 000000000..5e321f688
--- /dev/null
+++ b/src/aclk/schema-wrappers/node_info.cc
@@ -0,0 +1,136 @@
+#include "node_info.h"
+
+#include "proto/nodeinstance/info/v1/info.pb.h"
+
+#include "schema_wrapper_utils.h"
+
+static int generate_node_info(nodeinstance::info::v1::NodeInfo *info, struct aclk_node_info *data)
+{
+ google::protobuf::Map<std::string, std::string> *map;
+
+ if (data->name)
+ info->set_name(data->name);
+
+ if (data->os)
+ info->set_os(data->os);
+ if (data->os_name)
+ info->set_os_name(data->os_name);
+ if (data->os_version)
+ info->set_os_version(data->os_version);
+
+ if (data->kernel_name)
+ info->set_kernel_name(data->kernel_name);
+ if (data->kernel_version)
+ info->set_kernel_version(data->kernel_version);
+
+ if (data->architecture)
+ info->set_architecture(data->architecture);
+
+ info->set_cpus(data->cpus);
+
+ if (data->cpu_frequency)
+ info->set_cpu_frequency(data->cpu_frequency);
+
+ if (data->memory)
+ info->set_memory(data->memory);
+
+ if (data->disk_space)
+ info->set_disk_space(data->disk_space);
+
+ if (data->version)
+ info->set_version(data->version);
+
+ if (data->release_channel)
+ info->set_release_channel(data->release_channel);
+
+ if (data->timezone)
+ info->set_timezone(data->timezone);
+
+ if (data->virtualization_type)
+ info->set_virtualization_type(data->virtualization_type);
+
+ if (data->container_type)
+ info->set_container_type(data->container_type);
+
+ if (data->custom_info)
+ info->set_custom_info(data->custom_info);
+
+ if (data->machine_guid)
+ info->set_machine_guid(data->machine_guid);
+
+ nodeinstance::info::v1::MachineLearningInfo *ml_info = info->mutable_ml_info();
+ ml_info->set_ml_capable(data->ml_info.ml_capable);
+ ml_info->set_ml_enabled(data->ml_info.ml_enabled);
+
+ map = info->mutable_host_labels();
+ rrdlabels_walkthrough_read(data->host_labels_ptr, label_add_to_map_callback, map);
+ return 0;
+}
+
+char *generate_update_node_info_message(size_t *len, struct update_node_info *info)
+{
+ nodeinstance::info::v1::UpdateNodeInfo msg;
+
+ msg.set_node_id(info->node_id);
+ msg.set_claim_id(info->claim_id);
+
+ if (generate_node_info(msg.mutable_data(), &info->data))
+ return NULL;
+
+ set_google_timestamp_from_timeval(info->updated_at, msg.mutable_updated_at());
+ msg.set_machine_guid(info->machine_guid);
+ msg.set_child(info->child);
+
+ nodeinstance::info::v1::MachineLearningInfo *ml_info = msg.mutable_ml_info();
+ ml_info->set_ml_capable(info->ml_info.ml_capable);
+ ml_info->set_ml_enabled(info->ml_info.ml_enabled);
+
+ struct capability *capa;
+ if (info->node_capabilities) {
+ capa = info->node_capabilities;
+ while (capa->name) {
+ aclk_lib::v1::Capability *proto_capa = msg.mutable_node_info()->add_capabilities();
+ capability_set(proto_capa, capa);
+ capa++;
+ }
+ }
+ if (info->node_instance_capabilities) {
+ capa = info->node_instance_capabilities;
+ while (capa->name) {
+ aclk_lib::v1::Capability *proto_capa = msg.mutable_node_instance_info()->add_capabilities();
+ capability_set(proto_capa, capa);
+ capa++;
+ }
+ }
+
+ *len = PROTO_COMPAT_MSG_SIZE(msg);
+ char *bin = (char*)mallocz(*len);
+ if (bin)
+ msg.SerializeToArray(bin, *len);
+
+ return bin;
+}
+
+char *generate_update_node_collectors_message(size_t *len, struct update_node_collectors *upd_node_collectors)
+{
+ nodeinstance::info::v1::UpdateNodeCollectors msg;
+
+ msg.set_node_id(upd_node_collectors->node_id);
+ msg.set_claim_id(upd_node_collectors->claim_id);
+
+ void *colls;
+ dfe_start_read(upd_node_collectors->node_collectors, colls) {
+ struct collector_info *c =(struct collector_info *)colls;
+ nodeinstance::info::v1::CollectorInfo *col = msg.add_collectors();
+ col->set_plugin(c->plugin);
+ col->set_module(c->module);
+ }
+ dfe_done(colls);
+
+ *len = PROTO_COMPAT_MSG_SIZE(msg);
+ char *bin = (char*)mallocz(*len);
+ if (bin)
+ msg.SerializeToArray(bin, *len);
+
+ return bin;
+}
diff --git a/src/aclk/schema-wrappers/node_info.h b/src/aclk/schema-wrappers/node_info.h
new file mode 100644
index 000000000..4f57601df
--- /dev/null
+++ b/src/aclk/schema-wrappers/node_info.h
@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef ACLK_SCHEMA_WRAPPER_NODE_INFO_H
+#define ACLK_SCHEMA_WRAPPER_NODE_INFO_H
+
+#include <stdlib.h>
+#include <stdint.h>
+
+#include "capability.h"
+#include "database/rrd.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+struct machine_learning_info {
+ bool ml_capable;
+ bool ml_enabled;
+};
+
+struct aclk_node_info {
+ const char *name;
+
+ const char *os;
+ const char *os_name;
+ const char *os_version;
+ const char *kernel_name;
+ const char *kernel_version;
+ const char *architecture;
+ uint32_t cpus;
+ const char *cpu_frequency;
+ const char *memory;
+ const char *disk_space;
+ const char *version;
+ const char *release_channel;
+ const char *timezone;
+ const char *virtualization_type;
+ const char *container_type;
+ const char *custom_info;
+ const char *machine_guid;
+
+ RRDLABELS *host_labels_ptr;
+ struct machine_learning_info ml_info;
+};
+
+struct update_node_info {
+ char *node_id;
+ char *claim_id;
+ struct aclk_node_info data;
+ struct timeval updated_at;
+ char *machine_guid;
+ int child;
+
+ struct machine_learning_info ml_info;
+
+ struct capability *node_capabilities;
+ struct capability *node_instance_capabilities;
+};
+
+struct collector_info {
+ const char *module;
+ const char *plugin;
+};
+
+struct update_node_collectors {
+ char *claim_id;
+ char *node_id;
+ DICTIONARY *node_collectors;
+};
+
+char *generate_update_node_info_message(size_t *len, struct update_node_info *info);
+
+char *generate_update_node_collectors_message(size_t *len, struct update_node_collectors *collectors);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* ACLK_SCHEMA_WRAPPER_NODE_INFO_H */
diff --git a/src/aclk/schema-wrappers/proto_2_json.cc b/src/aclk/schema-wrappers/proto_2_json.cc
new file mode 100644
index 000000000..854396510
--- /dev/null
+++ b/src/aclk/schema-wrappers/proto_2_json.cc
@@ -0,0 +1,88 @@
+#include <google/protobuf/message.h>
+#include <google/protobuf/util/json_util.h>
+
+#include "proto/alarm/v1/config.pb.h"
+#include "proto/alarm/v1/stream.pb.h"
+#include "proto/aclk/v1/lib.pb.h"
+#include "proto/agent/v1/connection.pb.h"
+#include "proto/agent/v1/disconnect.pb.h"
+#include "proto/nodeinstance/connection/v1/connection.pb.h"
+#include "proto/nodeinstance/create/v1/creation.pb.h"
+#include "proto/nodeinstance/info/v1/info.pb.h"
+#include "proto/context/v1/stream.pb.h"
+#include "proto/context/v1/context.pb.h"
+#include "proto/agent/v1/cmds.pb.h"
+
+#include "libnetdata/libnetdata.h"
+
+#include "proto_2_json.h"
+
+using namespace google::protobuf::util;
+
+static google::protobuf::Message *msg_name_to_protomsg(const char *msgname)
+{
+//tx side
+ if (!strcmp(msgname, "UpdateAgentConnection"))
+ return new agent::v1::UpdateAgentConnection;
+ if (!strcmp(msgname, "UpdateNodeInstanceConnection"))
+ return new nodeinstance::v1::UpdateNodeInstanceConnection;
+ if (!strcmp(msgname, "CreateNodeInstance"))
+ return new nodeinstance::create::v1::CreateNodeInstance;
+ if (!strcmp(msgname, "UpdateNodeInfo"))
+ return new nodeinstance::info::v1::UpdateNodeInfo;
+ if (!strcmp(msgname, "AlarmCheckpoint"))
+ return new alarms::v1::AlarmCheckpoint;
+ if (!strcmp(msgname, "ProvideAlarmConfiguration"))
+ return new alarms::v1::ProvideAlarmConfiguration;
+ if (!strcmp(msgname, "AlarmSnapshot"))
+ return new alarms::v1::AlarmSnapshot;
+ if (!strcmp(msgname, "AlarmLogEntry"))
+ return new alarms::v1::AlarmLogEntry;
+ if (!strcmp(msgname, "UpdateNodeCollectors"))
+ return new nodeinstance::info::v1::UpdateNodeCollectors;
+ if (!strcmp(msgname, "ContextsUpdated"))
+ return new context::v1::ContextsUpdated;
+ if (!strcmp(msgname, "ContextsSnapshot"))
+ return new context::v1::ContextsSnapshot;
+
+//rx side
+ if (!strcmp(msgname, "CreateNodeInstanceResult"))
+ return new nodeinstance::create::v1::CreateNodeInstanceResult;
+ if (!strcmp(msgname, "SendNodeInstances"))
+ return new agent::v1::SendNodeInstances;
+ if (!strcmp(msgname, "StartAlarmStreaming"))
+ return new alarms::v1::StartAlarmStreaming;
+ if (!strcmp(msgname, "SendAlarmCheckpoint"))
+ return new alarms::v1::SendAlarmCheckpoint;
+ if (!strcmp(msgname, "SendAlarmConfiguration"))
+ return new alarms::v1::SendAlarmConfiguration;
+ if (!strcmp(msgname, "SendAlarmSnapshot"))
+ return new alarms::v1::SendAlarmSnapshot;
+ if (!strcmp(msgname, "DisconnectReq"))
+ return new agent::v1::DisconnectReq;
+ if (!strcmp(msgname, "ContextsCheckpoint"))
+ return new context::v1::ContextsCheckpoint;
+ if (!strcmp(msgname, "StopStreamingContexts"))
+ return new context::v1::StopStreamingContexts;
+ if (!strcmp(msgname, "CancelPendingRequest"))
+ return new agent::v1::CancelPendingRequest;
+
+ return NULL;
+}
+
+char *protomsg_to_json(const void *protobin, size_t len, const char *msgname)
+{
+ google::protobuf::Message *msg = msg_name_to_protomsg(msgname);
+ if (msg == NULL)
+ return strdupz("Don't know this message type by name.");
+
+ if (!msg->ParseFromArray(protobin, len))
+ return strdupz("Can't parse this message. Malformed or wrong parser used.");
+
+ JsonPrintOptions options;
+
+ std::string output;
+ google::protobuf::util::MessageToJsonString(*msg, &output, options);
+ delete msg;
+ return strdupz(output.c_str());
+}
diff --git a/src/aclk/schema-wrappers/proto_2_json.h b/src/aclk/schema-wrappers/proto_2_json.h
new file mode 100644
index 000000000..3bd98478c
--- /dev/null
+++ b/src/aclk/schema-wrappers/proto_2_json.h
@@ -0,0 +1,18 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef PROTO_2_JSON_H
+#define PROTO_2_JSON_H
+
+#include <sys/types.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+char *protomsg_to_json(const void *protobin, size_t len, const char *msgname);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* PROTO_2_JSON_H */
diff --git a/src/aclk/schema-wrappers/schema_wrapper_utils.cc b/src/aclk/schema-wrappers/schema_wrapper_utils.cc
new file mode 100644
index 000000000..96a4b9bf1
--- /dev/null
+++ b/src/aclk/schema-wrappers/schema_wrapper_utils.cc
@@ -0,0 +1,22 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "schema_wrapper_utils.h"
+
+void set_google_timestamp_from_timeval(struct timeval tv, google::protobuf::Timestamp *ts)
+{
+ ts->set_nanos(tv.tv_usec*1000);
+ ts->set_seconds(tv.tv_sec);
+}
+
+void set_timeval_from_google_timestamp(const google::protobuf::Timestamp &ts, struct timeval *tv)
+{
+ tv->tv_sec = ts.seconds();
+ tv->tv_usec = ts.nanos()/1000;
+}
+
+int label_add_to_map_callback(const char *name, const char *value, RRDLABEL_SRC ls __maybe_unused, void *data)
+{
+ auto map = (google::protobuf::Map<std::string, std::string> *)data;
+ map->insert({name, value});
+ return 1;
+}
diff --git a/src/aclk/schema-wrappers/schema_wrapper_utils.h b/src/aclk/schema-wrappers/schema_wrapper_utils.h
new file mode 100644
index 000000000..693a4ce5f
--- /dev/null
+++ b/src/aclk/schema-wrappers/schema_wrapper_utils.h
@@ -0,0 +1,24 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#ifndef SCHEMA_WRAPPER_UTILS_H
+#define SCHEMA_WRAPPER_UTILS_H
+
+#include "database/rrd.h"
+
+#include <sys/time.h>
+#include <google/protobuf/timestamp.pb.h>
+#include <google/protobuf/map.h>
+
+#if GOOGLE_PROTOBUF_VERSION < 3001000
+#define PROTO_COMPAT_MSG_SIZE(msg) (size_t)msg.ByteSize()
+#define PROTO_COMPAT_MSG_SIZE_PTR(msg) (size_t)msg->ByteSize()
+#else
+#define PROTO_COMPAT_MSG_SIZE(msg) msg.ByteSizeLong()
+#define PROTO_COMPAT_MSG_SIZE_PTR(msg) msg->ByteSizeLong()
+#endif
+
+void set_google_timestamp_from_timeval(struct timeval tv, google::protobuf::Timestamp *ts);
+void set_timeval_from_google_timestamp(const google::protobuf::Timestamp &ts, struct timeval *tv);
+int label_add_to_map_callback(const char *name, const char *value, RRDLABEL_SRC ls, void *data);
+
+#endif /* SCHEMA_WRAPPER_UTILS_H */
diff --git a/src/aclk/schema-wrappers/schema_wrappers.h b/src/aclk/schema-wrappers/schema_wrappers.h
new file mode 100644
index 000000000..b651b8845
--- /dev/null
+++ b/src/aclk/schema-wrappers/schema_wrappers.h
@@ -0,0 +1,19 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// utility header to include all the message wrappers at once
+
+#ifndef SCHEMA_WRAPPERS_H
+#define SCHEMA_WRAPPERS_H
+
+#include "connection.h"
+#include "node_connection.h"
+#include "node_creation.h"
+#include "alarm_config.h"
+#include "alarm_stream.h"
+#include "node_info.h"
+#include "capability.h"
+#include "context_stream.h"
+#include "context.h"
+#include "agent_cmds.h"
+
+#endif /* SCHEMA_WRAPPERS_H */