Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try to fix decrypted payload quirks if it fails to parse as json #236

Merged
merged 1 commit into from
Mar 4, 2018

Conversation

jschmer
Copy link
Contributor

@jschmer jschmer commented Feb 25, 2018

It sometimes happens that Xiaomi cloud sends an extra byte after \x00. Need to strip everything after the last \x00 to produce valid json data.

This is an example of decrypted data that Xiaomi cloud sent to my Gen1 vacuum:
{"id":123456789,"result":["https://awsbj0.fds.api.xiaomi.com/robomap/roboroommap...","https://awsbj0.fds.api.xiaomi.com/robomap/roboroommap...","https://awsbj0.fds.api.xiaomi.com/robomap/roboroommap...","https://awsbj0.fds.api.xiaomi.com/robomap/roboroommap..."]}\x00x

The extra last byte will prevent miio from loading this data as this is invalid json.

This will fix dgiese/dustcloud#77

@coveralls
Copy link

coveralls commented Feb 25, 2018

Coverage Status

Coverage increased (+0.7%) to 67.144% when pulling 0128d89 on jschmer:JsonLoadFix into 9927ef0 on rytilahti:master.

miio/protocol.py Outdated
@@ -151,7 +151,7 @@ def _decode(self, obj, context, path):
try:
# pp(context)
decrypted = Utils.decrypt(obj, context['_']['token'])
decrypted = decrypted.rstrip(b"\x00")
decrypted = decrypted[:decrypted.rfind(b"\x00")]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your solution isn't accurate. rfind returns -1 if there is no \x00 at all. decrypted will be shortened by one character in this case. The previous code didn't touch the payload.

>>> decrypted = "asdfg"
>>> decrypted.rstrip("f")
'asdfg'
>>> decrypted[:decrypted.rfind("f")]
'asd'
>>> decrypted[:decrypted.rfind("h")]
'asdf'

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. There is already a fixup for a different invalid json string scenario in that method so I'll extend that to add this special case.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You didn't get my point. Your code introduces a new case. If 0x00 isn't present the json will be destroyed now because the last char (}) will be removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I wasn't clear enough in my comment. I'll revert this change and instead provide a similar way to fix the decrypted data like is done for the powerstrip edge case.

self.assertEqual(parsed_msg.data.value['id'], 123456)
self.assertEqual(parsed_msg.data.value['otu_stat'], 0)

# can parse message with invalid json for edge case xiaomi cloud reply to _sync.batch_gen_room_up_url

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (109 > 100 characters)

miio/protocol.py Outdated
lambda decrypted_bytes: decrypted_bytes.replace(b',,"otu_stat"', b',"otu_stat"'),
# xiaomi cloud returns invalid json when answering _sync.batch_gen_room_up_url
# command so try to sanitize it
lambda decrypted_bytes: decrypted_bytes[:decrypted_bytes.rfind(b'\x00')] if b'\x00' in decrypted_bytes else decrypted_bytes

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (135 > 100 characters)

miio/protocol.py Outdated
else decrypted_bytes
]

for i, fix in enumerate(decrypted_fixes):
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@syssi This loop tries to fix the decrypted data as long as it can't be loaded as json. This makes it easy to add additional fixes for new edge cases and it won't meddle with the data when it can be loaded the first time.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I think such pieces are called quirks sometimes, but this will do fine 👍 I have not yet tested the code itself, but will do it later this week.


checksum = Utils.md5(magic+length+unknown+did+epoch+token+encrypted_data)

return magic+length+unknown+did+epoch+checksum+encrypted_data
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Had to write my own message building function because Message.build() will always produce valid json but we want to explicitly test if Message.parse() can handle invalid json input.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's great to have tests for such behaviours, so thanks for this!

@@ -17,7 +19,7 @@ def test_encrypt(self):

encrypted = Utils.encrypt(payload, token)
decrypted = Utils.decrypt(encrypted, token)
self.assertEquals(payload, decrypted)
self.assertEqual(payload, decrypted)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As we use pytest, this could be simply assert payload == decrypted I suppose.


checksum = Utils.md5(magic+length+unknown+did+epoch+token+encrypted_data)

return magic+length+unknown+did+epoch+checksum+encrypted_data
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's great to have tests for such behaviours, so thanks for this!

self.assertIsNotNone(parsed_msg.data)
self.assertIsNotNone(parsed_msg.data.value)
self.assertIsInstance(parsed_msg.data.value, dict)
self.assertEqual(parsed_msg.data.value['id'], 123456)
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See the above comment about pytest & assert, also https://docs.pytest.org/en/latest/assert.html .

miio/protocol.py Outdated
else decrypted_bytes
]

for i, fix in enumerate(decrypted_fixes):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! I think such pieces are called quirks sometimes, but this will do fine 👍 I have not yet tested the code itself, but will do it later this week.

@dgiese
Copy link

dgiese commented Feb 28, 2018

Great fix ;) Was wondering where the weird errors were originating from.

miio/protocol.py Outdated
return json.loads(decoded)
except Exception as ex:
_LOGGER.error("unable to parse json '%s': %s", decoded, ex)
# log the error when decrypted bytes couldn't be loaded after trying all quirk adaptions

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

line too long (104 > 100 characters)

@jschmer jschmer changed the title Discard everything after the last \x00 byte Try to fix decrypted payload quirks if it fails to parse as json Mar 3, 2018
@rytilahti
Copy link
Owner

Tested and works fine with vacuum & strip, so let's merge this. Thanks again! 👍

@rytilahti rytilahti merged commit 761a9b3 into rytilahti:master Mar 4, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

unable to parse json
6 participants