/* xdrv_02_mqtt.ino - mqtt support for Tasmota Copyright (C) 2021 Theo Arends 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, either version 3 of the License, or (at your option) any later version. 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 . */ #define XDRV_02 2 #ifndef MQTT_WIFI_CLIENT_TIMEOUT #define MQTT_WIFI_CLIENT_TIMEOUT 200 // Wifi TCP connection timeout (default is 5000 mSec) #endif #include #define USE_MQTT_NEW_PUBSUBCLIENT // #define DEBUG_DUMP_TLS // allow dumping of TLS Flash keys #ifdef USE_MQTT_TLS #include "WiFiClientSecureLightBearSSL.h" BearSSL::WiFiClientSecure_light *tlsClient; #endif WiFiClient EspClient; // Wifi Client - non-TLS #ifdef USE_MQTT_AZURE_IOT #undef MQTT_PORT #define MQTT_PORT 8883 #if defined(USE_MQTT_AZURE_DPS_SCOPEID) && defined(USE_MQTT_AZURE_DPS_PRESHAREDKEY) #include // dedicated tlsHttpsClient for DPS as the 'tlsClient' above causes error '-1' in httpsClient after it is associated with PubSub. It cost ~5K of heap BearSSL::WiFiClientSecure_light *tlsHttpsClient = new BearSSL::WiFiClientSecure_light(1024,1024); HTTPClient httpsClient; int httpsClientReturn; #endif // USE_MQTT_AZURE_DPS_SCOPEID #include #include #endif // USE_MQTT_AZURE_IOT const char kMqttCommands[] PROGMEM = "|" // No prefix // SetOption synonyms D_SO_MQTTJSONONLY "|" #ifdef USE_MQTT_TLS D_SO_MQTTTLS "|" #endif D_SO_MQTTNORETAIN "|" D_SO_MQTTDETACHRELAY "|" // regular commands #if defined(USE_MQTT_TLS) && !defined(USE_MQTT_TLS_CA_CERT) D_CMND_MQTTFINGERPRINT "|" #endif D_CMND_MQTTUSER "|" D_CMND_MQTTPASSWORD "|" D_CMND_MQTTKEEPALIVE "|" D_CMND_MQTTTIMEOUT "|" #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) D_CMND_TLSKEY "|" #endif D_CMND_MQTTHOST "|" D_CMND_MQTTPORT "|" D_CMND_MQTTRETRY "|" D_CMND_STATETEXT "|" D_CMND_MQTTCLIENT "|" D_CMND_FULLTOPIC "|" D_CMND_PREFIX "|" D_CMND_GROUPTOPIC "|" D_CMND_TOPIC "|" D_CMND_PUBLISH "|" D_CMND_MQTTLOG "|" D_CMND_BUTTONTOPIC "|" D_CMND_SWITCHTOPIC "|" D_CMND_BUTTONRETAIN "|" D_CMND_SWITCHRETAIN "|" D_CMND_POWERRETAIN "|" D_CMND_SENSORRETAIN "|" D_CMND_INFORETAIN "|" D_CMND_STATERETAIN "|" D_CMND_FILEUPLOAD "|" D_CMND_FILEDOWNLOAD ; SO_SYNONYMS(kMqttSynonyms, 90, #ifdef USE_MQTT_TLS 103, #endif 104, 114 ); // const uint8_t kMqttSynonyms[] PROGMEM = { // 4, // number of synonyms // 90, 103, 104, 114, // }; void (* const MqttCommand[])(void) PROGMEM = { #if defined(USE_MQTT_TLS) && !defined(USE_MQTT_TLS_CA_CERT) &CmndMqttFingerprint, #endif &CmndMqttUser, &CmndMqttPassword, &CmndMqttKeepAlive, &CmndMqttTimeout, #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) &CmndTlsKey, #endif &CmndMqttHost, &CmndMqttPort, &CmndMqttRetry, &CmndStateText, &CmndMqttClient, &CmndFullTopic, &CmndPrefix, &CmndGroupTopic, &CmndTopic, &CmndPublish, &CmndMqttlog, &CmndButtonTopic, &CmndSwitchTopic, &CmndButtonRetain, &CmndSwitchRetain, &CmndPowerRetain, &CmndSensorRetain, &CmndInfoRetain, &CmndStateRetain, &CmndFileUpload, &CmndFileDownload }; struct MQTT { uint32_t file_pos = 0; // MQTT file position during upload/download uint32_t file_size = 0; // MQTT total file size uint32_t file_type = 0; // MQTT File type (See UploadTypes) uint8_t* file_buffer = nullptr; // MQTT file buffer MD5Builder md5; // MQTT md5 String file_md5; // MQTT received file md5 (32 chars) uint16_t connect_count = 0; // MQTT re-connect count uint16_t retry_counter = 1; // MQTT connection retry counter uint16_t retry_counter_delay = 1; // MQTT retry counter multiplier uint16_t topic_size; // MQTT topic length with terminating uint8_t initial_connection_state = 2; // MQTT connection messages state uint8_t file_id = 0; // MQTT unique file id during upload/download bool connected = false; // MQTT virtual connection status bool allowed = false; // MQTT enabled and parameters valid bool mqtt_tls = false; // MQTT TLS is enabled } Mqtt; #ifdef USE_MQTT_TLS // This part of code is necessary to store Private Key and Cert in Flash #ifdef USE_MQTT_AWS_IOT //#include const br_ec_private_key *AWS_IoT_Private_Key = nullptr; const br_x509_certificate *AWS_IoT_Client_Certificate = nullptr; class tls_entry_t { public: uint32_t name; // simple 4 letters name. Currently 'skey', 'crt ', 'crt1', 'crt2' uint16_t start; // start offset uint16_t len; // len of object }; // 8 bytes const static uint32_t TLS_NAME_SKEY = 0x2079656B; // 'key ' little endian const static uint32_t TLS_NAME_CRT = 0x20747263; // 'crt ' little endian class tls_dir_t { public: tls_entry_t entry[4]; // 4 entries max, only 4 used today, for future use }; // 4*8 = 64 bytes tls_dir_t tls_dir; // memory copy of tls_dir from flash #endif // USE_MQTT_AWS_IOT // check whether the fingerprint is filled with a single value // Filled with 0x00 = accept any fingerprint and learn it for next time // Filled with 0xFF = accept any fingerpring forever bool is_fingerprint_mono_value(uint8_t finger[20], uint8_t value) { for (uint32_t i = 0; i<20; i++) { if (finger[i] != value) { return false; } } return true; } #endif // USE_MQTT_TLS void MakeValidMqtt(uint32_t option, char* str) { // option 0 = replace by underscore // option 1 = delete character uint32_t i = 0; while (str[i] > 0) { // if ((str[i] == '/') || (str[i] == '+') || (str[i] == '#') || (str[i] == ' ')) { if ((str[i] == '+') || (str[i] == '#') || (str[i] == ' ')) { if (option) { uint32_t j = i; while (str[j] > 0) { str[j] = str[j +1]; j++; } i--; } else { str[i] = '_'; } } i++; } } /*********************************************************************************************\ * MQTT driver specific code need to provide the following functions: * * bool MqttIsConnected() * void MqttDisconnect() * void MqttSubscribeLib(char *topic) * bool MqttPublishLib(const char* topic, bool retained) \*********************************************************************************************/ #include // Max message size calculated by PubSubClient is (MQTT_MAX_PACKET_SIZE < 5 + 2 + strlen(topic) + plength) #if (MQTT_MAX_PACKET_SIZE -TOPSZ -7) < MIN_MESSZ // If the max message size is too small, throw an error at compile time. See PubSubClient.cpp line 359 #error "MQTT_MAX_PACKET_SIZE is too small in libraries/PubSubClient/src/PubSubClient.h, increase it to at least 1200" #endif PubSubClient MqttClient; void MqttInit(void) { #ifdef USE_MQTT_AZURE_IOT Settings.mqtt_port = 8883; #endif //USE_MQTT_AZURE_IOT #ifdef USE_MQTT_TLS if ((8883 == Settings.mqtt_port) || (8884 == Settings.mqtt_port)) { // Turn on TLS for port 8883 (TLS) and 8884 (TLS, client certificate) Settings.flag4.mqtt_tls = true; } Mqtt.mqtt_tls = Settings.flag4.mqtt_tls; // this flag should not change even if we change the SetOption (until reboot) // Detect AWS IoT and set default parameters String host = String(SettingsText(SET_MQTT_HOST)); if (host.indexOf(F(".iot.")) && host.endsWith(F(".amazonaws.com"))) { // look for ".iot." and ".amazonaws.com" in the domain name Settings.flag4.mqtt_no_retain = true; } if (Mqtt.mqtt_tls) { #ifdef ESP32 tlsClient = new BearSSL::WiFiClientSecure_light(2048,2048); #else // ESP32 - ESP8266 tlsClient = new BearSSL::WiFiClientSecure_light(1024,1024); #endif #ifdef USE_MQTT_AWS_IOT loadTlsDir(); // load key and certificate data from Flash if ((nullptr != AWS_IoT_Private_Key) && (nullptr != AWS_IoT_Client_Certificate)) { tlsClient->setClientECCert(AWS_IoT_Client_Certificate, AWS_IoT_Private_Key, 0xFFFF /* all usages, don't care */, 0); } #endif #ifdef USE_MQTT_TLS_CA_CERT tlsClient->setTrustAnchor(Tasmota_TA, nitems(Tasmota_TA)); #endif // USE_MQTT_TLS_CA_CERT MqttClient.setClient(*tlsClient); } else { MqttClient.setClient(EspClient); // non-TLS } #else // USE_MQTT_TLS MqttClient.setClient(EspClient); #endif // USE_MQTT_TLS MqttClient.setKeepAlive(Settings.mqtt_keepalive); MqttClient.setSocketTimeout(Settings.mqtt_socket_timeout); } #ifdef USE_MQTT_AZURE_IOT String Sha256Sign(String dataToSign, String preSharedKey){ AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "sha256 dataToSign is '%s'"), String(dataToSign).c_str()); char dataToSignChar[dataToSign.length() + 1]; dataToSign.toCharArray(dataToSignChar, dataToSign.length() + 1); unsigned char decodedPSK[32]; unsigned char encryptedSignature[100]; unsigned char encodedSignature[100]; br_sha256_context sha256_context; br_hmac_key_context hmac_key_context; br_hmac_context hmac_context; // need to base64 decode the Preshared key and the length int base64_decoded_device_length = decode_base64((unsigned char*)preSharedKey.c_str(), decodedPSK); // create the sha256 hmac and hash the data br_sha256_init(&sha256_context); br_hmac_key_init(&hmac_key_context, sha256_context.vtable, decodedPSK, base64_decoded_device_length); br_hmac_init(&hmac_context, &hmac_key_context, 32); br_hmac_update(&hmac_context, dataToSignChar, sizeof(dataToSignChar)-1); br_hmac_out(&hmac_context, encryptedSignature); // base64 decode the HMAC to a char encode_base64(encryptedSignature, br_hmac_size(&hmac_context), encodedSignature); // creating the real SAS Token AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "sha256 signature is '%s'"), String((char*)encodedSignature).c_str()); return String((char*)encodedSignature); } String urlEncodeBase64(String stringToEncode){ // correctly URL encoding the 64 characters of Base64 and the '=' sign stringToEncode.replace("+", "%2B"); stringToEncode.replace("=", "%3D"); stringToEncode.replace("/", "%2F"); return stringToEncode; } String AzurePSKtoToken(char *iotHubFQDN, const char *deviceId, const char *preSharedKey, int sasTTL = 86400){ int ttl = time(NULL) + sasTTL; String dataToSignString = urlEncodeBase64(String(iotHubFQDN) + "/devices/" + String(deviceId)) + "\n" + String(ttl); String signedData = Sha256Sign(dataToSignString, String(preSharedKey)); // creating the real SAS Token String realSASToken = "SharedAccessSignature sr=" + urlEncodeBase64(String(iotHubFQDN) + "/devices/" + String(deviceId)); realSASToken += "&sig=" + urlEncodeBase64(signedData) + "&se=" + String(ttl); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Azure IoT Hub SAS Token is '%s'"), realSASToken.c_str()); return realSASToken; } #if defined(USE_MQTT_AZURE_DPS_SCOPEID) && defined(USE_MQTT_AZURE_DPS_PRESHAREDKEY) String AzureDSPPSKtoToken(String scopeId, String deviceId, const char *preSharedKey, int sasTTL = 3600){ int ttl = time(NULL) + sasTTL; String dataToSignString = urlEncodeBase64(scopeId + "/registrations/" + deviceId) + "\n" + String(ttl); String signedData = Sha256Sign(dataToSignString, String(preSharedKey)); // creating the real SAS Token String realSASToken = "SharedAccessSignature sr=" + urlEncodeBase64(scopeId + "/registrations/" + deviceId); realSASToken += "&sig=" + urlEncodeBase64(signedData) + "&skn=registration" + "&se=" + String(ttl); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Azure DPS SAS Token is '%s'"), realSASToken.c_str()); return realSASToken; } void ProvisionAzureDPS(){ AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "Starting Azure DPS registration...")); // Scope and Key are derived from user_config_override.h, USE_MQTT_AZURE_DPS_SCOPE_ENDPOINT is optional String dPSScopeId = USE_MQTT_AZURE_DPS_SCOPEID; String dPSPreSharedKey = USE_MQTT_AZURE_DPS_PRESHAREDKEY; #if defined(USE_MQTT_AZURE_DPS_SCOPE_ENDPOINT) String endpoint=USE_MQTT_AZURE_DPS_SCOPE_ENDPOINT; #else String endpoint="https://global.azure-devices-provisioning.net/"; #endif //USE_MQTT_AZURE_DPS_SCOPE_ENDPOINT String MACAddress = WiFi.macAddress(); MACAddress.replace(":", ""); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "DPS register for %s, scope %s to %s."), MACAddress.c_str(), dPSScopeId.c_str(), endpoint.c_str()); // derive our PSK from the DPS and set the device ID String devicePresharedKey = Sha256Sign(MACAddress, dPSPreSharedKey); char devicePresharedKeyChar[devicePresharedKey.length() + 1]; devicePresharedKey.toCharArray(devicePresharedKeyChar, devicePresharedKey.length() + 1); // generate a SAS Token with this new derived key String dPSSASToken = AzureDSPPSKtoToken(dPSScopeId, MACAddress, devicePresharedKey.c_str()); // REST to DPS to start the assigning process String dPSURL = endpoint + dPSScopeId + "/registrations/" + MACAddress + "/register?api-version=2019-03-31"; String dPSPutContent = "{\"registrationId\": \"" + MACAddress + "\"}"; httpsClient.setReuse(true); httpsClient.begin(*tlsHttpsClient, dPSURL); httpsClient.addHeader("User-Agent", "Tasmota"); httpsClient.addHeader("Content-Type", "application/json"); httpsClient.addHeader("Content-Encoding", "utf-8"); httpsClient.addHeader("Authorization", dPSSASToken); httpsClientReturn = httpsClient.PUT(dPSPutContent); String dPSAssigningResponseJSON; if (httpsClientReturn == HTTP_CODE_ACCEPTED){ dPSAssigningResponseJSON = httpsClient.getString(); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "DPS Assigning response '%s'"), dPSAssigningResponseJSON.c_str()); } else { dPSAssigningResponseJSON = httpsClient.getString(); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "DPS Assigning response '%s'"), dPSAssigningResponseJSON.c_str()); AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_MQTT "Azure DPS REST assignment connection failed with code '%d'. Restarting."), httpsClientReturn); WebRestart(1); } if (dPSAssigningResponseJSON.indexOf("\"assigning\"") == -1){ AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_MQTT "Azure DPS assignment failed with response '%s'. Restarting."), dPSAssigningResponseJSON.c_str()); WebRestart(1); } else { AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Azure DPS assignment response '%s'."), dPSAssigningResponseJSON.c_str()); } httpsClient.end(); JsonParser dPSAssigningResponseParser((char*) dPSAssigningResponseJSON.c_str()); JsonParserObject dPSAssigningResponseRoot = dPSAssigningResponseParser.getRootObject(); String dPSAssigningOperationId = dPSAssigningResponseRoot.getStr("operationId"); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "DPS operationId is '%s'."), dPSAssigningOperationId.c_str()); bool assigned = false; int assignedCounter = 1; String dPSAssignedResponseJSON; dPSURL = endpoint + dPSScopeId + "/registrations/" + MACAddress + "/operations/" + dPSAssigningOperationId + "?api-version=2019-03-31"; while (!assigned && assignedCounter < 5){ httpsClient.begin(*tlsHttpsClient, dPSURL); httpsClient.addHeader("User-Agent", "Tasmota"); httpsClient.addHeader("Content-Type", "application/json"); httpsClient.addHeader("Content-Encoding", "utf-8"); httpsClient.addHeader("Authorization", dPSSASToken); httpsClientReturn = httpsClient.GET(); if (httpsClientReturn == HTTP_CODE_OK){ dPSAssignedResponseJSON = httpsClient.getString(); } else if (httpsClientReturn != HTTP_CODE_ACCEPTED){ AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_MQTT "Azure DPS REST check connection failed with code '%d'."), httpsClientReturn); } if (dPSAssignedResponseJSON.indexOf("\"status\":\"assigned\"") > 0){ assigned = true; } else if (httpsClientReturn != HTTP_CODE_ACCEPTED) { AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "DPS try %d, response '%s'."), assignedCounter, dPSAssignedResponseJSON.c_str()); } delay(1000 * assignedCounter); assignedCounter+=1; } httpsClient.end(); if (assigned){ AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Azure DPS registration response '%s'."), dPSAssignedResponseJSON.c_str()); JsonParser parser((char*) dPSAssignedResponseJSON.c_str()); JsonParserObject stateObject = parser.getRootObject()[PSTR("registrationState")].getObject(); String deviceId = stateObject["deviceId"].getStr(); String iotHub = stateObject["assignedHub"].getStr(); bool newProvision = false; if (String(SettingsText(SET_MQTT_PWD)) != devicePresharedKey || String(SettingsText(SET_MQTT_HOST)) != iotHub || String(SettingsText(SET_MQTT_CLIENT)) != deviceId || String(SettingsText(SET_MQTT_USER)) != deviceId) { newProvision = true; SettingsUpdateText(SET_MQTT_PWD, devicePresharedKey.c_str()); SettingsUpdateText(SET_MQTT_HOST, iotHub.c_str()); SettingsUpdateText(SET_MQTT_CLIENT, deviceId.c_str()); SettingsUpdateText(SET_MQTT_USER, deviceId.c_str()); } if (newProvision){ // because this is the first time we have been provisioned must reboot AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "Azure DPS registration success, changed in DPS registration, restarting.")); WebRestart(1); } else { AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "Azure DPS registration success, no changes.")); } } else { AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_MQTT "Azure DPS registration response failed with response '%s'."), dPSAssignedResponseJSON.c_str()); } } #endif // USE_MQTT_AZURE_DPS_SCOPEID #endif // USE_MQTT_AZURE_IOT bool MqttIsConnected(void) { return MqttClient.connected(); } void MqttDisconnect(void) { MqttClient.disconnect(); } void MqttSubscribeLib(const char *topic) { #ifdef USE_MQTT_AZURE_IOT // Azure IoT Hub currently does not support custom topics: https://docs.microsoft.com/en-us/azure/iot-hub/iot-hub-mqtt-support String realTopicString = "devices/" + String(SettingsText(SET_MQTT_CLIENT)); realTopicString += "/messages/devicebound/#"; MqttClient.subscribe(realTopicString.c_str()); SettingsUpdateText(SET_MQTT_FULLTOPIC, SettingsText(SET_MQTT_CLIENT)); SettingsUpdateText(SET_MQTT_TOPIC, SettingsText(SET_MQTT_CLIENT)); #else MqttClient.subscribe(topic); #endif // USE_MQTT_AZURE_IOT MqttClient.loop(); // Solve LmacRxBlk:1 messages } void MqttUnsubscribeLib(const char *topic) { MqttClient.unsubscribe(topic); MqttClient.loop(); // Solve LmacRxBlk:1 messages } bool MqttPublishLib(const char* topic, bool retained) { // If Prefix1 equals Prefix2 disable next MQTT subscription to prevent loop if (!strcmp(SettingsText(SET_MQTTPREFIX1), SettingsText(SET_MQTTPREFIX2))) { char *str = strstr(topic, SettingsText(SET_MQTTPREFIX1)); if (str == topic) { TasmotaGlobal.mqtt_cmnd_blocked_reset = 4; // Allow up to four seconds before resetting residual cmnd blocks TasmotaGlobal.mqtt_cmnd_blocked++; } } bool result; #ifdef USE_MQTT_AZURE_IOT String sourceTopicString = urlEncodeBase64(String(topic)); String topicString = "devices/" + String(SettingsText(SET_MQTT_CLIENT)); topicString+= "/messages/events/topic=" + sourceTopicString; JsonParser mqtt_message((char*) String(TasmotaGlobal.mqtt_data).c_str()); JsonParserObject message_object = mqtt_message.getRootObject(); if (message_object.isValid()) { // only sending valid JSON, yet this is optional result = MqttClient.publish(topicString.c_str(), TasmotaGlobal.mqtt_data, retained); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Sending '%s'"), TasmotaGlobal.mqtt_data); } else { AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Invalid JSON, '%s' for topic '%s', not sending to Azure IoT Hub"), TasmotaGlobal.mqtt_data, topic); result = true; } #else result = MqttClient.publish(topic, TasmotaGlobal.mqtt_data, retained); #endif // USE_MQTT_AZURE_IOT yield(); // #3313 return result; } #ifdef DEBUG_TASMOTA_CORE void MqttDumpData(char* topic, char* data, uint32_t data_len) { char dump_data[data_len +1]; memcpy(dump_data, data, sizeof(dump_data)); // Make another copy for removing optional control characters DEBUG_CORE_LOG(PSTR(D_LOG_MQTT "Size %d, \"%s %s\""), data_len, topic, RemoveControlCharacter(dump_data)); } #endif void MqttDataHandler(char* mqtt_topic, uint8_t* mqtt_data, unsigned int data_len) { #ifdef USE_DEBUG_DRIVER ShowFreeMem(PSTR("MqttDataHandler")); #endif // Do not allow more data than would be feasable within stack space if (data_len >= MQTT_MAX_PACKET_SIZE) { return; } // Do not execute multiple times if Prefix1 equals Prefix2 if (!strcmp(SettingsText(SET_MQTTPREFIX1), SettingsText(SET_MQTTPREFIX2))) { char *str = strstr(mqtt_topic, SettingsText(SET_MQTTPREFIX1)); if ((str == mqtt_topic) && TasmotaGlobal.mqtt_cmnd_blocked) { TasmotaGlobal.mqtt_cmnd_blocked--; return; } } // Save MQTT data ASAP as it's data is discarded by PubSubClient with next publish as used in MQTTlog char topic[TOPSZ]; #ifdef USE_MQTT_AZURE_IOT // for Azure, we read the topic from the property of the message String fullTopicString = String(mqtt_topic); String toppicUpper = fullTopicString; toppicUpper.toUpperCase(); int startOfTopic = toppicUpper.indexOf("TOPIC="); if (startOfTopic == -1){ AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_MQTT "Azure IoT message without the property topic.")); return; } String newTopic = fullTopicString.substring(startOfTopic + 6); newTopic.replace("%2F", "/"); if (newTopic.indexOf("/") == -1){ AddLog(LOG_LEVEL_ERROR, PSTR(D_LOG_MQTT "Invalid Topic %s"), newTopic.c_str()); return; } strlcpy(topic, newTopic.c_str(), sizeof(topic)); #else strlcpy(topic, mqtt_topic, sizeof(topic)); #endif // USE_MQTT_AZURE_IOT Mqtt.topic_size = strlen(topic) + 1; mqtt_data[data_len] = 0; char data[data_len +1]; memcpy(data, mqtt_data, sizeof(data)); #ifdef DEBUG_TASMOTA_CORE MqttDumpData(topic, data, data_len); // Use a function to save stack space used by dump_data #endif // MQTT pre-processing XdrvMailbox.index = strlen(topic); XdrvMailbox.data_len = data_len; XdrvMailbox.topic = topic; XdrvMailbox.data = (char*)data; if (XdrvCall(FUNC_MQTT_DATA)) { return; } ShowSource(SRC_MQTT); CommandHandler(topic, data, data_len); } /*********************************************************************************************/ void MqttRetryCounter(uint8_t value) { Mqtt.retry_counter = value; } void MqttSubscribe(const char *topic) { AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT D_SUBSCRIBE_TO " %s"), topic); MqttSubscribeLib(topic); } void MqttUnsubscribe(const char *topic) { AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT D_UNSUBSCRIBE_FROM " %s"), topic); MqttUnsubscribeLib(topic); } void MqttPublishLoggingAsync(bool refresh) { static uint32_t index = 1; if (!Settings.mqttlog_level || !Settings.flag.mqtt_enabled || !Mqtt.connected) { return; } // SetOption3 - Enable MQTT if (refresh && !NeedLogRefresh(Settings.mqttlog_level, index)) { return; } char* line; size_t len; while (GetLog(Settings.mqttlog_level, &index, &line, &len)) { strlcpy(TasmotaGlobal.mqtt_data, line, len); // No JSON and ugly!! char stopic[TOPSZ]; GetTopic_P(stopic, STAT, TasmotaGlobal.mqtt_topic, PSTR("LOGGING")); MqttPublishLib(stopic, false); } } void MqttPublish(const char* topic, bool retained) { #ifdef USE_DEBUG_DRIVER ShowFreeMem(PSTR("MqttPublish")); #endif if (Settings.flag4.mqtt_no_retain) { // SetOption104 - Disable all MQTT retained messages, some brokers don't support it: AWS IoT, Losant retained = false; // Some brokers don't support retained, they will disconnect if received } String log_data; // 20210420 Moved to heap to solve tight stack resulting in exception 2 if (Settings.flag.mqtt_enabled && MqttPublishLib(topic, retained)) { // SetOption3 - Enable MQTT log_data = F(D_LOG_MQTT); // MQT: log_data += topic; // stat/tasmota/STATUS2 } else { log_data = F(D_LOG_RESULT); // RSL: log_data += strrchr(topic,'/')+1; // STATUS2 retained = false; // Without MQTT enabled there is no retained message } log_data += F(" = "); // = log_data += TasmotaGlobal.mqtt_data; // {"StatusFWR":{"Version":... if (retained) { log_data += F(" (" D_RETAINED ")"); } // (retained) AddLogData(LOG_LEVEL_INFO, log_data.c_str()); // MQT: stat/tasmota/STATUS2 = {"StatusFWR":{"Version":... if (Settings.ledstate &0x04) { TasmotaGlobal.blinks++; } } void MqttPublish(const char* topic) { MqttPublish(topic, false); } void MqttPublishPrefixTopic_P(uint32_t prefix, const char* subtopic, bool retained) { /* prefix 0 = cmnd using subtopic * prefix 1 = stat using subtopic * prefix 2 = tele using subtopic * prefix 4 = cmnd using subtopic or RESULT * prefix 5 = stat using subtopic or RESULT * prefix 6 = tele using subtopic or RESULT */ char romram[64]; snprintf_P(romram, sizeof(romram), ((prefix > 3) && !Settings.flag.mqtt_response) ? S_RSLT_RESULT : subtopic); // SetOption4 - Switch between MQTT RESULT or COMMAND UpperCase(romram, romram); prefix &= 3; char stopic[TOPSZ]; GetTopic_P(stopic, prefix, TasmotaGlobal.mqtt_topic, romram); MqttPublish(stopic, retained); #if defined(USE_MQTT_AWS_IOT) || defined(USE_MQTT_AWS_IOT_LIGHT) if ((prefix > 0) && (Settings.flag4.awsiot_shadow) && (Mqtt.connected)) { // placeholder for SetOptionXX // compute the target topic char *topic = SettingsText(SET_MQTT_TOPIC); char topic2[strlen(topic)+1]; // save buffer onto stack strcpy(topic2, topic); // replace any '/' with '_' char *s = topic2; while (*s) { if ('/' == *s) { *s = '_'; } s++; } // update topic is "$aws/things//shadow/update" snprintf_P(romram, sizeof(romram), PSTR("$aws/things/%s/shadow/update"), topic2); // copy buffer char *mqtt_save = (char*) malloc(strlen(TasmotaGlobal.mqtt_data)+1); if (!mqtt_save) { return; } // abort strcpy(mqtt_save, TasmotaGlobal.mqtt_data); snprintf_P(TasmotaGlobal.mqtt_data, sizeof(TasmotaGlobal.mqtt_data), PSTR("{\"state\":{\"reported\":%s}}"), mqtt_save); free(mqtt_save); bool result = MqttClient.publish(romram, TasmotaGlobal.mqtt_data, false); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Updated shadow: %s"), romram); yield(); // #3313 } #endif // USE_MQTT_AWS_IOT } void MqttPublishPrefixTopic_P(uint32_t prefix, const char* subtopic) { MqttPublishPrefixTopic_P(prefix, subtopic, false); } void MqttPublishPrefixTopicRulesProcess_P(uint32_t prefix, const char* subtopic, bool retained) { MqttPublishPrefixTopic_P(prefix, subtopic, retained); XdrvRulesProcess(0); } void MqttPublishPrefixTopicRulesProcess_P(uint32_t prefix, const char* subtopic) { MqttPublishPrefixTopicRulesProcess_P(prefix, subtopic, false); } void MqttPublishTeleSensor(void) { MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); // CMND_SENSORRETAIN } void MqttPublishPowerState(uint32_t device) { char stopic[TOPSZ]; char scommand[33]; if ((device < 1) || (device > TasmotaGlobal.devices_present)) { device = 1; } #ifdef USE_SONOFF_IFAN if (IsModuleIfan() && (device > 1)) { if (GetFanspeed() < MaxFanspeed()) { // 4 occurs when fanspeed is 3 and RC button 2 is pressed #ifdef USE_DOMOTICZ DomoticzUpdateFanState(); // RC Button feedback #endif // USE_DOMOTICZ snprintf_P(scommand, sizeof(scommand), PSTR(D_CMND_FANSPEED)); GetTopic_P(stopic, STAT, TasmotaGlobal.mqtt_topic, (Settings.flag.mqtt_response) ? scommand : S_RSLT_RESULT); // SetOption4 - Switch between MQTT RESULT or COMMAND Response_P(S_JSON_COMMAND_NVALUE, scommand, GetFanspeed()); MqttPublish(stopic); } } else { #endif // USE_SONOFF_IFAN GetPowerDevice(scommand, device, sizeof(scommand), Settings.flag.device_index_enable); // SetOption26 - Switch between POWER or POWER1 GetTopic_P(stopic, STAT, TasmotaGlobal.mqtt_topic, (Settings.flag.mqtt_response) ? scommand : S_RSLT_RESULT); // SetOption4 - Switch between MQTT RESULT or COMMAND Response_P(S_JSON_COMMAND_SVALUE, scommand, GetStateText(bitRead(TasmotaGlobal.power, device -1))); MqttPublish(stopic); if (!Settings.flag4.only_json_message) { // SetOption90 - Disable non-json MQTT response GetTopic_P(stopic, STAT, TasmotaGlobal.mqtt_topic, scommand); Response_P(GetStateText(bitRead(TasmotaGlobal.power, device -1))); MqttPublish(stopic, Settings.flag.mqtt_power_retain); // CMND_POWERRETAIN } #ifdef USE_SONOFF_IFAN } #endif // USE_SONOFF_IFAN } void MqttPublishAllPowerState(void) { for (uint32_t i = 1; i <= TasmotaGlobal.devices_present; i++) { MqttPublishPowerState(i); #ifdef USE_SONOFF_IFAN if (IsModuleIfan()) { break; } // Report status of light relay only #endif // USE_SONOFF_IFAN } } void MqttPublishPowerBlinkState(uint32_t device) { char scommand[33]; if ((device < 1) || (device > TasmotaGlobal.devices_present)) { device = 1; } Response_P(PSTR("{\"%s\":\"" D_JSON_BLINK " %s\"}"), GetPowerDevice(scommand, device, sizeof(scommand), Settings.flag.device_index_enable), GetStateText(bitRead(TasmotaGlobal.blink_mask, device -1))); // SetOption26 - Switch between POWER or POWER1 MqttPublishPrefixTopicRulesProcess_P(RESULT_OR_STAT, S_RSLT_POWER); } /*********************************************************************************************/ uint16_t MqttConnectCount(void) { return Mqtt.connect_count; } void MqttDisconnected(int state) { Mqtt.connected = false; Mqtt.retry_counter = Settings.mqtt_retry * Mqtt.retry_counter_delay; if ((Settings.mqtt_retry * Mqtt.retry_counter_delay) < 120) { Mqtt.retry_counter_delay++; } MqttClient.disconnect(); AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CONNECT_FAILED_TO " %s:%d, rc %d. " D_RETRY_IN " %d " D_UNIT_SECOND), SettingsText(SET_MQTT_HOST), Settings.mqtt_port, state, Mqtt.retry_counter); TasmotaGlobal.rules_flag.mqtt_disconnected = 1; } void MqttConnected(void) { char stopic[TOPSZ]; if (Mqtt.allowed) { AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_CONNECTED)); Mqtt.connected = true; Mqtt.retry_counter = 0; Mqtt.retry_counter_delay = 1; Mqtt.connect_count++; GetTopic_P(stopic, TELE, TasmotaGlobal.mqtt_topic, S_LWT); Response_P(PSTR(MQTT_LWT_ONLINE)); MqttPublish(stopic, true); if (!Settings.flag4.only_json_message) { // SetOption90 - Disable non-json MQTT response // Satisfy iobroker (#299) ResponseClear(); MqttPublishPrefixTopic_P(CMND, S_RSLT_POWER); } GetTopic_P(stopic, CMND, TasmotaGlobal.mqtt_topic, PSTR("#")); MqttSubscribe(stopic); if (strstr_P(SettingsText(SET_MQTT_FULLTOPIC), MQTT_TOKEN_TOPIC) != nullptr) { uint32_t real_index = SET_MQTT_GRP_TOPIC; for (uint32_t i = 0; i < MAX_GROUP_TOPICS; i++) { if (1 == i) { real_index = SET_MQTT_GRP_TOPIC2 -1; } if (strlen(SettingsText(real_index +i))) { GetGroupTopic_P(stopic, PSTR("#"), real_index +i); // SetOption75 0: %prefix%/nothing/%topic% = cmnd/nothing//# or SetOption75 1: cmnd/ MqttSubscribe(stopic); } } GetFallbackTopic_P(stopic, PSTR("#")); MqttSubscribe(stopic); } XdrvCall(FUNC_MQTT_SUBSCRIBE); } if (Mqtt.initial_connection_state) { if (ResetReason() != REASON_DEEP_SLEEP_AWAKE) { char stopic2[TOPSZ]; Response_P(PSTR("{\"Info1\":{\"" D_CMND_MODULE "\":\"%s\",\"" D_JSON_VERSION "\":\"%s%s\",\"" D_JSON_FALLBACKTOPIC "\":\"%s\",\"" D_CMND_GROUPTOPIC "\":\"%s\"}}"), ModuleName().c_str(), TasmotaGlobal.version, TasmotaGlobal.image_name, GetFallbackTopic_P(stopic, ""), GetGroupTopic_P(stopic2, "", SET_MQTT_GRP_TOPIC)); MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_RSLT_INFO "1"), Settings.flag5.mqtt_info_retain); #ifdef USE_WEBSERVER if (Settings.webserver) { #if LWIP_IPV6 Response_P(PSTR("{\"Info2\":{\"" D_JSON_WEBSERVER_MODE "\":\"%s\",\"" D_CMND_HOSTNAME "\":\"%s\",\"" D_CMND_IPADDRESS "\":\"%s\",\"IPv6Address\":\"%s\"}}"), (2 == Settings.webserver) ? PSTR(D_ADMIN) : PSTR(D_USER), NetworkHostname(), NetworkAddress().toString().c_str(), WifiGetIPv6().c_str(), Settings.flag5.mqtt_info_retain); #else Response_P(PSTR("{\"Info2\":{\"" D_JSON_WEBSERVER_MODE "\":\"%s\",\"" D_CMND_HOSTNAME "\":\"%s\",\"" D_CMND_IPADDRESS "\":\"%s\"}}"), (2 == Settings.webserver) ? PSTR(D_ADMIN) : PSTR(D_USER), NetworkHostname(), NetworkAddress().toString().c_str(), Settings.flag5.mqtt_info_retain); #endif // LWIP_IPV6 = 1 MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_RSLT_INFO "2"), Settings.flag5.mqtt_info_retain); } #endif // USE_WEBSERVER Response_P(PSTR("{\"Info3\":{\"" D_JSON_RESTARTREASON "\":")); if (CrashFlag()) { CrashDump(); } else { ResponseAppend_P(PSTR("\"%s\""), GetResetReason().c_str()); } ResponseJsonEndEnd(); MqttPublishPrefixTopicRulesProcess_P(TELE, PSTR(D_RSLT_INFO "3"), Settings.flag5.mqtt_info_retain); } MqttPublishAllPowerState(); if (Settings.tele_period) { TasmotaGlobal.tele_period = Settings.tele_period -5; // Enable TelePeriod in 5 seconds } TasmotaGlobal.rules_flag.system_boot = 1; XdrvCall(FUNC_MQTT_INIT); } Mqtt.initial_connection_state = 0; TasmotaGlobal.global_state.mqtt_down = 0; if (Settings.flag.mqtt_enabled) { // SetOption3 - Enable MQTT TasmotaGlobal.rules_flag.mqtt_connected = 1; } } void MqttReconnect(void) { char stopic[TOPSZ]; Mqtt.allowed = Settings.flag.mqtt_enabled; // SetOption3 - Enable MQTT if (Mqtt.allowed) { #if defined(USE_MQTT_AZURE_DPS_SCOPEID) && defined(USE_MQTT_AZURE_DPS_PRESHAREDKEY) ProvisionAzureDPS(); #endif #ifdef USE_DISCOVERY #ifdef MQTT_HOST_DISCOVERY MqttDiscoverServer(); #endif // MQTT_HOST_DISCOVERY #endif // USE_DISCOVERY if (!strlen(SettingsText(SET_MQTT_HOST)) || !Settings.mqtt_port) { Mqtt.allowed = false; } #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) // don't enable MQTT for AWS IoT if Private Key or Certificate are not set if (Mqtt.mqtt_tls) { if (0 == strlen(SettingsText(SET_MQTT_PWD))) { // we anticipate that an empty password does not make sense with TLS. This avoids failed connections Mqtt.allowed = false; } } #endif } if (!Mqtt.allowed) { MqttConnected(); return; } #ifdef USE_EMULATION UdpDisconnect(); #endif // USE_EMULATION Mqtt.connected = false; Mqtt.retry_counter = Settings.mqtt_retry * Mqtt.retry_counter_delay; TasmotaGlobal.global_state.mqtt_down = 1; #ifdef FIRMWARE_MINIMAL #ifndef USE_MQTT_TLS // Don't try to connect if MQTT requires TLS but TLS is not supported if (Settings.flag4.mqtt_tls) { return; } #endif #endif AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT D_ATTEMPTING_CONNECTION)); char *mqtt_user = nullptr; char *mqtt_pwd = nullptr; if (strlen(SettingsText(SET_MQTT_USER))) { mqtt_user = SettingsText(SET_MQTT_USER); } if (strlen(SettingsText(SET_MQTT_PWD))) { mqtt_pwd = SettingsText(SET_MQTT_PWD); } GetTopic_P(stopic, TELE, TasmotaGlobal.mqtt_topic, S_LWT); Response_P(S_LWT_OFFLINE); if (MqttClient.connected()) { MqttClient.disconnect(); } EspClient.setTimeout(MQTT_WIFI_CLIENT_TIMEOUT); #ifdef USE_MQTT_TLS if (Mqtt.mqtt_tls) { tlsClient->stop(); } else { // EspClient = WiFiClient(); // Wifi Client reconnect issue 4497 (https://github.com/esp8266/Arduino/issues/4497) MqttClient.setClient(EspClient); } #else // EspClient = WiFiClient(); // Wifi Client reconnect issue 4497 (https://github.com/esp8266/Arduino/issues/4497) MqttClient.setClient(EspClient); #endif if (2 == Mqtt.initial_connection_state) { // Executed once just after power on and wifi is connected Mqtt.initial_connection_state = 1; } MqttClient.setCallback(MqttDataHandler); #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) // re-assign private keys in case it was updated in between if (Mqtt.mqtt_tls) { if ((nullptr != AWS_IoT_Private_Key) && (nullptr != AWS_IoT_Client_Certificate)) { tlsClient->setClientECCert(AWS_IoT_Client_Certificate, AWS_IoT_Private_Key, 0xFFFF /* all usages, don't care */, 0); } } #endif MqttClient.setServer(SettingsText(SET_MQTT_HOST), Settings.mqtt_port); uint32_t mqtt_connect_time = millis(); #if defined(USE_MQTT_TLS) && !defined(USE_MQTT_TLS_CA_CERT) bool allow_all_fingerprints; bool learn_fingerprint1; bool learn_fingerprint2; if (Mqtt.mqtt_tls) { allow_all_fingerprints = false; learn_fingerprint1 = is_fingerprint_mono_value(Settings.mqtt_fingerprint[0], 0x00); learn_fingerprint2 = is_fingerprint_mono_value(Settings.mqtt_fingerprint[1], 0x00); allow_all_fingerprints |= is_fingerprint_mono_value(Settings.mqtt_fingerprint[0], 0xff); allow_all_fingerprints |= is_fingerprint_mono_value(Settings.mqtt_fingerprint[1], 0xff); allow_all_fingerprints |= learn_fingerprint1; allow_all_fingerprints |= learn_fingerprint2; tlsClient->setPubKeyFingerprint(Settings.mqtt_fingerprint[0], Settings.mqtt_fingerprint[1], allow_all_fingerprints); } #endif bool lwt_retain = Settings.flag4.mqtt_no_retain ? false : true; // no retained last will if "no_retain" #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) if (Mqtt.mqtt_tls) { if ((nullptr != AWS_IoT_Private_Key) && (nullptr != AWS_IoT_Client_Certificate)) { // if private key is there, we remove user/pwd mqtt_user = nullptr; mqtt_pwd = nullptr; } } #endif #ifdef USE_MQTT_AZURE_IOT String azureMqtt_password = SettingsText(SET_MQTT_PWD); if (azureMqtt_password.indexOf("SharedAccessSignature") == -1) { // assuming a PreSharedKey was provided, calculating a SAS Token into azureMqtt_password AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "Authenticating with an Azure IoT Hub Token")); azureMqtt_password = AzurePSKtoToken(SettingsText(SET_MQTT_HOST), SettingsText(SET_MQTT_CLIENT), SettingsText(SET_MQTT_PWD)); } String azureMqtt_userString = String(SettingsText(SET_MQTT_HOST)) + "/" + String(SettingsText(SET_MQTT_CLIENT)); + "/?api-version=2018-06-30"; if (MqttClient.connect(TasmotaGlobal.mqtt_client, azureMqtt_userString.c_str(), azureMqtt_password.c_str(), stopic, 1, lwt_retain, TasmotaGlobal.mqtt_data, MQTT_CLEAN_SESSION)) { #else if (MqttClient.connect(TasmotaGlobal.mqtt_client, mqtt_user, mqtt_pwd, stopic, 1, lwt_retain, TasmotaGlobal.mqtt_data, MQTT_CLEAN_SESSION)) { #endif // USE_MQTT_AZURE_IOT #ifdef USE_MQTT_TLS if (Mqtt.mqtt_tls) { #ifdef ESP8266 AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "TLS connected in %d ms, max ThunkStack used %d"), millis() - mqtt_connect_time, tlsClient->getMaxThunkStackUse()); #elif defined(ESP32) AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "TLS connected in %d ms, stack low mark %d"), millis() - mqtt_connect_time, uxTaskGetStackHighWaterMark(nullptr)); #endif if (!tlsClient->getMFLNStatus()) { AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "MFLN not supported by TLS server")); } #ifndef USE_MQTT_TLS_CA_CERT // don't bother with fingerprints if using CA validation const uint8_t *recv_fingerprint = tlsClient->getRecvPubKeyFingerprint(); // create a printable version of the fingerprint received char buf_fingerprint[64]; ToHex_P(recv_fingerprint, 20, buf_fingerprint, sizeof(buf_fingerprint), ' '); AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_MQTT "Server fingerprint: %s"), buf_fingerprint); bool learned = false; // If the fingerprint slot is marked for update, we'll do so. // Otherwise, if the fingerprint slot had the magic trust-on-first-use // value, we will save the current fingerprint there, but only if the other fingerprint slot // *didn't* match it. if (recv_fingerprint[20] & 0x1 || (learn_fingerprint1 && 0 != memcmp(recv_fingerprint, Settings.mqtt_fingerprint[1], 20))) { memcpy(Settings.mqtt_fingerprint[0], recv_fingerprint, 20); learned = true; } // As above, but for the other slot. if (recv_fingerprint[20] & 0x2 || (learn_fingerprint2 && 0 != memcmp(recv_fingerprint, Settings.mqtt_fingerprint[0], 20))) { memcpy(Settings.mqtt_fingerprint[1], recv_fingerprint, 20); learned = true; } if (learned) { AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "Fingerprint learned: %s"), buf_fingerprint); SettingsSaveAll(); // save settings } #endif // !USE_MQTT_TLS_CA_CERT } #endif // USE_MQTT_TLS MqttConnected(); } else { #ifdef USE_MQTT_TLS if (Mqtt.mqtt_tls) { AddLog(LOG_LEVEL_INFO, PSTR(D_LOG_MQTT "TLS connection error: %d"), tlsClient->getLastError()); } #endif MqttDisconnected(MqttClient.state()); // status codes are documented here http://pubsubclient.knolleary.net/api.html#state } } void MqttCheck(void) { if (Settings.flag.mqtt_enabled) { // SetOption3 - Enable MQTT if (!MqttIsConnected()) { TasmotaGlobal.global_state.mqtt_down = 1; if (!Mqtt.retry_counter) { MqttReconnect(); } else { Mqtt.retry_counter--; } } else { TasmotaGlobal.global_state.mqtt_down = 0; } } else { TasmotaGlobal.global_state.mqtt_down = 0; if (Mqtt.initial_connection_state) { MqttReconnect(); } } } bool KeyTopicActive(uint32_t key) { // key = 0 - Button topic // key = 1 - Switch topic key &= 1; char key_topic[TOPSZ]; Format(key_topic, SettingsText(SET_MQTT_BUTTON_TOPIC + key), sizeof(key_topic)); return ((strlen(key_topic) != 0) && strcmp(key_topic, "0")); } /*********************************************************************************************\ * Commands \*********************************************************************************************/ #if defined(USE_MQTT_TLS) && !defined(USE_MQTT_TLS_CA_CERT) void CmndMqttFingerprint(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= 2)) { char fingerprint[60]; if ((XdrvMailbox.data_len > 0) && (XdrvMailbox.data_len < sizeof(fingerprint))) { if (SC_DEFAULT == Shortcut()) { memcpy_P(Settings.mqtt_fingerprint[XdrvMailbox.index -1], (1 == XdrvMailbox.index) ? default_fingerprint1 : default_fingerprint2, sizeof(default_fingerprint1)); } else { strlcpy(fingerprint, (SC_CLEAR == Shortcut()) ? "" : XdrvMailbox.data, sizeof(fingerprint)); char *p = fingerprint; for (uint32_t i = 0; i < 20; i++) { Settings.mqtt_fingerprint[XdrvMailbox.index -1][i] = strtol(p, &p, 16); } } TasmotaGlobal.restart_flag = 2; } ResponseCmndIdxChar(ToHex_P((unsigned char *)Settings.mqtt_fingerprint[XdrvMailbox.index -1], 20, fingerprint, sizeof(fingerprint), ' ')); } } #endif void CmndMqttUser(void) { if (XdrvMailbox.data_len > 0) { SettingsUpdateText(SET_MQTT_USER, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? PSTR(MQTT_USER) : XdrvMailbox.data); TasmotaGlobal.restart_flag = 2; } ResponseCmndChar(SettingsText(SET_MQTT_USER)); } void CmndMqttPassword(void) { bool show_asterisk = (2 == XdrvMailbox.index); if (XdrvMailbox.data_len > 0) { SettingsUpdateText(SET_MQTT_PWD, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? PSTR(MQTT_PASS) : XdrvMailbox.data); if (!show_asterisk) { ResponseCmndChar(SettingsText(SET_MQTT_PWD)); } TasmotaGlobal.restart_flag = 2; } else { show_asterisk = true; } if (show_asterisk) { Response_P(S_JSON_COMMAND_ASTERISK, XdrvMailbox.command); } } void CmndMqttKeepAlive(void) { if ((XdrvMailbox.payload >= 1) && (XdrvMailbox.payload <= 100)) { Settings.mqtt_keepalive = XdrvMailbox.payload; #ifdef USE_MQTT_NEW_PUBSUBCLIENT MqttClient.setKeepAlive(Settings.mqtt_keepalive); #endif } ResponseCmndNumber(Settings.mqtt_keepalive); } void CmndMqttTimeout(void) { if ((XdrvMailbox.payload >= 1) && (XdrvMailbox.payload <= 100)) { Settings.mqtt_socket_timeout = XdrvMailbox.payload; #ifdef USE_MQTT_NEW_PUBSUBCLIENT MqttClient.setSocketTimeout(Settings.mqtt_socket_timeout); #endif } ResponseCmndNumber(Settings.mqtt_socket_timeout); } void CmndMqttlog(void) { if ((XdrvMailbox.payload >= LOG_LEVEL_NONE) && (XdrvMailbox.payload <= LOG_LEVEL_DEBUG_MORE)) { Settings.mqttlog_level = XdrvMailbox.payload; } ResponseCmndNumber(Settings.mqttlog_level); } void CmndMqttHost(void) { if (XdrvMailbox.data_len > 0) { SettingsUpdateText(SET_MQTT_HOST, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? MQTT_HOST : XdrvMailbox.data); TasmotaGlobal.restart_flag = 2; } ResponseCmndChar(SettingsText(SET_MQTT_HOST)); } void CmndMqttPort(void) { if ((XdrvMailbox.payload > 0) && (XdrvMailbox.payload < 65536)) { Settings.mqtt_port = (1 == XdrvMailbox.payload) ? MQTT_PORT : XdrvMailbox.payload; TasmotaGlobal.restart_flag = 2; } ResponseCmndNumber(Settings.mqtt_port); } void CmndMqttRetry(void) { if ((XdrvMailbox.payload >= MQTT_RETRY_SECS) && (XdrvMailbox.payload < 32001)) { Settings.mqtt_retry = XdrvMailbox.payload; Mqtt.retry_counter = Settings.mqtt_retry; } ResponseCmndNumber(Settings.mqtt_retry); } void CmndStateText(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_STATE_TEXT)) { if (!XdrvMailbox.usridx) { ResponseCmndAll(SET_STATE_TXT1, MAX_STATE_TEXT); } else { if (XdrvMailbox.data_len > 0) { for (uint32_t i = 0; i <= XdrvMailbox.data_len; i++) { if (XdrvMailbox.data[i] == ' ') XdrvMailbox.data[i] = '_'; } SettingsUpdateText(SET_STATE_TXT1 + XdrvMailbox.index -1, XdrvMailbox.data); } ResponseCmndIdxChar(GetStateText(XdrvMailbox.index -1)); } } } void CmndMqttClient(void) { if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { SettingsUpdateText(SET_MQTT_CLIENT, (SC_DEFAULT == Shortcut()) ? PSTR(MQTT_CLIENT_ID) : XdrvMailbox.data); TasmotaGlobal.restart_flag = 2; } ResponseCmndChar(SettingsText(SET_MQTT_CLIENT)); } void CmndFullTopic(void) { if (XdrvMailbox.data_len > 0) { MakeValidMqtt(1, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, TasmotaGlobal.mqtt_client)) { SetShortcutDefault(); } char stemp1[TOPSZ]; strlcpy(stemp1, (SC_DEFAULT == Shortcut()) ? MQTT_FULLTOPIC : XdrvMailbox.data, sizeof(stemp1)); if (strcmp(stemp1, SettingsText(SET_MQTT_FULLTOPIC))) { Response_P((Settings.flag.mqtt_offline) ? S_LWT_OFFLINE : ""); // SetOption10 - Control MQTT LWT message format MqttPublishPrefixTopic_P(TELE, S_LWT, true); // Offline or remove previous retained topic SettingsUpdateText(SET_MQTT_FULLTOPIC, stemp1); TasmotaGlobal.restart_flag = 2; } } ResponseCmndChar(SettingsText(SET_MQTT_FULLTOPIC)); } void CmndPrefix(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_MQTT_PREFIXES)) { if (!XdrvMailbox.usridx) { ResponseCmndAll(SET_MQTTPREFIX1, MAX_MQTT_PREFIXES); } else { if (XdrvMailbox.data_len > 0) { MakeValidMqtt(0, XdrvMailbox.data); SettingsUpdateText(SET_MQTTPREFIX1 + XdrvMailbox.index -1, (SC_DEFAULT == Shortcut()) ? (1==XdrvMailbox.index) ? PSTR(SUB_PREFIX) : (2==XdrvMailbox.index) ? PSTR(PUB_PREFIX) : PSTR(PUB_PREFIX2) : XdrvMailbox.data); TasmotaGlobal.restart_flag = 2; } ResponseCmndIdxChar(SettingsText(SET_MQTTPREFIX1 + XdrvMailbox.index -1)); } } } void CmndPublish(void) { // Allow wildcard character "#" as space replacement in topic (#10258) // publish cmnd/theo#arends/power 2 ==> publish cmnd/theo arends/power 2 if (XdrvMailbox.data_len > 0) { char *payload_part; char *mqtt_part = strtok_r(XdrvMailbox.data, " ", &payload_part); if (mqtt_part) { char stemp1[TOPSZ]; strlcpy(stemp1, mqtt_part, sizeof(stemp1)); ReplaceChar(stemp1, '#', ' '); if ((payload_part != nullptr) && strlen(payload_part)) { strlcpy(TasmotaGlobal.mqtt_data, payload_part, sizeof(TasmotaGlobal.mqtt_data)); } else { ResponseClear(); } MqttPublish(stemp1, (XdrvMailbox.index == 2)); // ResponseCmndDone(); ResponseClear(); } } } void CmndGroupTopic(void) { if ((XdrvMailbox.index > 0) && (XdrvMailbox.index <= MAX_GROUP_TOPICS)) { if (XdrvMailbox.data_len > 0) { uint32_t settings_text_index = (1 == XdrvMailbox.index) ? SET_MQTT_GRP_TOPIC : SET_MQTT_GRP_TOPIC2 + XdrvMailbox.index - 2; MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, TasmotaGlobal.mqtt_client)) { SetShortcutDefault(); } SettingsUpdateText(settings_text_index, (SC_CLEAR == Shortcut()) ? "" : (SC_DEFAULT == Shortcut()) ? PSTR(MQTT_GRPTOPIC) : XdrvMailbox.data); // Eliminate duplicates, have at least one and fill from index 1 char stemp[MAX_GROUP_TOPICS][TOPSZ]; uint32_t read_index = 0; uint32_t real_index = SET_MQTT_GRP_TOPIC; for (uint32_t i = 0; i < MAX_GROUP_TOPICS; i++) { if (1 == i) { real_index = SET_MQTT_GRP_TOPIC2 -1; } if (strlen(SettingsText(real_index +i))) { bool not_equal = true; for (uint32_t j = 0; j < read_index; j++) { if (!strcmp(SettingsText(real_index +i), stemp[j])) { // Topics are case-sensitive not_equal = false; } } if (not_equal) { strncpy(stemp[read_index], SettingsText(real_index +i), sizeof(stemp[read_index])); read_index++; } } } if (0 == read_index) { SettingsUpdateText(SET_MQTT_GRP_TOPIC, PSTR(MQTT_GRPTOPIC)); } else { uint32_t write_index = 0; uint32_t real_index = SET_MQTT_GRP_TOPIC; for (uint32_t i = 0; i < MAX_GROUP_TOPICS; i++) { if (1 == i) { real_index = SET_MQTT_GRP_TOPIC2 -1; } if (write_index < read_index) { SettingsUpdateText(real_index +i, stemp[write_index]); write_index++; } else { SettingsUpdateText(real_index +i, ""); } } } TasmotaGlobal.restart_flag = 2; } ResponseCmndAll(SET_MQTT_GRP_TOPIC, MAX_GROUP_TOPICS); } } void CmndTopic(void) { if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, TasmotaGlobal.mqtt_client)) { SetShortcutDefault(); } char stemp1[TOPSZ]; strlcpy(stemp1, (SC_DEFAULT == Shortcut()) ? MQTT_TOPIC : XdrvMailbox.data, sizeof(stemp1)); if (strcmp(stemp1, SettingsText(SET_MQTT_TOPIC))) { Response_P((Settings.flag.mqtt_offline) ? S_LWT_OFFLINE : ""); // SetOption10 - Control MQTT LWT message format MqttPublishPrefixTopic_P(TELE, S_LWT, true); // Offline or remove previous retained topic SettingsUpdateText(SET_MQTT_TOPIC, stemp1); TasmotaGlobal.restart_flag = 2; } } ResponseCmndChar(SettingsText(SET_MQTT_TOPIC)); } void CmndButtonTopic(void) { if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, TasmotaGlobal.mqtt_client)) { SetShortcutDefault(); } switch (Shortcut()) { case SC_CLEAR: SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, ""); break; case SC_DEFAULT: SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, TasmotaGlobal.mqtt_topic); break; case SC_USER: SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, MQTT_BUTTON_TOPIC); break; default: SettingsUpdateText(SET_MQTT_BUTTON_TOPIC, XdrvMailbox.data); } } ResponseCmndChar(SettingsText(SET_MQTT_BUTTON_TOPIC)); } void CmndSwitchTopic(void) { if (!XdrvMailbox.grpflg && (XdrvMailbox.data_len > 0)) { MakeValidMqtt(0, XdrvMailbox.data); if (!strcmp(XdrvMailbox.data, TasmotaGlobal.mqtt_client)) { SetShortcutDefault(); } switch (Shortcut()) { case SC_CLEAR: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, ""); break; case SC_DEFAULT: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, TasmotaGlobal.mqtt_topic); break; case SC_USER: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, PSTR(MQTT_SWITCH_TOPIC)); break; default: SettingsUpdateText(SET_MQTT_SWITCH_TOPIC, XdrvMailbox.data); } } ResponseCmndChar(SettingsText(SET_MQTT_SWITCH_TOPIC)); } void CmndButtonRetain(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { if (!XdrvMailbox.payload) { for (uint32_t i = 1; i <= MAX_KEYS; i++) { SendKey(KEY_BUTTON, i, CLEAR_RETAIN); // Clear MQTT retain in broker } } Settings.flag.mqtt_button_retain = XdrvMailbox.payload; // CMND_BUTTONRETAIN } ResponseCmndStateText(Settings.flag.mqtt_button_retain); // CMND_BUTTONRETAIN } void CmndSwitchRetain(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { if (!XdrvMailbox.payload) { for (uint32_t i = 1; i <= MAX_SWITCHES; i++) { SendKey(KEY_SWITCH, i, CLEAR_RETAIN); // Clear MQTT retain in broker } } Settings.flag.mqtt_switch_retain = XdrvMailbox.payload; // CMND_SWITCHRETAIN } ResponseCmndStateText(Settings.flag.mqtt_switch_retain); // CMND_SWITCHRETAIN } void CmndPowerRetain(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { if (!XdrvMailbox.payload) { char stemp1[TOPSZ]; char scommand[CMDSZ]; for (uint32_t i = 1; i <= TasmotaGlobal.devices_present; i++) { // Clear MQTT retain in broker GetTopic_P(stemp1, STAT, TasmotaGlobal.mqtt_topic, GetPowerDevice(scommand, i, sizeof(scommand), Settings.flag.device_index_enable)); // SetOption26 - Switch between POWER or POWER1 ResponseClear(); MqttPublish(stemp1, Settings.flag.mqtt_power_retain); // CMND_POWERRETAIN } } Settings.flag.mqtt_power_retain = XdrvMailbox.payload; // CMND_POWERRETAIN if (Settings.flag.mqtt_power_retain) { Settings.flag4.only_json_message = 0; // SetOption90 - Disable non-json MQTT response } } ResponseCmndStateText(Settings.flag.mqtt_power_retain); // CMND_POWERRETAIN } void CmndSensorRetain(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { if (!XdrvMailbox.payload) { ResponseClear(); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_SENSOR), Settings.flag.mqtt_sensor_retain); // CMND_SENSORRETAIN MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_ENERGY), Settings.flag.mqtt_sensor_retain); // CMND_SENSORRETAIN } Settings.flag.mqtt_sensor_retain = XdrvMailbox.payload; // CMND_SENSORRETAIN } ResponseCmndStateText(Settings.flag.mqtt_sensor_retain); // CMND_SENSORRETAIN } void CmndInfoRetain(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { if (!XdrvMailbox.payload) { ResponseClear(); MqttPublishPrefixTopic_P(TELE, PSTR(D_RSLT_INFO), Settings.flag5.mqtt_info_retain); // CMND_INFORETAIN } Settings.flag5.mqtt_info_retain = XdrvMailbox.payload; // CMND_INFORETAIN } ResponseCmndStateText(Settings.flag5.mqtt_info_retain); // CMND_INFORETAIN } void CmndStateRetain(void) { if ((XdrvMailbox.payload >= 0) && (XdrvMailbox.payload <= 1)) { if (!XdrvMailbox.payload) { ResponseClear(); MqttPublishPrefixTopic_P(STAT, PSTR(D_RSLT_STATE), Settings.flag5.mqtt_state_retain); // CMND_STATERETAIN } Settings.flag5.mqtt_state_retain = XdrvMailbox.payload; // CMND_STATERETAIN } ResponseCmndStateText(Settings.flag5.mqtt_state_retain); // CMND_STATERETAIN } /* The download chunk size is the data size before it is encoded to base64. It is smaller than the upload chunksize as it is bound by MESSZ The download buffer with length MESSZ (1042) contains - Payload ({"Id":117,"Data":""}) */ const uint32_t FileTransferHeaderSize = 21; // {"Id":116,"Data":""} const uint32_t mqtt_file_chuck_size = (((MESSZ - FileTransferHeaderSize) / 4) * 3) -2; uint32_t FileUploadChunckSize(void) { /* The upload chunk size is the data size before it is encoded to base64. It can be larger than the download chunksize which is bound by MESSZ The PubSubClient upload buffer with length MQTT_MAX_PACKET_SIZE (1200) contains - Header of 5 bytes (MQTT_MAX_HEADER_SIZE) - Topic string terminated with a zero (stat/demo/FILEUPLOAD) - Payload ({"Id":116,"Data":""}) */ const uint32_t PubSubClientHeaderSize = 5; // MQTT_MAX_HEADER_SIZE return (((MQTT_MAX_PACKET_SIZE - PubSubClientHeaderSize - Mqtt.topic_size - FileTransferHeaderSize) / 4) * 3) -2; } void CmndFileUpload(void) { /* Upload (binary) max 700 bytes chunks of data base64 encoded with MD5 hash over base64 decoded data FileUpload 0 - Abort current upload FileUpload {"File":"Config_wemos10_9.4.0.3.dmp","Id":116,"Type":2,"Size":4096} FileUpload {"Id":116,"Data":"CRJcTQ9fYGF ... OT1BRUlNUVVZXWFk="} FileUpload {"Id":116,"Data":" ... "} FileUpload {"Id":116,"Md5":"496fcbb433bbca89833063174d2c5747"} */ const char* base64_data = nullptr; uint32_t rcv_id = 0; char* dataBuf = (char*)XdrvMailbox.data; if (strlen(dataBuf) > 8) { // Workaround exception if empty JSON like {} - Needs checks JsonParser parser((char*) dataBuf); JsonParserObject root = parser.getRootObject(); if (root) { JsonParserToken val = root[PSTR("ID")]; if (val) { rcv_id = val.getUInt(); } val = root[PSTR("TYPE")]; if (val) { Mqtt.file_type = val.getUInt(); } val = root[PSTR("SIZE")]; if (val) { Mqtt.file_size = val.getUInt(); } val = root[PSTR("MD5")]; if (val) { Mqtt.file_md5 = val.getStr(); } val = root[PSTR("DATA")]; if (val) { base64_data = val.getStr(); } } } if ((0 == Mqtt.file_id) && (rcv_id > 0) && (Mqtt.file_size > 0) && (Mqtt.file_type > 0)) { // Init upload buffer Mqtt.file_buffer = nullptr; if (UPL_SETTINGS == Mqtt.file_type) { if (SettingsConfigBackup()) { Mqtt.file_buffer = settings_buffer; } } if (!Mqtt.file_buffer) { ResponseCmndChar(PSTR(D_JSON_INVALID_FILE_TYPE)); } else { Mqtt.file_id = rcv_id; Mqtt.file_pos = 0; Mqtt.md5 = MD5Builder(); Mqtt.md5.begin(); // TasmotaGlobal.masterlog_level = LOG_LEVEL_DEBUG_MORE; // Hide upload data logging } } else if ((Mqtt.file_id > 0) && (Mqtt.file_id != rcv_id)) { // Error receiving data if (UPL_SETTINGS == Mqtt.file_type) { SettingsBufferFree(); } Mqtt.file_buffer = nullptr; ResponseCmndChar(PSTR(D_JSON_ABORTED)); } if (Mqtt.file_buffer) { if ((Mqtt.file_pos < Mqtt.file_size) && base64_data) { // Save upload into buffer - Handle possible buffer overflows uint32_t rcvd_bytes = decode_base64_length((unsigned char*)base64_data); unsigned char decode_output[rcvd_bytes]; decode_base64((unsigned char*)base64_data, (unsigned char*)decode_output); uint32_t bytes_left = Mqtt.file_size - Mqtt.file_pos; uint32_t read_bytes = (bytes_left < rcvd_bytes) ? bytes_left : rcvd_bytes; uint8_t* buffer = Mqtt.file_buffer + Mqtt.file_pos; memcpy(buffer, decode_output, read_bytes); Mqtt.md5.add(buffer, read_bytes); Mqtt.file_pos += read_bytes; } if ((Mqtt.file_pos < Mqtt.file_size) || (Mqtt.file_md5.length() != 32)) { // {"Id":116,"MaxSize":"765"} Response_P(PSTR("{\"Id\":%d,\"MaxSize\":%d}"), Mqtt.file_id, FileUploadChunckSize()); } else { Mqtt.md5.calculate(); if (strcasecmp(Mqtt.file_md5.c_str(), Mqtt.md5.toString().c_str())) { ResponseCmndChar(PSTR(D_JSON_MD5_MISMATCH)); } else { // Process upload data en free buffer ResponseCmndDone(); if (UPL_SETTINGS == Mqtt.file_type) { if (!SettingsConfigRestore()) { ResponseCmndFailed(); } else { TasmotaGlobal.restart_flag = 2; // Always restart to re-enable disabled features during update } } } Mqtt.file_buffer = nullptr; } } if (!Mqtt.file_buffer) { // TasmotaGlobal.masterlog_level = LOG_LEVEL_NONE; // Enable logging Mqtt.file_id = 0; Mqtt.file_size = 0; Mqtt.file_type = 0; Mqtt.file_md5 = (const char*) nullptr; // Force deallocation of the String internal memory } MqttPublishPrefixTopic_P(STAT, XdrvMailbox.command); // Enforce stat/wemos10/FILEUPLOAD ResponseClear(); } void CmndFileDownload(void) { /* Download (binary) max 700 bytes chunks of data base64 encoded with MD5 hash over base64 decoded data Currently supports Settings (file type 2) Filedownload 0 - Abort current download FileDownload 2 - Start download of settings file FileDownload - Continue downloading data until reception of MD5 hash */ if (Mqtt.file_id && Mqtt.file_buffer) { bool finished = false; if (0 == XdrvMailbox.payload) { // Abort file download ResponseCmndChar(PSTR(D_JSON_ABORTED)); finished = true; } else if (Mqtt.file_pos < Mqtt.file_size) { uint32_t bytes_left = Mqtt.file_size - Mqtt.file_pos; uint32_t write_bytes = (bytes_left < mqtt_file_chuck_size) ? bytes_left : mqtt_file_chuck_size; uint8_t* buffer = Mqtt.file_buffer + Mqtt.file_pos; Mqtt.md5.add(buffer, write_bytes); // {"Id":117,"Data":"CRJcTQ9fYGF ... OT1BRUlNUVVZXWFk="} Response_P(PSTR("{\"Id\":%d,\"Data\":\""), Mqtt.file_id); // FileTransferHeaderSize char base64_data[encode_base64_length(write_bytes)]; encode_base64((unsigned char*)buffer, write_bytes, (unsigned char*)base64_data); ResponseAppend_P(base64_data); ResponseAppend_P("\"}"); Mqtt.file_pos += write_bytes; } else { Mqtt.md5.calculate(); // {"Id":117,"Md5":"496fcbb433bbca89833063174d2c5747"} Response_P(PSTR("{\"Id\":%d,\"Md5\":\"%s\"}"), Mqtt.file_id, Mqtt.md5.toString().c_str()); finished = true; } if (finished) { if (UPL_SETTINGS == Mqtt.file_type) { SettingsBufferFree(); } Mqtt.file_id = 0; } } else if (XdrvMailbox.data_len) { Mqtt.file_buffer = nullptr; Mqtt.file_id = (UtcTime() & 0xFE) +1; // Odd id between 1 and 255 if (UPL_SETTINGS == XdrvMailbox.payload) { uint32_t len = SettingsConfigBackup(); if (len) { Mqtt.file_type = UPL_SETTINGS; Mqtt.file_buffer = settings_buffer; Mqtt.file_size = len; // {"File":"Config_wemos10_9.4.0.3.dmp","Id":117,"Type":2,"Size":4096} Response_P(PSTR("{\"File\":\"%s\",\"Id\":%d,\"Type\":%d,\"Size\":%d}"), SettingsConfigFilename().c_str(), Mqtt.file_id, Mqtt.file_type, len); } } if (Mqtt.file_buffer) { Mqtt.file_pos = 0; Mqtt.md5 = MD5Builder(); Mqtt.md5.begin(); } else { Mqtt.file_id = 0; ResponseCmndFailed(); } } MqttPublishPrefixTopic_P(STAT, XdrvMailbox.command); ResponseClear(); } /*********************************************************************************************\ * TLS private key and certificate - store into Flash \*********************************************************************************************/ #if defined(USE_MQTT_TLS) && defined(USE_MQTT_AWS_IOT) #ifdef ESP32 static uint8_t * tls_spi_start = nullptr; const static size_t tls_spi_len = 0x0400; // 1kb blocs const static size_t tls_block_offset = 0x0000; // don't need offset in FS #else // const static uint16_t tls_spi_start_sector = EEPROM_LOCATION + 4; // 0xXXFF // const static uint8_t* tls_spi_start = (uint8_t*) ((tls_spi_start_sector * SPI_FLASH_SEC_SIZE) + 0x40200000); // 0x40XFF000 const static uint16_t tls_spi_start_sector = 0xFF; // Force last bank of first MB const static uint8_t* tls_spi_start = (uint8_t*) 0x402FF000; // 0x402FF000 const static size_t tls_spi_len = 0x1000; // 4kb blocs const static size_t tls_block_offset = 0x0400; #endif const static size_t tls_block_len = 0x0400; // 1kb const static size_t tls_obj_store_offset = tls_block_offset + sizeof(tls_dir_t); inline void TlsEraseBuffer(uint8_t *buffer) { memset(buffer + tls_block_offset, 0xFF, tls_block_len); } // static data structures for Private Key and Certificate, only the pointer // to binary data will change to a region in SPI Flash static br_ec_private_key EC = { 23, nullptr, 0 }; static br_x509_certificate CHAIN[] = { { nullptr, 0 } }; // load a copy of the tls_dir from flash into ram // and calculate the appropriate data structures for AWS_IoT_Private_Key and AWS_IoT_Client_Certificate void loadTlsDir(void) { #ifdef ESP32 // We load the file in RAM and use it as if it was in Flash. The buffer is never deallocated once we loaded TLS keys AWS_IoT_Private_Key = nullptr; AWS_IoT_Client_Certificate = nullptr; if (TfsFileExists(TASM_FILE_TLSKEY)) { if (tls_spi_start == nullptr){ tls_spi_start = (uint8_t*) malloc(tls_block_len); if (tls_spi_start == nullptr) { return; } } TfsLoadFile(TASM_FILE_TLSKEY, tls_spi_start, tls_block_len); } else { return; // file does not exist, do nothing } #endif memcpy_P(&tls_dir, tls_spi_start + tls_block_offset, sizeof(tls_dir)); // calculate the addresses for Key and Cert in Flash if ((TLS_NAME_SKEY == tls_dir.entry[0].name) && (tls_dir.entry[0].len > 0)) { EC.x = (unsigned char *)(tls_spi_start + tls_obj_store_offset + tls_dir.entry[0].start); EC.xlen = tls_dir.entry[0].len; AWS_IoT_Private_Key = &EC; } else { AWS_IoT_Private_Key = nullptr; } if ((TLS_NAME_CRT == tls_dir.entry[1].name) && (tls_dir.entry[1].len > 0)) { CHAIN[0].data = (unsigned char *) (tls_spi_start + tls_obj_store_offset + tls_dir.entry[1].start); CHAIN[0].data_len = tls_dir.entry[1].len; AWS_IoT_Client_Certificate = CHAIN; } else { AWS_IoT_Client_Certificate = nullptr; } //Serial.printf("AWS_IoT_Private_Key = %x, AWS_IoT_Client_Certificate = %x\n", AWS_IoT_Private_Key, AWS_IoT_Client_Certificate); } const char ALLOCATE_ERROR[] PROGMEM = "TLSKey " D_JSON_ERROR ": cannot allocate buffer."; void CmndTlsKey(void) { #ifdef DEBUG_DUMP_TLS if (0 == XdrvMailbox.index){ CmndTlsDump(); } #endif // DEBUG_DUMP_TLS if ((XdrvMailbox.index >= 1) && (XdrvMailbox.index <= 2)) { tls_dir_t *tls_dir_write; if (XdrvMailbox.data_len > 0) { // write new value // first copy SPI buffer into ram uint8_t *spi_buffer = (uint8_t*) malloc(tls_spi_len); if (!spi_buffer) { AddLog(LOG_LEVEL_ERROR, ALLOCATE_ERROR); return; } if (tls_spi_start != nullptr) { // safeguard for ESP32 memcpy_P(spi_buffer, tls_spi_start, tls_spi_len); } else { memset(spi_buffer, 0, tls_spi_len); // safeguard for ESP32, removed by compiler for ESP8266 } // remove any white space from the base64 RemoveSpace(XdrvMailbox.data); // allocate buffer for decoded base64 uint32_t bin_len = decode_base64_length((unsigned char*)XdrvMailbox.data); uint8_t *bin_buf = nullptr; if (bin_len > 0) { bin_buf = (uint8_t*) malloc(bin_len + 4); if (!bin_buf) { AddLog(LOG_LEVEL_ERROR, ALLOCATE_ERROR); free(spi_buffer); return; } } // decode base64 if (bin_len > 0) { decode_base64((unsigned char*)XdrvMailbox.data, bin_buf); } // address of writable tls_dir in buffer tls_dir_write = (tls_dir_t*) (spi_buffer + tls_block_offset); bool save_file = false; // for ESP32, do we need to write file if (1 == XdrvMailbox.index) { // Try to write Private key // Start by erasing all #ifdef ESP32 if (TfsFileExists(TASM_FILE_TLSKEY)) { TfsDeleteFile(TASM_FILE_TLSKEY); // delete file } #else TlsEraseBuffer(spi_buffer); // Erase any previously stored data #endif if (bin_len > 0) { if (bin_len != 32) { // no private key was previously stored, abort AddLog(LOG_LEVEL_INFO, PSTR("TLSKey: Certificate must be 32 bytes: %d."), bin_len); free(spi_buffer); free(bin_buf); return; } tls_entry_t *entry = &tls_dir_write->entry[0]; entry->name = TLS_NAME_SKEY; entry->start = 0; entry->len = bin_len; memcpy(spi_buffer + tls_obj_store_offset + entry->start, bin_buf, entry->len); save_file = true; } else { // if lenght is zero, simply erase this SPI flash area } } else if (2 == XdrvMailbox.index) { // Try to write Certificate if (TLS_NAME_SKEY != tls_dir.entry[0].name) { // no private key was previously stored, abort AddLog(LOG_LEVEL_INFO, PSTR("TLSKey: cannot store Cert if no Key previously stored.")); free(spi_buffer); free(bin_buf); return; } if (bin_len <= 256) { // Certificate lenght too short AddLog(LOG_LEVEL_INFO, PSTR("TLSKey: Certificate length too short: %d."), bin_len); free(spi_buffer); free(bin_buf); return; } tls_entry_t *entry = &tls_dir_write->entry[1]; entry->name = TLS_NAME_CRT; entry->start = (tls_dir_write->entry[0].start + tls_dir_write->entry[0].len + 3) & ~0x03; // align to 4 bytes boundary entry->len = bin_len; memcpy(spi_buffer + tls_obj_store_offset + entry->start, bin_buf, entry->len); save_file = true; } #ifdef ESP32 if (save_file) { TfsSaveFile(TASM_FILE_TLSKEY, spi_buffer, tls_spi_len); } #else if (ESP.flashEraseSector(tls_spi_start_sector)) { ESP.flashWrite(tls_spi_start_sector * SPI_FLASH_SEC_SIZE, (uint32_t*) spi_buffer, SPI_FLASH_SEC_SIZE); } #endif free(spi_buffer); free(bin_buf); } loadTlsDir(); // reload into memory any potential change Response_P(PSTR("{\"%s1\":%d,\"%s2\":%d}"), XdrvMailbox.command, AWS_IoT_Private_Key ? tls_dir.entry[0].len : -1, XdrvMailbox.command, AWS_IoT_Client_Certificate ? tls_dir.entry[1].len : -1); } } #ifdef DEBUG_DUMP_TLS // Dump TLS Flash data - don't activate in production to protect your private keys uint32_t bswap32(uint32_t x) { return ((x << 24) & 0xff000000 ) | ((x << 8) & 0x00ff0000 ) | ((x >> 8) & 0x0000ff00 ) | ((x >> 24) & 0x000000ff ); } void CmndTlsDump(void) { if (tls_spi_start == nullptr) { return; } // safeguard for ESP32, removed by compiler for ESP8266 uint32_t start = (uint32_t)tls_spi_start + tls_block_offset; uint32_t end = start + tls_block_len -1; for (uint32_t pos = start; pos < end; pos += 0x10) { uint32_t* values = (uint32_t*)(pos); Serial.printf_P(PSTR("%08x: %08x %08x %08x %08x\n"), pos, bswap32(values[0]), bswap32(values[1]), bswap32(values[2]), bswap32(values[3])); } } #endif // DEBUG_DUMP_TLS #endif /*********************************************************************************************\ * Presentation \*********************************************************************************************/ #ifdef USE_WEBSERVER #define WEB_HANDLE_MQTT "mq" const char S_CONFIGURE_MQTT[] PROGMEM = D_CONFIGURE_MQTT; const char HTTP_BTN_MENU_MQTT[] PROGMEM = "

"; const char HTTP_FORM_MQTT1[] PROGMEM = "
 " D_MQTT_PARAMETERS " " "
" "

" D_HOST " (" MQTT_HOST ")

" "

" D_PORT " (" STR(MQTT_PORT) ")

" #ifdef USE_MQTT_TLS "


" #endif // USE_MQTT_TLS "

" D_CLIENT " (%s)

"; const char HTTP_FORM_MQTT2[] PROGMEM = "

" D_USER " (" MQTT_USER ")

" "


" "

" D_TOPIC " = %%topic%% (%s)

" "

" D_FULL_TOPIC " (%s)

"; void HandleMqttConfiguration(void) { if (!HttpCheckPriviledgedAccess()) { return; } AddLog(LOG_LEVEL_DEBUG, PSTR(D_LOG_HTTP D_CONFIGURE_MQTT)); if (Webserver->hasArg(F("save"))) { MqttSaveSettings(); WebRestart(1); return; } char str[TOPSZ]; WSContentStart_P(PSTR(D_CONFIGURE_MQTT)); WSContentSendStyle(); WSContentSend_P(HTTP_FORM_MQTT1, SettingsText(SET_MQTT_HOST), Settings.mqtt_port, #ifdef USE_MQTT_TLS Mqtt.mqtt_tls ? PSTR(" checked") : "", // SetOption102 - Enable MQTT TLS #endif // USE_MQTT_TLS Format(str, PSTR(MQTT_CLIENT_ID), sizeof(str)), PSTR(MQTT_CLIENT_ID), SettingsText(SET_MQTT_CLIENT)); WSContentSend_P(HTTP_FORM_MQTT2, (!strlen(SettingsText(SET_MQTT_USER))) ? "0" : SettingsText(SET_MQTT_USER), Format(str, PSTR(MQTT_TOPIC), sizeof(str)), PSTR(MQTT_TOPIC), SettingsText(SET_MQTT_TOPIC), PSTR(MQTT_FULLTOPIC), PSTR(MQTT_FULLTOPIC), SettingsText(SET_MQTT_FULLTOPIC)); WSContentSend_P(HTTP_FORM_END); WSContentSpaceButton(BUTTON_CONFIGURATION); WSContentStop(); } void MqttSaveSettings(void) { String cmnd = F(D_CMND_BACKLOG "0 "); cmnd += AddWebCommand(PSTR(D_CMND_MQTTHOST), PSTR("mh"), PSTR("1")); cmnd += AddWebCommand(PSTR(D_CMND_MQTTPORT), PSTR("ml"), PSTR("1")); cmnd += AddWebCommand(PSTR(D_CMND_MQTTCLIENT), PSTR("mc"), PSTR("1")); cmnd += AddWebCommand(PSTR(D_CMND_MQTTUSER), PSTR("mu"), PSTR("1")); cmnd += AddWebCommand(PSTR(D_CMND_MQTTPASSWORD "2"), PSTR("mp"), PSTR("\"")); cmnd += AddWebCommand(PSTR(D_CMND_TOPIC), PSTR("mt"), PSTR("1")); cmnd += AddWebCommand(PSTR(D_CMND_FULLTOPIC), PSTR("mf"), PSTR("1")); #ifdef USE_MQTT_TLS cmnd += F(";" D_CMND_SO "102 "); cmnd += Webserver->hasArg(F("b3")); // SetOption102 - Enable MQTT TLS #endif ExecuteWebCommand((char*)cmnd.c_str()); } #endif // USE_WEBSERVER /*********************************************************************************************\ * Interface \*********************************************************************************************/ bool Xdrv02(uint8_t function) { bool result = false; if (Settings.flag.mqtt_enabled) { // SetOption3 - Enable MQTT switch (function) { case FUNC_PRE_INIT: MqttInit(); break; case FUNC_EVERY_50_MSECOND: // https://github.com/knolleary/pubsubclient/issues/556 MqttClient.loop(); break; #ifdef USE_WEBSERVER case FUNC_WEB_ADD_BUTTON: WSContentSend_P(HTTP_BTN_MENU_MQTT); break; case FUNC_WEB_ADD_HANDLER: WebServer_on(PSTR("/" WEB_HANDLE_MQTT), HandleMqttConfiguration); break; #endif // USE_WEBSERVER case FUNC_COMMAND: result = DecodeCommand(kMqttCommands, MqttCommand, kMqttSynonyms); break; } } return result; }