-
-
Notifications
You must be signed in to change notification settings - Fork 554
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
Conversation
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")] |
There was a problem hiding this comment.
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'
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
miio/tests/test_protocol.py
Outdated
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
miio/tests/test_protocol.py
Outdated
@@ -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) |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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!
miio/tests/test_protocol.py
Outdated
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) |
There was a problem hiding this comment.
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): |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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)
Tested and works fine with vacuum & strip, so let's merge this. Thanks again! 👍 |
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