Try to fix decrypted payload quirks if it fails to parse as json#236
Try to fix decrypted payload quirks if it fails to parse as json#236rytilahti merged 1 commit intorytilahti:masterfrom
Conversation
miio/protocol.py
Outdated
| # 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.
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.
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.
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.
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.
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.
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.
@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.
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.
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.
It's great to have tests for such behaviours, so thanks for this!
miio/tests/test_protocol.py
Outdated
| 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.
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.
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.
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.
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.
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..."]}\x00xThe extra last byte will prevent miio from loading this data as this is invalid json.
This will fix dgiese/dustcloud#77