libpurecool icon indicating copy to clipboard operation
libpurecool copied to clipboard

Invalid login since 11/01/2021 ?

Open qdel opened this issue 4 years ago • 78 comments

Hi,

Since the 11/01/2021 morning, the login webservice return a 500 error with the html page: error preview.

The dyson link application seems to work. Credential are correct.

Note also that cloudfare seems to limit the number of request we can do on this webservice. After a couple of 500, we take an error 429 with a Retry-After header set to 3600. (if you want, i made a fix for this and can make a PR).

qdel avatar Feb 11 '21 10:02 qdel

Update:

Since around 12:00 Paris time i now meet a 401/{"Message":"Unable to authenticate user."}.

But i can still connect with the same credential using dyson link application and dyson website.

Sadly i don't know how to trace trafic between my phone and their webservices.

qdel avatar Feb 11 '21 12:02 qdel

Last time we had login issues with libpurecool they were caused by a lack of header info in the request, causing us to add a user agent (same as the app). It was assumed at the time this would eventually be blocked by dyson.

googanhiem avatar Feb 11 '21 13:02 googanhiem

Same issue here, as of today all 3 of my Dyson fans are unable to auth. Any ideas?

Tloram avatar Feb 11 '21 14:02 Tloram

Maybe if someone can track how the dyson app connect to the server, we will have all infos.

I can try making a custom made webserver and rerouting the dns call of my phone. But can't it right now.

qdel avatar Feb 11 '21 14:02 qdel

All I can work out MITM-ing my phone is that it's using the linkapp-api.dyson.com domain

bfayers avatar Feb 11 '21 15:02 bfayers

Also an FYI @shenxn is looking to build a new local control component to link Dyson to Home Assistant.

Not sure if they'd be able to shed any light on any changes to the Dyson API.

Another FYI, @etheralm has said he doesn't really have the time to work on this anymore, so its unlikely we'll be able to easily PR whatever change is needed for this issue.. probably needs a fork at this point.

googanhiem avatar Feb 11 '21 15:02 googanhiem

Header needs to be changed to android client and then it works again. Source: https://github.com/lukasroegner/homebridge-dyson-pure-cool/pull/153

I tested this via postman and got the account/password things in response.

bfayers avatar Feb 11 '21 15:02 bfayers

Header needs to be changed to android client and then it works again.

Just tested this change to libpurecool in home assistant (by editing current header in the dyson.py) and it didn't work for me.

googanhiem avatar Feb 11 '21 16:02 googanhiem

Just tested this change to libpurecool in home assistant (by editing current header in the dyson.py) and it didn't work for me.

The same request I tried earlier in Postman now doesn't work. seems to be hit or miss as to whether it's going to work or not 🤔

Once this auth is worked out, it may be worth considering saving the credentials the API gives in the hass integration - depending on how long they last ofc.

bfayers avatar Feb 11 '21 16:02 bfayers

@googanhiem Ah, interesting the android client header works but only if you log out of the app, then log back in and the endpoint will work with that header for.... an amount of time or number of requests that I don't know yet.

I guess this could be related to the app talking to linkapp-api.dyson.com at some point during it's auth process.

~~EDIT: after double checking it looks like re-authing the app then using the header already in the library also works.~~

Something has to be screwy though, I can't get the library to auth even if Postman can.

bfayers avatar Feb 11 '21 16:02 bfayers

Yeah, I can't replicate the behaviour you're talking about, too bad it would be a decent temp fix.

Maybe some of the auth requests are making it through cloudflare at the moment... so if you reboot and it works.. hold off rebooting for a while if you can.

googanhiem avatar Feb 11 '21 17:02 googanhiem

Yeah, something is really odd - I've got the Account and Password auths from Postman luckily - I can manually make the library work with testing like this:

dc = DysonAccount("","","")
dc._logged = True
dc._auth = HTTPBasicAuth("accountresponsefrompostman","passwordresponsefrompostman")

devs = dc.devices()
connected = devs[0].connect("ipaddress")
devs[0].turn_on()

which does result in the fan turning on.

I was able to achieve HASS functionality again by editing /usr/src/homeassistant/homeassistant/components/dyson/__init__.py

adding an import of from requests.auth import HTTPBasicAuth and below the call for logged = dyson_account.login() adding in:

    logged = True
    dyson_account._logged = True
    dyson_account._auth = HTTPBasicAuth("account", "password")

Of course, I've no idea how long those values last.

And just to clarify, it seems to be repeatable that using Postman to POST https://appapi.cp.dyson.com/v1/userregistration/authenticate?country=GB with appropriate JSON data always results in the Account and Password values if done immediately after re-authenticating the official dyson app - even with a user agent of DysonLink/29019 CFNetwork/1188 Darwin/20.0.0

Exporting the postman request as code is this:

import requests

url = "https://appapi.cp.dyson.com/v1/userregistration/authenticate?country=GB"

payload="{\r\n    \"Email\": \"emailhere\",\r\n    \"Password\": \"passwordhere\"\r\n}"
headers = {
  'User-Agent': 'DysonLink/29019 CFNetwork/1188 Darwin/20.0.0',
  'Content-Type': 'application/json'
}

response = requests.request("POST", url, headers=headers, data=payload)

print(response.text)

And this does actually work - providing you've just re-authenticated the official app.

I also think it's worth mentioning that both those values have not changed during all my testing today.

bfayers avatar Feb 11 '21 18:02 bfayers

To properly send JSON data, you can use

response = requests.request("POST", url, headers=headers, json=payload)

instead of manually add Content-Type header. Note payload should be a dictionary instead of string.

shenxn avatar Feb 12 '21 02:02 shenxn

I think I found the problem. To make the authentication work, we should first check account status by making a GET request to /v1/userregistration/userstatus?country=GB&email=YOUR_EMAIL_ADDRESS and then do the normal login.

shenxn avatar Feb 12 '21 03:02 shenxn

@shenxn Just tried your recommendation in my ioBroker-Adapter and can confirm: it's working.

Grizzelbee avatar Feb 12 '21 10:02 Grizzelbee

instead of manually add Content-Type header. Note payload should be a dictionary instead of string.

Ah yeah, my bad - that's just the code that postman generated.

bfayers avatar Feb 12 '21 11:02 bfayers

So in summary we only need to add this piece of code before we do the login.

        requests.get("https://{0}/v1/userregistration/userstatus?country={1}&email={2}".format(
            self._dyson_api_url, self._country, self._email
        ),
            headers=self._headers,
            verify=False
        )

Alexwijn avatar Feb 12 '21 11:02 Alexwijn

So in summary we only need to add this piece of code before we do the login.

        requests.get("https://{0}/v1/userregistration/userstatus?country={1}&email={2}".format(
            self._dyson_api_url, self._country, self._email
        ),
            headers=self._headers,
            verify=False
        )

You can use the params arg instead of string format so that requests can handle URL encode for you.

shenxn avatar Feb 12 '21 11:02 shenxn

You can use the params arg instead of string format so that requests can handle URL encode for you.

That is also an option, I just thought I would match the current code style.

Alexwijn avatar Feb 12 '21 12:02 Alexwijn

Hey @shenxn - apologies I am a tad confused. Are you suggesting the only change required is to add adding the following before login within the dyson.py file? The changes from @bfayers are not required?

requests.get("https://{0}/v1/userregistration/userstatus?country={1}&email={2}".format( self._dyson_api_url, self._country, self._email ), headers=self._headers, verify=False )

I have tried that but still not working so just curious if @bfayers changes are also requried and your suggestion gets around having to auth using the phone app before login from HA?

class DysonAccount: """Dyson account."""

def __init__(self, email, password, country):
    """Create a new Dyson account.

    :param email: User email
    :param password: User password
    :param country: 2 characters language code
    """
    self._email = email
    self._password = password
    self._country = country
    self._logged = False
    self._auth = None
    self._headers = {'User-Agent': DYSON_API_USER_AGENT}
    if country == "CN":
        self._dyson_api_url = DYSON_API_URL_CN
    else:
        self._dyson_api_url = DYSON_API_URL

def login(self):
    **"""Check Dyson Account Status"""
    requests.get("https://{0}/v1/userregistration/userstatus?country={1}&email={2}".format(
        self._dyson_api_url, self._country, self._email
    ),
        headers=self._headers,
        verify=False
    )**

    """Login to dyson web services."""
    urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
    _LOGGER.debug("Disabling insecure request warnings since "
                  "dyson are using a self signed certificate.")

    request_body = {
        "Email": self._email,
        "Password": self._password
    }
    login = requests.post(
        "https://{0}/v1/userregistration/authenticate?country={1}".format(
            self._dyson_api_url, self._country),
        headers=self._headers,
        data=request_body,
        verify=False
    )

abshgrp avatar Feb 12 '21 12:02 abshgrp

Based on this thread I've added exactly that to my Kotlin integration with Dyson and it seems to work.

The only difference is that I parse the result of the userstatus call to check the account is ACTIVE.

bmorris591 avatar Feb 12 '21 12:02 bmorris591

I have tried just @bfayers changes but I keep getting the following error. What am I doing wrong here?

Traceback (most recent call last): File "/usr/src/homeassistant/homeassistant/setup.py", line 213, in _async_setup_component result = await task File "/usr/local/lib/python3.8/concurrent/futures/thread.py", line 57, in run result = self.fn(*self.args, **self.kwargs) File "/usr/src/homeassistant/homeassistant/components/dyson/init.py", line 68, in setup dyson_devices = dyson_account.devices() File "/usr/local/lib/python3.8/site-packages/libpurecool/dyson.py", line 93, in devices for device in device_response.json(): File "/usr/local/lib/python3.8/site-packages/requests/models.py", line 900, in json return complexjson.loads(self.text, **kwargs) File "/usr/local/lib/python3.8/site-packages/simplejson/init.py", line 525, in loads return _default_decoder.decode(s) File "/usr/local/lib/python3.8/site-packages/simplejson/decoder.py", line 370, in decode obj, end = self.raw_decode(s) File "/usr/local/lib/python3.8/site-packages/simplejson/decoder.py", line 400, in raw_decode return self.scan_once(s, idx=_w(s, idx).end()) simplejson.errors.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

image

abshgrp avatar Feb 12 '21 12:02 abshgrp

Hmm...I got it working now but I also needed to change the login logic to this:

        login = requests.post(
            "https://{0}/v1/userregistration/authenticate?country={1}".format(
                self._dyson_api_url, self._country),
            headers=self._headers,
            json=request_body,
            verify=False
        )

Notice I changed data to json.

Alexwijn avatar Feb 12 '21 12:02 Alexwijn

Hey @Alexwijn did you only change the dyson.py file not the init.py?

abshgrp avatar Feb 12 '21 12:02 abshgrp

@bfayers code works great. Thanks all

googanhiem avatar Feb 12 '21 13:02 googanhiem

Thanks @bfayers! Updated dyson.py as per your commit and restored my init.py back to original and Dyson integration is now working again.

For anyone else that is confused and possibly not as technically proficient like myself, you only need to update the dyson.py file located /usr/local/lib/python3.8/site-packages/libpurecool. Refer to merge request above. Cheers

abshgrp avatar Feb 12 '21 13:02 abshgrp

@bfayers just saw your PR, made an answer on https://github.com/home-assistant/core/issues/46400#issuecomment-778177969 but it was not the correct place to.

Find on my code that i also manage a http error 429.

qdel avatar Feb 12 '21 13:02 qdel

Find on my code that i also manage a http error 429.

@qdel Yeah - if you try to talk to the authentication API too much you'll get 429'd and have to wait an hour before trying again

Thanks @bfayers! Updated dyson.py as per your commit and restored my init.py back to original and Dyson integration is now working again.

@abshgrp Great to hear!

bfayers avatar Feb 12 '21 13:02 bfayers

#38 fixes it for me too, thanks @bfayers, @shenxn and others that helped figure out the solution! In case this helps others, rather than patch by hand I upgrade-installed the updated branch straight from git into my HA Supervised install before restarting: docker exec -t homeassistant pip3 install --upgrade git+https://github.com/bfayers/libpurecool.git@fix_auth

crowbarz avatar Feb 12 '21 15:02 crowbarz

@crowbarz Thanks for your help, now my Dyson is working again.

#38 fixes it for me too, thanks @bfayers, @shenxn and others that helped figure out the solution! In case this helps others, rather than patch by hand I upgrade-installed the updated branch straight from git into my HA Supervised install before restarting: docker exec -t homeassistant pip3 install --upgrade git+https://github.com/bfayers/libpurecool.git@fix_auth

Thanks for your help, Now my Dyson is working again.

kritin81 avatar Feb 12 '21 16:02 kritin81