diff --git a/README.md b/README.md index d309bb4..6c3ed82 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ Basic commands (enough to get it working in nearly all environments): - set [ap_ssid|ap_password] _value_: changes the settings for the soft-AP of the ESP (for your stations) - show [config|stats|script|mqtt]: prints the current config or some status information and statistics - save: saves the current config parameters to flash +- set broker_user _unsername_: sets the username for authentication of MQTT clients ("none" if no auth, default) +- set broker_password _password_: sets the password for authentication of MQTT clients ("none" if empty, default) - lock [_password_]: saves and locks the current config, changes are not allowed. Password can be left open if already set before - unlock _password_: unlocks the config, requires password from the lock command - reset [factory]: resets the esp, 'factory' optionally resets WiFi params to default values (works on a locked device only from serial console) @@ -70,7 +72,7 @@ By default the "remote" MQTT client is disabled. It can be enabled by setting th # Scripting The esp_uMQTT_broker comes with a build-in scripting engine. A script enables the ESP not just to act as a passive broker but to react on events (publications and timing events) and to send out its own items. -Here is a demo of a script to give you an idea of the power of the scripting feature. This script controls a Sonoff switch module. It connects to a remote MQTT broker and in parallel offers locally its own. On both brokers it subscribes to a topic named '/martinshome/switch/1/command', where it receives commands, and it publishes the topic '/martinshome/switch/1/status' with the current state of the switch relay. It understands the commands 'on','off', 'toggle', and 'blink'. Blinking is realized via a timer event. Local status is stored in the two variables $1 (switch state) and $2 (blinking on/off). The 'on gpio_interrupt' clause reacts on pressing the pushbutton of the Sonnoff and simply toggle the switch (and stops blinking). The last two 'on clock' clauses implement a daily on and off period: +Here is a demo of a script to give you an idea of the power of the scripting feature. This script controls a Sonoff switch module. It connects to a remote MQTT broker and in parallel offers locally its own. On both brokers it subscribes to a topic named '/martinshome/switch/1/command', where it receives commands, and it publishes the topic '/martinshome/switch/1/status' with the current state of the switch relay. It understands the commands 'on','off', 'toggle', and 'blink'. Blinking is realized via a timer event. Local status is stored in the two variables $1 (switch state) and $2 (blinking on/off). The 'on gpio_interrupt' clause reacts on pressing the pushbutton of the Sonnoff and simply toggles the switch (and stops blinking). The last two 'on clock' clauses implement a daily on and off period: ``` % Config params, overwrite any previous settings from the commandline @@ -78,6 +80,8 @@ config ap_ssid MyAP config ap_password stupidPassword config ntp_server 1.pool.ntp.org config mqtt_host 192.168.1.20 +config broker_user Martin +config broker_password secret % Now the initialization, this is done once after booting on init @@ -143,7 +147,7 @@ do publish local /martinshome/switch/1/status $1 retained publish remote /martinshome/switch/1/status $1 retained -% The local pushbotton +% The local pushbutton on gpio_interrupt 0 pullup do println "New state GPIO 0: " | $this_gpio @@ -263,10 +267,9 @@ The broker does support: - retained messages - LWT - QoS level 0 -- a subset of MQTT (CONNECT, DISCONNECT, SUBSCRIBE, UNSUBSCRIBE, PUBLISH, PING) +- username/password authentication The broker does not yet support: -- username, password authentication - QoS levels other than 0 - many TCP(=MQTT) clients - non-clear sessions @@ -294,3 +297,12 @@ void MQTT_local_onData(MqttDataCallback dataCb); With these functions you can publish and subscribe topics as a local client like you would with a remote MQTT broker. +Username/password authentication is provided with the following interface: + +```c +typedef bool (*MqttAuthCallback)(const char* username, const char *password); + +void MQTT_server_onAuth(MqttAuthCallback authCb); +``` + +If an *MqttAuthCallback* function is provided, it is called on each connect request. Based on username and password the function has to return *true* for authenticated or *false* for rejected. No provided username/password are empty strings. If no *MqttAuthCallback* function is set, each request will be admitted. diff --git a/firmware/0x00000.bin b/firmware/0x00000.bin index c72a42e..9d99408 100644 Binary files a/firmware/0x00000.bin and b/firmware/0x00000.bin differ diff --git a/firmware/0x10000.bin b/firmware/0x10000.bin index 508a3bb..1c0cbab 100644 Binary files a/firmware/0x10000.bin and b/firmware/0x10000.bin differ diff --git a/mqtt/include/mqtt_server.h b/mqtt/include/mqtt_server.h index ed94fce..145202a 100644 --- a/mqtt/include/mqtt_server.h +++ b/mqtt/include/mqtt_server.h @@ -14,6 +14,8 @@ #define LOCAL_MQTT_CLIENT ((void*)-1) +typedef bool (*MqttAuthCallback)(const char* username, const char *password); + typedef struct _MQTT_ClientCon { struct espconn *pCon; // uint8_t security; @@ -38,6 +40,7 @@ typedef struct _MQTT_ClientCon { extern MQTT_ClientCon *clientcon_list; bool MQTT_server_start(uint16_t portno, uint16_t max_subscriptions, uint16_t max_retained_topics); +void MQTT_server_onAuth(MqttAuthCallback authCb); bool MQTT_local_publish(uint8_t* topic, uint8_t* data, uint16_t data_length, uint8_t qos, uint8_t retain); bool MQTT_local_subscribe(uint8_t* topic, uint8_t qos); diff --git a/mqtt/mqtt_server.c b/mqtt/mqtt_server.c index 474c851..05db432 100644 --- a/mqtt/mqtt_server.c +++ b/mqtt/mqtt_server.c @@ -36,6 +36,7 @@ LOCAL uint8_t zero_len_id[2] = { 0, 0 }; MQTT_ClientCon *clientcon_list; LOCAL MqttDataCallback local_data_cb = NULL; +LOCAL MqttAuthCallback local_auth_cb = NULL; //#define MQTT_INFO os_printf #define MQTT_WARNING os_printf @@ -335,22 +336,18 @@ static void ICACHE_FLASH_ATTR MQTT_ClientCon_recv_cb(void *arg, char *pdata, uns } } + uint16_t msg_used_len = var_header_len; + MQTT_INFO("MQTT: Connect flags %x\r\n", variable_header->flags); clientcon->connect_info.clean_session = (variable_header->flags & MQTT_CONNECT_FLAG_CLEAN_SESSION) != 0; - if ((variable_header->flags & MQTT_CONNECT_FLAG_USERNAME) != 0 || - (variable_header->flags & MQTT_CONNECT_FLAG_PASSWORD) != 0) { - MQTT_WARNING("MQTT: Connect option currently not supported\r\n"); - msg_conn_ret = CONNECTION_REFUSE_NOT_AUTHORIZED; - clientcon->connState = TCP_DISCONNECTING; - break; - } + clientcon->connect_info.keepalive = (variable_header->keepaliveMsb << 8) + variable_header->keepaliveLsb; espconn_regist_time(clientcon->pCon, 2 * clientcon->connect_info.keepalive, 1); MQTT_INFO("MQTT: Keepalive %d\r\n", clientcon->connect_info.keepalive); // Get the client id - uint16_t id_len = clientcon->mqtt_state.message_length - (2 + var_header_len); - const char *client_id = mqtt_get_str(&clientcon->mqtt_state.in_buffer[2 + var_header_len], &id_len); + uint16_t id_len = clientcon->mqtt_state.message_length - (2 + msg_used_len); + const char *client_id = mqtt_get_str(&clientcon->mqtt_state.in_buffer[2 + msg_used_len], &id_len); if (client_id == NULL || id_len > 80) { MQTT_WARNING("MQTT: Client Id invalid\r\n"); msg_conn_ret = CONNECTION_REFUSE_ID_REJECTED; @@ -384,6 +381,7 @@ static void ICACHE_FLASH_ATTR MQTT_ClientCon_recv_cb(void *arg, char *pdata, uns break; } } + msg_used_len += 2 + id_len; // Get the LWT clientcon->connect_info.will_retain = (variable_header->flags & MQTT_CONNECT_FLAG_WILL_RETAIN) != 0; @@ -396,9 +394,9 @@ static void ICACHE_FLASH_ATTR MQTT_ClientCon_recv_cb(void *arg, char *pdata, uns return; } } else { - uint16_t lw_topic_len = clientcon->mqtt_state.message_length - (4 + var_header_len + id_len); + uint16_t lw_topic_len = clientcon->mqtt_state.message_length - (2 + msg_used_len); const char *lw_topic = - mqtt_get_str(&clientcon->mqtt_state.in_buffer[4 + var_header_len + id_len], &lw_topic_len); + mqtt_get_str(&clientcon->mqtt_state.in_buffer[2 + msg_used_len], &lw_topic_len); if (lw_topic == NULL) { MQTT_WARNING("MQTT: Last Will topic invalid\r\n"); @@ -422,11 +420,12 @@ static void ICACHE_FLASH_ATTR MQTT_ClientCon_recv_cb(void *arg, char *pdata, uns clientcon->connState = TCP_DISCONNECTING; break; } + msg_used_len += 2 + lw_topic_len; uint16_t lw_data_len = - clientcon->mqtt_state.message_length - (6 + var_header_len + id_len + lw_topic_len); + clientcon->mqtt_state.message_length - (2 + msg_used_len); const char *lw_data = - mqtt_get_str(&clientcon->mqtt_state.in_buffer[6 + var_header_len + id_len + lw_topic_len], + mqtt_get_str(&clientcon->mqtt_state.in_buffer[2 + msg_used_len], &lw_data_len); if (lw_data == NULL) { @@ -446,6 +445,71 @@ static void ICACHE_FLASH_ATTR MQTT_ClientCon_recv_cb(void *arg, char *pdata, uns clientcon->connState = TCP_DISCONNECTING; break; } + msg_used_len += 2 + lw_data_len; + } + + // Get the username + if ((variable_header->flags & MQTT_CONNECT_FLAG_USERNAME) != 0) { + uint16_t username_len = clientcon->mqtt_state.message_length - (2 + msg_used_len); + const char *username = + mqtt_get_str(&clientcon->mqtt_state.in_buffer[2 + msg_used_len], &username_len); + + if (username == NULL) { + MQTT_WARNING("MQTT: Username invalid\r\n"); + MQTT_ServerDisconnect(clientcon); + return; + } + + clientcon->connect_info.username = (char *)os_zalloc(username_len+1); + if (clientcon->connect_info.username != NULL) { + os_memcpy(clientcon->connect_info.username, username, username_len); + clientcon->connect_info.username[username_len] = '\0'; + MQTT_INFO("MQTT: Username %s\r\n", clientcon->connect_info.username); + } else { + MQTT_ERROR("MQTT: Out of mem\r\n"); + msg_conn_ret = CONNECTION_REFUSE_SERVER_UNAVAILABLE; + clientcon->connState = TCP_DISCONNECTING; + break; + } + msg_used_len += 2 + username_len; + } + + // Get the password + if ((variable_header->flags & MQTT_CONNECT_FLAG_PASSWORD) != 0) { + + if ((variable_header->flags & MQTT_CONNECT_FLAG_USERNAME) == 0) { + MQTT_WARNING("MQTT: Password without username\r\n"); + MQTT_ServerDisconnect(clientcon); + return; + } + + uint16_t password_len = clientcon->mqtt_state.message_length - (2 + msg_used_len); + const char *password = + mqtt_get_str(&clientcon->mqtt_state.in_buffer[2 + msg_used_len], &password_len); + + if (password != NULL) + clientcon->connect_info.password = (char *)os_zalloc(password_len+1); + if (clientcon->connect_info.password != NULL) { + os_memcpy(clientcon->connect_info.password, password, password_len); + clientcon->connect_info.password[password_len] = '\0'; + MQTT_INFO("MQTT: Password %s\r\n", clientcon->connect_info.password); + } else { + MQTT_ERROR("MQTT: Out of mem\r\n"); + msg_conn_ret = CONNECTION_REFUSE_SERVER_UNAVAILABLE; + clientcon->connState = TCP_DISCONNECTING; + break; + } + msg_used_len += 2 + password_len; + } + + // Check Auth + if ((local_auth_cb != NULL) && + local_auth_cb(clientcon->connect_info.username==NULL?"":clientcon->connect_info.username, + clientcon->connect_info.password==NULL?"":clientcon->connect_info.password) == false) { + MQTT_WARNING("MQTT: Authorization failed\r\n"); + msg_conn_ret = CONNECTION_REFUSE_NOT_AUTHORIZED; + clientcon->connState = TCP_DISCONNECTING; + break; } msg_conn_ret = CONNECTION_ACCEPTED; @@ -812,3 +876,7 @@ bool ICACHE_FLASH_ATTR MQTT_local_unsubscribe(uint8_t * topic) { void ICACHE_FLASH_ATTR MQTT_local_onData(MqttDataCallback dataCb) { local_data_cb = dataCb; } + +void ICACHE_FLASH_ATTR MQTT_server_onAuth(MqttAuthCallback authCb) { + local_auth_cb = authCb; +} diff --git a/user/config_flash.c b/user/config_flash.c index 4179d15..cebdecf 100644 --- a/user/config_flash.c +++ b/user/config_flash.c @@ -34,6 +34,9 @@ void config_load_default(sysconfig_p config) { config->clock_speed = 80; config->config_port = CONSOLE_SERVER_PORT; + os_sprintf(config->mqtt_broker_user, "%s", "none"); + config->mqtt_broker_password[0] = 0; + #ifdef MQTT_CLIENT os_sprintf(config->mqtt_host, "%s", "none"); config->mqtt_port = 1883; diff --git a/user/config_flash.h b/user/config_flash.h index 6e405ec..30a2dc0 100644 --- a/user/config_flash.h +++ b/user/config_flash.h @@ -13,7 +13,7 @@ #define FLASH_BLOCK_NO 0xc -#define MAGIC_NUMBER 0x015005fc +#define MAGIC_NUMBER 0x015005fd typedef struct { @@ -46,6 +46,9 @@ typedef struct uint16_t clock_speed; // Freq of the CPU uint16_t config_port; // Port on which the concole listenes (0 if no access) + uint8_t mqtt_broker_user[32]; // Username for client login, "none" if empty + uint8_t mqtt_broker_password[32]; // Password for client login + #ifdef MQTT_CLIENT uint8_t mqtt_host[32]; // IP or hostname of the MQTT broker, "none" if empty uint16_t mqtt_port; // Port of the MQTT broker diff --git a/user/script.sonoff b/user/script.sonoff index 37bedfe..456094b 100644 --- a/user/script.sonoff +++ b/user/script.sonoff @@ -2,6 +2,8 @@ config ap_ssid DerKluge config ap_password Bonn2016 config ntp_server 1.de.pool.ntp.org +config broker_user Martin +config broker_password secret config mqtt_host martinshome.fritz.box config speed 160 @@ -70,7 +72,7 @@ do publish remote /martinshome/switch/1/status $1 retained -% The local pushbotton +% The local pushbutton on gpio_interrupt 0 pullup do println "New state GPIO 0: " | $this_gpio diff --git a/user/user_main.c b/user/user_main.c index 44f1ad2..58931c7 100644 --- a/user/user_main.c +++ b/user/user_main.c @@ -348,7 +348,7 @@ void ICACHE_FLASH_ATTR console_handle_command(struct espconn *pespconn) { if (strcmp(tokens[0], "help") == 0) { os_sprintf(response, - "show [config|stats|mqtt|script]\r\n|set [ssid|password|auto_connect|ap_ssid|ap_password|network|dns|ip|netmask|gw|ap_on|ap_open|speed|config_port] \r\n|quit|save [config]|reset [factory]|lock []|unlock "); + "show [config|stats|mqtt|script]\r\n|set [ssid|password|auto_connect|ap_ssid|ap_password|network|dns|ip|netmask|gw|ap_on|ap_open|speed|config_port|broker_user|broker_password] \r\n|quit|save [config]|reset [factory]|lock []|unlock "); to_console(response); #ifdef SCRIPTED os_sprintf(response, "|script "); @@ -396,6 +396,14 @@ void ICACHE_FLASH_ATTR console_handle_command(struct espconn *pespconn) { // if static DNS, add it os_sprintf(response, config.dns_addr.addr ? "DNS: %d.%d.%d.%d\r\n" : "", IP2STR(&config.dns_addr)); to_console(response); + + if (os_strcmp(config.mqtt_broker_user, "none") != 0) { + os_sprintf(response, + "MQTT broker username: %s password: %s\r\n", + config.mqtt_broker_user, + config.locked ? "***" : (char *)config.mqtt_broker_password); + to_console(response); + } #ifdef MQTT_CLIENT os_sprintf(response, "MQTT client %s\r\n", mqtt_enabled ? "enabled" : "disabled"); to_console(response); @@ -819,6 +827,23 @@ void ICACHE_FLASH_ATTR console_handle_command(struct espconn *pespconn) { goto command_handled; } #endif + if (strcmp(tokens[1], "broker_user") == 0) { + os_strncpy(config.mqtt_broker_user, tokens[2], 32); + config.mqtt_broker_user[31] = '\0'; + os_sprintf(response, "Broker username set\r\n"); + goto command_handled; + } + + if (strcmp(tokens[1], "broker_password") == 0) { + if (os_strcmp(tokens[2], "none") == 0) { + config.mqtt_broker_password[0] = '\0'; + } else { + os_strncpy(config.mqtt_broker_password, tokens[2], 32); + config.mqtt_broker_password[31] = '\0'; + } + os_sprintf(response, "Broker password set\r\n"); + goto command_handled; + } #ifdef NTP if (strcmp(tokens[1], "ntp_server") == 0) { os_strncpy(config.ntp_server, tokens[2], 32); @@ -1188,7 +1213,21 @@ void ICACHE_FLASH_ATTR user_set_station_config(void) { wifi_station_set_auto_connect(config.auto_connect != 0); } -void ICACHE_FLASH_ATTR user_init() { + +bool ICACHE_FLASH_ATTR mqtt_broker_auth(const char* username, const char *password) { + if (os_strcmp(config.mqtt_broker_user, "none") == 0) + return true; + + if (os_strcmp(username, config.mqtt_broker_user) != 0 || + os_strcmp(password, config.mqtt_broker_password) != 0) { + os_printf("Authentication with %s/%s failed\r\n", username, password); + return false; + } + return true; +} + + +void user_init() { struct ip_info info; connected = false; @@ -1297,11 +1336,12 @@ void ICACHE_FLASH_ATTR user_init() { espconn_tcp_set_max_con(10); os_printf("Max number of TCP clients: %d\r\n", espconn_tcp_get_max_con()); + MQTT_local_onData(MQTT_local_DataCallback); + MQTT_server_onAuth(mqtt_broker_auth); + MQTT_server_start(1883 /*port */ , 30 /*max_subscriptions */ , 30 /*max_retained_items */ ); - MQTT_local_onData(MQTT_local_DataCallback); - //Start task system_os_task(user_procTask, user_procTaskPrio, user_procTaskQueue, user_procTaskQueueLen);