peterhinch / micropython-mqtt

A 'resilient' asynchronous MQTT driver. Recovers from WiFi and broker outages.
MIT License
549 stars 116 forks source link

"Bad CONNACK" when connecting to HiveMQ broker #100

Closed JayPalm closed 1 year ago

JayPalm commented 1 year ago

I am trying to connect to a free broker from HiveMQ. Everything works fine when I use the following config on a public test broker (from eclipse)

config["client_id"] = "uPy-client-004"
config["port"] = 8883
config["ssl"] = True
# # Eclipse config
# config["server"] = "mqtt.eclipseprojects.io"
# config["user"] = "rw"
# config["password"] = "readwrite"

However, when I switch the server, user, and password to the appropriate values for my HiveMQ broker, I receive the "BAD CONNACK" error.

This exact config (server, port, user, pass, etc) work successfully in other clients, including the simple umqtt included in micropython-lib.

It appears to be caused by this clause in the _connect method:

if resp[3] != 0 or resp[0] != 0x20 or resp[1] != 0x02:
            raise OSError(-1, "Bad CONNACK")  # Bad CONNACK e.g. authentication fail.

This is the value of resp when connecting to Eclipse (working config): bytearray(b' \x02\x00\x00')

This is the value of resp when connecting to HiveMQ (not working): bytearray(b' \x02\x00\x05')

I've been trying figure out what that 4th byte in the response means, beyond indicating that the authentication was unsuccessful.

peterhinch commented 1 year ago

resp[3] is the MQTT connect return code. A value of 0 means connection accepted. Unfortunately the broker is rejecting your connection. [EDIT] My previous response was wrong. A value of 5 means "0x05 Connection Refused, not authorized" (from MQTT spec para 3.2.2.3 Connect Return code). I think you may need to create an account.

peterhinch commented 1 year ago

I have pushed an update improving the error reporting. README section 7 now explains these error codes.

JayPalm commented 1 year ago

Thanks for the reply and explanation. Any idea why a config (server, user, password) would work with the mqtt.simple module in micropython-lib but not here? The response of "Not Authorized" suggests an issue with user/password to me, but I've checked and double checked that they work with other clients.

Thanks for your help.

peterhinch commented 1 year ago

I can replicate this problem. I created an account with HiveMQTT and ran their Paho/Python3 example successfully. Attempts to connect with mqtt_as produce the response code 5.

You report success using mqtt.simple: I can't replicate this - I get response code 5. Please could you post your successful script (feel free to redact password and username).

I think this is the key to a solution because my ._connect method is based on that in mqtt.simple.

You are not the first to struggle with HiveMQTT: see https://github.com/peterhinch/micropython-mqtt/issues/94.

ebolisa commented 1 year ago

You are not the first to struggle with HiveMQTT: see #94.

I'm using a pico w to send json data to nodered via that server and, afaik, I don't get any errors. I also use nodered to send control signals to the pico. Nodered is installed on my Rpi 3+ at home but, accessible via internet.

# Define configuration
config['server'] = 'broker.mqttdashboard.com'  # Change to suit
config['ssid'] = wifimgr.get_profiles()[0] # using a modified wifimgr.py to get my wifi credentials
config['wifi_pw'] = wifimgr.get_profiles()[1]
config['user'] = 'my_user_name'
config['password'] = 'my_pass'
config['subs_cb'] = sub_cb
config['wifi_coro'] = wifi_han
config['will'] = (pub_topic, 'Goodbye cruel world!', False, 0)
config['connect_coro'] = conn_han
config['keepalive'] = 120

# Set up client. Enable optional debug statements.
MQTTClient.DEBUG = False
client = MQTTClient(config)

# asyncio.create_task(temp_sensor())

try:
    asyncio.run(main(client))
finally:  # Prevent LmacRxBlk:1 errors.
    client.close()
    green_led(True) # led section modified
    asyncio.new_event_loop() 

mqtt_as: VERSION = (0, 7, 0)

peterhinch commented 1 year ago

I suspect I didn't make myself clear. I understood that you had achieved success with mqtt.simple(). I found it failed so I wanted to see your working script that used mqtt.simple(). Mine is

from simple import MQTTClient  # simple.py is in the Pico root directory

import do_connect  # My connection script
do_connect.do_connect()

def main():
    c = MQTTClient("",
                   server='106b0a678db04a00be920085ef5f84f3.s2.eu.hivemq.cloud',
                   user="pythoncoder",
                   password="my_password",
                   ssl=True
                   )
    c.connect()  # FAILS with code 5
    c.publish(b"foo_topic", b"hello")
    c.disconnect()

main()

If you have a similar script that works, hopefully we'll be on the way to a solution.

At the moment I'm baffled by this. HiveMQ uses V5 of the MQTT protocol but their Paho example works if you specify V3.1.1. The connect string from simple.py and mqtt_as.py is correct for V3.1.1. So both should work, but in my testing neither do.

ebolisa commented 1 year ago

If you have a similar script that works, hopefully we'll be on the way to a solution.

This' what I tried:

import network
from utime import sleep
from umachine import reset

ssid = "my_ssid"
password = "my_pass"

def wifiConnect():
    print('-- Connecting...')
    wifi = network.WLAN(network.STA_IF)
    wifi.active(True)
    if wifi.isconnected():
        wifi.disconnect()
    wifi.connect(ssid, password)
    wifi.ifconfig(('192.168.1.99', '255.255.255.0', '192.168.1.1', '8.8.8.8'))
    sleep(2)

    cnt = 0
    while not wifi.isconnected():
        print('Re_Connecting...')
        sleep(2)
        cnt += 1
        if cnt == 10:
            t = '** Reset ESP **'
            print(t)
            sleep(5)
            reset()

    print('Wifi connected')
    print(wifi.ifconfig())

    sleep(5)
    return True

print(wifiConnect())

from simple import MQTTClient  # simple.py is in the Pico root directory
# could not find it so I used this one: https://github.com/micropython/micropython-lib/blob/master/micropython/umqtt.simple/umqtt/simple.py

def main():
    c = MQTTClient("",
                   server='broker.mqttdashboard.com',
                   user='username',
                   password='user_pass',
                   ssl=True
                   )
    c.connect()  # FAILS with code 5
    c.publish(b"foo_topic", b"hello")
    c.disconnect()

main()

and this' what I got:

>>> %Run -c $EDITOR_CONTENT
-- Connecting...
Re_Connecting...
Wifi connected
('192.168.1.99', '255.255.255.0', '192.168.1.1', '8.8.8.8')
True
>>> 

from mqttx app: Topic: foo_topicQoS: 0 hello 2023-01-03 15:48:19:264

edit1: That's if the simple.py lib I used is correct. edit2: The only difference I see is the name of the server.

peterhinch commented 1 year ago

We are both using the same simple.py. The fact that we have different connect scripts is irrelevant. In both cases we have an established connection. So:

edit2: The only difference I see is the name of the server.

Agreed.

I don't know what is broker.mqttdashboard.com. As far as I can see, to access HiveMQ you need to use the URL they supply you with. Yours will be something similar to 106b0a678db04a00be920085ef5f84f3.s2.eu.hivemq.cloud. Perhaps you could try the "simple" script with that URL.

With their URL I can access HiveMQ with mosquitto and with Paho/Python but not with simple.py.

As a general point, if simple.py does not work, mqtt_as won't either because their connect methods are very similar. So I suggest we work on getting simple.py working as it's easier to debug being, er, simple :) Conversely, if we can connect with simple.py then I'm confident of being able to fix any problem with mqtt_as.

ebolisa commented 1 year ago

Ok. If you click on the "broker" link you mentioned above, it redirects to a dashboard which is the one I'm using. If you click on "106b0a678db04a00be920085ef5f84f3.s2.eu.hivemq.cloud" it goes nowhere. and I guess this' the reason the code fails:

>>> %Run -c $EDITOR_CONTENT
-- Connecting...
Re_Connecting...
Wifi connected
('192.168.1.99', '255.255.255.0', '192.168.1.1', '8.8.8.8')
True
Traceback (most recent call last):
  File "<stdin>", line 50, in <module>
  File "<stdin>", line 46, in main
  File "simple.py", line 110, in connect
MQTTException: 5
>>> 
peterhinch commented 1 year ago

Each HiveMQ user gets their own URL so the specific 106b0a678db04a00be920085ef5f84f3.s2.eu.hivemq.cloud URL will only work with my username and password.

peterhinch commented 1 year ago

EUREKA.

The key is that the server name must be broker.mqttdashboard.com. Then both simple.py and mqtt_as work.

This is quite obscure as all the examples they post use the long cryptic URL. @ebolisa To satisfy my curiosity, where did you find the name broker.mqttdashboard.com?

ebolisa commented 1 year ago

To satisfy my curiosity, where did you find the name broker.mqttdashboard.com?

Clicking on that link you're directed to their web. Click on X or "Cancel" on the popup window. The login settings are on the right side of the page. Cheers.

peterhinch commented 1 year ago

I'm still puzzled by the two URL's. With mosquitto_sub I can only connect to the long hex URL, yet the Python solutions can only connect to broker.mqttdashboard.com. The two are separate. My async test script regularly publishes but if I subscribe with mosquitto_sub I get nothing. So my prime debugging tools mosquitto_pub and mosquitto_sub are useless.

Tomorrow I'll verify that two async clients can talk to each other, but surely they will...

ebolisa commented 1 year ago

Tomorrow I'll verify that two async clients can talk to each other, but surely they will...

I cannot help you much there Peter. I'm not a programmer but a retired bio-med engineer who just cannot stay away from electronics. Have a wonderful year!

JayPalm commented 1 year ago

@peterhinch I guess I misspoke regarding the mqtt.simple example config, it is slightly different. See here for what needs to be added to the client config to get it working, basically this:

config["ssl_params"] = {
    "server_hostname": "<broker_address>"
}

However, adding this to the config for mqtt_as hasn't yielded success for me yet.

Putting together a script, will post shortly.

Update: this gist is working for me, feel free to run it with my broker and credentials.

JayPalm commented 1 year ago

I am having success with this now: gist.

I added the ssl_param dict as above. I'm not sure why it wasn't working before.

peterhinch commented 1 year ago

Thank you. I was beginning to think I was going to have to search for a bug in both clients.

I now have this working with mqtt_as, testing with mosquitto_pub and mosquitto_sub. For reference here is my config:

# Credentials
broker = '106b0a678db04a00be920085ef5f84f3.s2.eu.hivemq.cloud'
config['user'] = 'my username'
config['password'] = 'my password'
# Define configuration
config['will'] = (TOPIC, 'Goodbye cruel world!', False, 0)
config['keepalive'] = 120
config["queue_len"] = 1  # Use event interface with default queue
config['server'] = broker
config['ssl'] = True
config['ssl_params'] = {"server_hostname": broker}  # magic

I have added these notes to the docs.

JayPalm commented 1 year ago

No problem, and thanks for the info/help, @peterhinch! Would you accept a PR that showed or noted this "magic" addition in the TLS examples, and perhaps in the readme?

peterhinch commented 1 year ago

I'm always open to suggestions, but please check out my latest version of the readme as I updated it late yesterday.

I'd rather not change the code of the TLS samples as these have been tested, but code comments would be welcome.

peterhinch commented 1 year ago

Does anyone know why this works?

config['ssl_params'] = {"server_hostname": broker}

I had discounted TLS problems because the server responded to connect with a correctly formatted CONNACK packet (albeit showing failure). This implies that encryption and decryption was working correctly in both directions. Yet the server_hostname key is evidently necessary.

Any guidance or doc references on this dict would be much appreciated.