From a3d09ee444186b4ef7bd9668cbfcd065975df136 Mon Sep 17 00:00:00 2001 From: William Pietri Date: Sun, 3 Dec 2017 17:43:06 -0800 Subject: [PATCH] Hopefully adding support for other countries. --- Pipfile | 3 +- article/index.md | 398 +++++++++++++++++++++++++++++++++++++++++++++++ sucks.py | 42 +++-- test_sucks.py | 15 +- 4 files changed, 441 insertions(+), 17 deletions(-) create mode 100644 article/index.md diff --git a/Pipfile b/Pipfile index e49f2a0..9dc88d8 100644 --- a/Pipfile +++ b/Pipfile @@ -21,4 +21,5 @@ requests-mock = ">=1.3" sleekxmpp = ">=1.3" click = ">=6" requests = ">=2.18" -pycryptodome = ">=3.4" \ No newline at end of file +pycryptodome = ">=3.4" +"pycountry-convert" = ">=0.5" diff --git a/article/index.md b/article/index.md new file mode 100644 index 0000000..4bdb314 --- /dev/null +++ b/article/index.md @@ -0,0 +1,398 @@ +# Developer Attacks Robot! (adventures in reverse engineering) + +## outline + +- Introduction - + - I'm writing this up because these skills aren't just useful for dealing with robots; they're a good way to figure out existing systems debugging legacy code. + - I'm doing this all with Linux, but this all can be done +- snoop the traffic - let's set up as an access point and just use Wireshark to see what it's doing. + - close everything, put it in battery-saving mode + - DNS! Fine! + - HTTPS! Urgh. + - And what's this stuff? +- XMPP + - what's this protocol + - it's controlling the vacuum, so let's dig in + - what can I use? aha, mitmproxy + - wow, that's a bunch of junk! + - boil it down some + - boil it down some more +- start hacking + - simple XMPP script + - what's this auth stuff + - an RFC side trip + - send some commands + - oh my god, it works! +- auth troubles + - now it doesn't work, what happened? + - the android app is breaking things + - now we have to look at the HTTPS stuff + - mitmproxy to the rescue +- HTTPS + - 6 calls in 2 sets but hey, there in #4 is a token that works + - Let's make our hack script work for the second set + - it works! But what about this garbage? + - We can't figure it out from the protocol alone. To the code! +- APK decompliation + - Let's search for key strings + - Well, that's some strings. Where are the rest? + - Ok, more strings. Let's try implementing something. + - Can we send it with no signature? Nope. + - Can we send it with a fake signature? Nope. + - Gotta do it for real. Let's bring it! + - Hack, fail, hack, fail. (Scary! Low feedback!) Hack, succeed! +- Wrapping up + - Let's make a tidy final script (and share it!) + - What did we learn? + +## tl;dr + +I bought a robot vacuum. The control app didn't do quite what I wanted. Here I +explain in too much detail how I figured out how the app was talking to the robot +so that I could control it from my own code. Tools used include Wireshark, +mitmproxy, xmppproxy, adb, and a Java decompiler. + +## introduction + +After many years as a sysadmin and developer, after thousands of hours spent +running down problems, this is basically how I feel about other people's code: + +> I don't know who you are. I don't know what you want. If you are looking for +> ransom I can tell you I don't have money, but what I do have are a very +> particular set of skills. Skills I have acquired over a very long career. +> Skills that make me a nightmare for people like you. +> -- [Liam Neeson, Taken](http://www.esquire.com/entertainment/movies/a31775/taken-speech/) + + + +Recently, I bought a robot vacuum, the [Wirecutter-recommended](https://thewirecutter.com/reviews/best-robot-vacuum/) +[EcoVacs Deebot N79](https://www.ecovacs.com/us/deebot-robotic-vacuum-cleaner/DEEBOT-N79/). +It's honestly a pretty swell piece of hardware. Watching it go, it's clear that +it's the product not just of smarts, but of a lot of thoughtful experimentation. +Compared with the early Roomba I had, it feels *evolved*. + +But the Android app is... not as good. In particular, you can have it start +cleaning at a certain time. However, it only stops cleaning when it runs out +of battery, or circa 90 minutes. My place is small; I need maybe 10 minutes +of daily cleaning. That bothered me. It's a mechanical device, and its brushes +and filters are consumables. It's not expensive, but having it waste 8/9ths +of its lifespan cleaning already-clean floors bugged me. I also didn't need +the extra noise and hassle. + +So first, to Google! Did anybody already fix this? Nope. Is there a public API? +Nope. Have other people looked into this? Turns out yes! There was a forum +post with a little discussion and some sample code. Which was interesting, +but not enough to build from. How to I find out how to control my property? + +Here follows my answer to that. It's a cleaned-up version of my explorations, +written to share some of my tricks with others, and help people get more +comfortable figuring out what the hell is going on with their tech. My +target audience is people who code at least enough to be dangerous. By the +end, I hope you'll be more dangerous. + + +## you say you wanna snoop + +The first quesiton to answer: what the hell is that app doing?: Modern phones are, +for good reason, basically sealed boxes. I could root my phone so that I could do +more effective debugging from there. But since I mainly care about what it's doing +over the network, I'm going to start with snooping the network traffic. + +### Wireshark to the rescue! + +An easy way to capture all that traffic is to make your laptop a wifi router. +I"m using an Ubuntu laptop, so I needed to plug into an Ethernet network and then +make a few [simple networking changes](https://askubuntu.com/questions/180733/how-to-setup-an-access-point-mode-wi-fi-hotspot). +(Other OSes have similar functionality, so you should be able to play along.) +Once I got that working, I connected my phone to the new wifi access point. + +The next step was to make things clean and simple. Complexity is the enemy of +any good debugging effort. I put my phone in Battery Saver mode, which +minimizes background activity. I force-stopped the EcoVacs app and cleared +out its data, so we were starting fresh. Then I rebooted my phone, turned +Battery Saver back on, and let it sit for a couple minutes so its startup +activity was hopefully complete. + + + +I also checked out the network settings to see what my phone's +IP was. That will become important once we have some packets captured. + +That ready, I started up one of my favorite tools, Wireshark. This is basicially +x-ray vision for networks, and it is fucking magic. It has a zillion options +and tools and protocol decoders, most of which we will studiously ignore. I +told it to start capturing my wireless interface. It shows you what's happening +in real time, so I saw various traffic ticking by, including a bunch of Google +stuff. That seemed good, and it was my phone's IP address, so I was capturing +the right activity. + +Now to start up the app! I jotted down the exact time in my notes and started +up the EcoVacs app. I noted the time I hit the login button, and watched it +churn for a bit as it found my vacuum and got connected. I noted the time again +when I told the vaccum to start cleaning. After a bit, I told it to go back to +its charger. When all was quiet again, I stopped the capture. + +[SIDEBAR: In this article, I'm making it sound like I did things once. The +truth is that I did most of these steps many times, separated by periods of +confusion and swearing. You're getting the edited version here, because the +truth would be deadly dull. But as you go through this, don't be discouraged +if you have to try something again and again because the results were messy +or puzzling.] + +### what's all this garbage? + +If you're playing along, you now have a screen with a lot of technical-looking +stuff, done up in the [angry fruit salad](http://catb.org/jargon/html/A/angry-fruit-salad.html) +school of user interface design. Scrolling, there are thousands of entries, +each more arcane than the last. What is it? + +Well, honestly, for the moment I don't care , and you shouldn't either. There's 30 years of +layered technical history that is undeniably cool and interesting. Your +phone does a lot! Other things on your network are also doing a lot! Later +on you can come back, pick a random packet, and try to figure out what it's +doing. But for now, you should treat this like you're an easily distractable +person shopping at a megastore. Don't look at everything; just find what you +need and get the hell out. + +A great place to start is DNS. That is the protocol where domain names get +turned into internet addresses, a necessary step before any connection is made +Since DNS mostly runs over UDP port 53, I type in a filter at the top of the +window: "udp.port==53". (How did I know it was 53? Experience. But you can +check out a [short list](http://www.pearsonitcertification.com/articles/article.aspx?p=1868080) +of common port numbers or Wikipedia's [more comprehensive list](https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers).) +Now I have a fair bit less garbage. I see queries +about things I know are on my phone: Wikipedia, Lastpass, Mint, and of course +Google. + +As I get down to the timestamp for when I started the app, I see a familiar +name: ecovacs.com. +That's a good sign! Now I'm in the right place. I set the this a a timestamp +reference. Now the times are relative +to when the app got going, making it easier for me to keep track of things. + Scrolling down, I see it look up some +related domain names: eco-us-api.ecovacs.com, users-na.ecouser.net, +ne-na.ecouser.net, msg-na.ecouser.net, and a few others. I keep those in +mind for future investigation. + +Now that I'm in the right ballpark, I clear the filter and start looking +at the raw traffic. What does it do right after it looks up the eco-us-api +name? It connects to it! Yay! There are a whole bunch of packets going back +and forth between my phone and that server. If I clieck on the first, Wireshark +even shows in the left edge that all those packets are related. + +However, the fact that it's connecting to port 443 is a problem. That's the +port for HTTPS. The S meaning Secure, meaning encrypted, meaning naughty +snoopers like us can't see anything useful. There's a solution for that, but +let's keep looking and see what else we have. + +Further down, there's a connection on port 5223. +That's unusual, and Wireshark +just calls the protocol TCP, meaning it's not sure what to make of it. What +is it? To find out, I bring up the menu for one of the packets, pick "Follow", +and then "TCP Stream". This pulls all the packets together for connection, +extracts the payload as texts, and then displays the client as red and the +server as blue. + + + +This is one of my favorite Wireshark tools. Expecially for plain HTTP and other +non-encrypted protocols, it's great for seeing what's really going on. Here, +we see some XML at the top and then it decends into mostly garbage. Let's look +at the XML. + + + +``` + + + + + + + + + + + PLAIN + + + + + + +``` + +I don't know much about this, but there's an obvious hint. So I look at +jabber.org, and it mentions XMPP. I hazily recall Jabber as an instant +messaging tool, and XMPP is the protocol. That seems weird, but the +[XMPP website](https://xmpp.org/) mentions that a common use case is +Internet of Things. That makes some sense. If you have a lot of things, +like my phone, wanting to talk occasionally to other things, like my robot vacuum, +IM is a well-tested approach. + +The TLS thing, though, is a bit of a problem. When it says starttls, it's +upgrading from the insecure connection, which we can read, to a secure +connection, which we can't. (For more, see Wikipedia's [Opportunistic TLS](https://en.wikipedia.org/wiki/Opportunistic_TLS) +article.) But there's some good news. As I scroll down in the Wireshark +extract, I see this bit: + +``` +AU1.0...U... +Some-State1!0...U. +..Internet Widgits Pty Ltd +``` + +AU? Some State? Interenet Widgits? I recognize those as default values +for a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate). +That means the app is maybe not so serious about security here. So this seems +like a good place to break in. + +## robot jabber + +I do a little more reading on [XMPP](https://en.wikipedia.org/wiki/XMPP). +It's a proper internet standard and everything, so I flip through [RFC 6120](https://tools.ietf.org/html/rfc6120), +[RFC 6121](https://tools.ietf.org/html/rfc6121), and [RFC 6122](https://tools.ietf.org/html/rfc6122), +the standards documents that describe it. It feels like (and is) something +from the late 90s, when XML was all the rage. + +I rummage around a bit, an a search for "xmpp mitm" (that being Man In The Middle), +I find [xmpppeek](https://www.beneaththewaves.net/Software/XMPPPeek.html), an +XMPP proxy. I love this kind of thing: somebody made it for their own needs, +found it useful, and put it up on the Internet just in case. + +### looking inside the XMPP + +I install xmpppeek. But how to get the app to talk to my server instead of +the real one? Lies. In particular, my router lets me set up hostnames. That's +probably so I can easily set up a home server, but here, I'll abuse it. I tell +it to lie about the address for [TODO: server name] and return my laptop address +instead. Then I start up xmpppeek and tell it to forward to the real EcoVacs +server. It complains, saying it needs its own encryption certificate. I can +never remember the incantation for that, but the Internet does, so shortly it's +up and running. Will it work? + +I start up the Ecovacs app and shit starts scrolling on my laptop screen: + + + +I don't know what half of this means, but we can definitely see farther than +before into the XMPP transaction, so this looks like a step forward. And this +bit is particularly interesting: + +``` +[2017-11-15 14:49:48.016593-0MjAxNzEwMjU1OWYwZWU2M2M1ODhkADIwMTcxMDI1NTlmMGVlNjNjNTg4ZAAwLzEzYjQxMjk5L2V5aDlXZ0NOOVFkWGxEZkNDY1I1cVBBMUtLSnZ4eWts7:00] [(S2C) 47.88.66.164:5223 -> 192.168.1.227:48607] }}}PLAIN{{{ +[2017-11-15 14:49:48.255088-07:00] [(C2S) 192.168.1.227:48607 -> 47.88.66.164:5223] }}}MjAxNzEwMjU1OWYwZWU2M2M1ODhkADIwMTcxMDI1NTlmMGVlNjNjNTg4ZAAwL2VmY2Y5M2U2L2dyOWhDZkc0Q3NmeVU3QlRQeVRDNkJaWVgxQjFVazhE{{{ +[2017-11-15 14:49:48.444622-07:00] [(S2C) 47.88.66.164:5223 -> 192.168.1.227:48607] }}}{{{ +``` + +The server says, "for authenticating, I like the PLAIN method". The client says, +"cool, here's a lump of PLAIN auth". The server says, "success". And then we say, +in our best hacker voices, "I'm in!" If we can authenticate with something +straightforward, maybe we're good to go. + +Fishing further, I see something that looks like a username "2017102559f0ee63c588d@ecouser.net". Maybe that will come +in handy.There are also +nice bits of XML like `` +when I hit the auto-clean button in the app, and `` +when I have it stop. Once we're connected, bossing the robot around should be easy. +So how do we get connected? + +## ruby slippers + +For things like this, I like Ruby and Python. The languages are easy to get +started with, forgiving, and they have a ton of libraries. For this, let's try +Ruby. I quickly find an [XMPP client library](http://xmpp4r.github.io/) that +isn't too stale and even a +[simple how-to article](https://www.sitepoint.com/looking-xmpp-xmpp4r-gem/). +So let's see if we can build something that at least talks to the server. + +I use bundler to install the xmpp4r gem and then write the dumbest possible +program: + +``` +require 'xmpp4r' +require 'xmpp4r/client' + +include Jabber + + +Jabber.debug = true + +jid = JID.new('2017102559f0ee63c588d@ecouser.net') +client = Client.new(jid) +client.connect +``` + +I run that and it works! It connects. It doesn't do anything, but I'm on +the right trail. Can we log in just using the magic string from the XML +above? I add this: + +``` +client.auth('MjAxNzEwMjU1OWYwZWU2M2M1ODhkADIwMTcxMDI1NTlmMGVlNjNjNTg4ZAAwL2VmY2Y5M2U2L2dyOWhDZkc0Q3NmeVU3QlRQeVRDNkJaWVgxQjFVazhE') + +``` + +Does it work? No. In the debugging output, it sends a different, longer +string than the one we gave it. Now, alas, we have to actually learn +something about how this works. Luckily, XML is an implausibly verbose +protocol, and looking at the auth exchange there are valuable clues like +SASL and PLAIN. Going back to the [XMPP spec](https://tools.ietf.org/html/rfc6120#section-6) +there is a whole section on SASL, and SASL even has an [RFC of its own](https://tools.ietf.org/html/rfc4422). +The XMPP RFC tells us that data gets encoded as base64. So let's try +decoding that. + +``` +$ base64 -d +MjAxNzEwMjU1OWYwZWU2M2M1ODhkADIwMTcxMDI1NTlmMGVlNjNjNTg4ZAAwL2VmY2Y5M2U2L2dyOWhDZkc0Q3NmeVU3QlRQeVRDNkJaWVgxQjFVazhE +2017102559f0ee63c588d2017102559f0ee63c588d0/efcf93e6/gr9hCfG4CsfyU7BTPyTC6BZYX1B1Uk8D +``` +I give it the first line; it gives me the second. The first part of that +looks like the username above, but then it trails off into nonsense. +What's the rest? More rummaging leads me to [RFC 4616](https://tools.ietf.org/html/rfc4616), +which is specifically about SASL PLAIN. There it explains the format: + +``` + message = [authzid] UTF8NUL authcid UTF8NUL passwd + authcid = 1*SAFE ; MUST accept up to 255 octets + authzid = 1*SAFE ; MUST accept up to 255 octets + passwd = 1*SAFE ; MUST accept up to 255 octets + UTF8NUL = %x00 ; UTF-8 encoded NUL character +``` + +That's somewhat helpful. A password is somethign I understand. The docs say +that the two ids are the *authentication id* and the *authorization id*. I +don't know what the difference is, but I have learned that there is probably +a NUL character, which doesn't show up on my screen. + +To see it, I whip out one of my favorite tools: od. This is an old-school +Unix utility, short for "octal dump", because octal numbers were used heavily +back in the day. (Now generally we use hex.) But it does a ton more than that. +In particular, its `-c` option will dump things as characters when it can, and +then show us the hidden things. So now I try again: + +``` +$ base64 -d | od -c +MjAxNzEwMjU1OWYwZWU2M2M1ODhkADIwMTcxMDI1NTlmMGVlNjNjNTg4ZAAwL2VmY2Y5M2U2L2dyOWhDZkc0Q3NmeVU3QlRQeVRDNkJaWVgxQjFVazhE +0000000 2 0 1 7 1 0 2 5 5 9 f 0 e e 6 3 +0000020 c 5 8 8 d \0 2 0 1 7 1 0 2 5 5 9 +0000040 f 0 e e 6 3 c 5 8 8 d \0 0 / e f +0000060 c f 9 3 e 6 / g r 9 h C f G 4 C +0000100 s f y U 7 B T P y T C 6 B Z Y X +0000120 1 B 1 U k 8 D +0000127 +``` + +This is much better. The NUL character shows up as \0 here. So we can see +that the `authcid` is our username. And the `authzid` is too, so I guess we +don't have to understand the difference. And the rest must be the password. +Let's try authenticating with that. + +And no! It doesn't work. But why? Looking at the debugging output from the +script, I try feeding that into od -c as well. And I discover that it is +sending the @ecouser.net as part of the username. That's no good! Maybe +the username's just the first part. + + +SOMETHING ELSE IS FUCKED UP ABOUT THIS LIBRARY. I taught it to do EcoVacs +style auth and it is definitely not working. So there's something else in the +flow that isn't right. But what? diff --git a/sucks.py b/sucks.py index 3f19564..897d110 100644 --- a/sucks.py +++ b/sucks.py @@ -12,6 +12,7 @@ from threading import Event import click import requests +from pycountry_convert import country_alpha2_to_continent_code from sleekxmpp import ClientXMPP, Callback, MatchXPath from sleekxmpp.xmlstream import ET @@ -21,12 +22,12 @@ class EcoVacsAPI: SECRET = "Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GC" PUBLIC_KEY = 'MIIB/TCCAWYCCQDJ7TMYJFzqYDANBgkqhkiG9w0BAQUFADBCMQswCQYDVQQGEwJjbjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMCAXDTE3MDUwOTA1MTkxMFoYDzIxMTcwNDE1MDUxOTEwWjBCMQswCQYDVQQGEwJjbjEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDb8V0OYUGP3Fs63E1gJzJh+7iqeymjFUKJUqSD60nhWReZ+Fg3tZvKKqgNcgl7EGXp1yNifJKUNC/SedFG1IJRh5hBeDMGq0m0RQYDpf9l0umqYURpJ5fmfvH/gjfHe3Eg/NTLm7QEa0a0Il2t3Cyu5jcR4zyK6QEPn1hdIGXB5QIDAQABMA0GCSqGSIb3DQEBBQUAA4GBANhIMT0+IyJa9SU8AEyaWZZmT2KEYrjakuadOvlkn3vFdhpvNpnnXiL+cyWy2oU1Q9MAdCTiOPfXmAQt8zIvP2JC8j6yRTcxJCvBwORDyv/uBtXFxBPEC6MDfzU2gKAaHeeJUWrzRv34qFSaYkYta8canK+PSInylQTjJK9VqmjQ' MAIN_URL_FORMAT = 'https://eco-{country}-api.ecovacs.com/v1/private/{country}/{lang}/{deviceId}/{appCode}/{appVersion}/{channel}/{deviceType}' - USER_URL = 'https://users-na.ecouser.net:8000/user.do' + USER_URL_FORMAT = 'https://users-{continent}.ecouser.net:8000/user.do' REALM = 'ecouser.net' - def __init__(self, device_id, account_id, password_hash): + def __init__(self, device_id, account_id, password_hash, country, continent): self.meta = { - 'country': 'us', + 'country': country, 'lang': 'en', 'deviceId': device_id, 'appCode': 'i_eco_e', @@ -36,6 +37,8 @@ class EcoVacsAPI: } logging.debug("Setting up EcoVacsAPI") self.resource = device_id[0:8] + self.country = country + self.continent = continent login_info = self.__call_main_api('user/login', ('account', self.encrypt(account_id)), ('password', self.encrypt(password_hash))) @@ -83,7 +86,7 @@ class EcoVacsAPI: logging.debug("calling user api {} with {}".format(function, args)) params = {'todo': function} params.update(args) - response = requests.post(EcoVacsAPI.USER_URL, json=params) + response = requests.post(EcoVacsAPI.USER_URL_FORMAT.format(continent=self.continent), json=params) json = response.json() logging.debug("got {}".format(json)) if json['result'] == 'ok': @@ -130,13 +133,14 @@ class EcoVacsAPI: class VacBot(ClientXMPP): - def __init__(self, user, domain, resource, secret, vacuum): + def __init__(self, user, domain, resource, secret, vacuum, continent): ClientXMPP.__init__(self, user + '@' + domain, '0/' + resource + '/' + secret) self.user = user self.domain = domain self.resource = resource self.vacuum = vacuum + self.continent = continent self.credentials['authzid'] = user self.add_event_handler("session_start", self.session_start) @@ -190,7 +194,8 @@ class VacBot(ClientXMPP): c.send() def wrap_command(self, ctl): - q = self.make_iq_query(xmlns=u'com:ctl', ito=self.vacuum['did'] + '@' + self.vacuum['class'] + '.ecorobot.net/atom', + q = self.make_iq_query(xmlns=u'com:ctl', + ito=self.vacuum['did'] + '@' + self.vacuum['class'] + '.ecorobot.net/atom', ifrom=self.user + '@' + self.domain + '/' + self.resource) q['type'] = 'set' for child in q.xml: @@ -199,7 +204,7 @@ class VacBot(ClientXMPP): return q def connect_and_wait_until_ready(self): - self.connect(('47.88.66.164', '5223')) # TODO: change to domain name + self.connect(('msg-{}.ecouser.net'.format(self.continent), '5223')) self.process() self.wait_until_ready() @@ -317,6 +322,17 @@ def write_config(config): fp.write(key + '=' + config[key] + "\n") +def current_country(): + try: + return requests.get('http://ipinfo.io/json').json()['country'].lower() + except: + return 'us' + + +def continent_for_country(country_code): + return country_alpha2_to_continent_code(country_code.upper()).lower() + + def should_run(frequency): if frequency is None: return True @@ -337,7 +353,10 @@ def cli(charge, debug): @cli.command(help='logs in with specified email; run this first') @click.option('--email', prompt='Ecovacs app email') @click.option('--password', prompt='Ecovacs app password', hide_input=True) -def login(email, password): +@click.option('--country-code', prompt='your two-letter country code', default=lambda: current_country()) +@click.option('--continent-code', prompt='your two-letter continent code', + default=lambda: continent_for_country(click.get_current_context().params['country_code'])) +def login(email, password, country_code, continent_code): if config_file_exists() and not click.confirm('overwrite existing config?'): click.echo("Skipping login.") exit(0) @@ -352,6 +371,8 @@ def login(email, password): config['email'] = email config['password_hash'] = password_hash config['device_id'] = device_id + config['country'] = country_code.lower() + config['continent'] = continent_code.lower() write_config(config) click.echo("Config saved.") exit(0) @@ -395,9 +416,10 @@ def run(actions, charge, debug): if actions: config = read_config() - api = EcoVacsAPI(config['device_id'], config['email'], config['password_hash']) + api = EcoVacsAPI(config['device_id'], config['email'], config['password_hash'], + config['country'], config['continent']) vacuum = api.devices()[0] - vacbot = VacBot(api.uid, api.REALM, api.resource, api.user_access_token, vacuum) + vacbot = VacBot(api.uid, api.REALM, api.resource, api.user_access_token, vacuum, config['continent']) vacbot.connect_and_wait_until_ready() for action in actions: diff --git a/test_sucks.py b/test_sucks.py index d2fd128..a8b2e76 100644 --- a/test_sucks.py +++ b/test_sucks.py @@ -6,7 +6,7 @@ from nose.tools import * from sucks import * -# There are no tests for the XMPP stuff here because a) it's relatively complicated to test given +# There are few tests for the XMPP stuff here because a) it's relatively complicated to test given # the library's design and its multithreaded nature, and b) I'm manually testing every change anyhow, # as it's not clear how the robot really behaves. @@ -74,10 +74,13 @@ def test_should_run(): count += 1 assert_almost_equal(count, 9000, delta=200) +def test_continent_for_country(): + assert_equal(continent_for_country('us'), 'na') + assert_equal(continent_for_country('fr'), 'eu') def test_wrap_command(): v = VacBot('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', - {"did": "E0000000001234567890", "class": "126", "nick": "bob"}) + {"did": "E0000000001234567890", "class": "126", "nick": "bob"}, 'na') c = str(v.wrap_command(Clean(1).to_xml())) assert_true(re.search(r'from="20170101abcdefabcdefa@ecouser.net/abcdef12"', c)) assert_true(re.search(r'to="E0000000001234567890@126.ecorobot.net/atom"', c)) @@ -85,10 +88,10 @@ def test_wrap_command(): def test_model_variation(): v = VacBot('20170101abcdefabcdefa', 'ecouser.net', 'abcdef12', 'A1b2C3d4efghijklmNOPQrstuvwxyz12', - {"did": "E0000000001234567890", "class": "141", "nick": "bob"}) + {"did": "E0000000001234567890", "class": "141", "nick": "bob"}, 'na') c = str(v.wrap_command(Clean(1).to_xml())) assert_true(re.search(r'to="E0000000001234567890@141.ecorobot.net/atom"', c)) -b + def test_main_api_called(): with requests_mock.mock() as m: @@ -98,7 +101,7 @@ def test_main_api_called(): text='{"time": 1511200804607, "data": {"authCode": "5c28dac1ff580210e11292df57e87bef"}, "code": "0000", "msg": "X"}') r3 = m.post(re.compile('user.do'), text='{"todo": "result", "token": "jt5O7oDR3gPHdVKCeb8Czx8xw8mDXM6s", "result": "ok", "userId": "2017102559f0ee63c588d", "resource": "f8d99c4d"}') - api = EcoVacsAPI("long_device_id", "account_id", "password_hash") + api = EcoVacsAPI("long_device_id", "account_id", "password_hash", 'us', 'na') assert_equals(r1.call_count, 1) assert_equals(r2.call_count, 1) assert_equals(r3.call_count, 1) @@ -127,4 +130,4 @@ def make_api(): text='{"time": 1511200804607, "data": {"authCode": "abcdef01234567890abcdef012345678"}, "code": "0000", "msg": "X"}') m.post(re.compile('user.do'), text='{"todo": "result", "token": "base64base64base64base64base64ba", "result": "ok", "userId": "20170101abcdefabcdefa", "resource": "abcdef12"}') - return EcoVacsAPI("long_device_id", "account_id", "password_hash") + return EcoVacsAPI("long_device_id", "account_id", "password_hash", 'us', 'na')