Hopefully adding support for other countries.

This commit is contained in:
William Pietri
2017-12-03 17:43:06 -08:00
parent a66888d05e
commit a3d09ee444
4 changed files with 441 additions and 17 deletions
+2 -1
View File
@@ -21,4 +21,5 @@ requests-mock = ">=1.3"
sleekxmpp = ">=1.3"
click = ">=6"
requests = ">=2.18"
pycryptodome = ">=3.4"
pycryptodome = ">=3.4"
"pycountry-convert" = ">=0.5"
+398
View File
@@ -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/)
<!-- TODO: insert Taken photo -->
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.
<!--TODO: Screnshots of battery saver, etc-->
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. <!-- TODO: insert screnshot 500 -->
That's a good sign! Now I'm in the right place. I set the this a a timestamp
reference. <!-- TODO: insert screnshot 502 --> Now the times are relative
to when the app got going, making it easier for me to keep track of things.
<!-- TODO: insert screnshot 503 --> 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. <!-- TODO: insert screnshot 505 -->
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. <!-- TODO: insert screnshot 508 -->
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.
<!-- TODO: insert screnshot 509 -->
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.
<!-- TODO: format and colorize -->
```
<stream:stream to="ecouser.net" xmlns="jabber:client" xmlns:stream="http://etherx.jabber.org/streams" version="1.0">
<stream:stream xmlns:stream="http://etherx.jabber.org/streams" xmlns="jabber:client" version="1.0" id="a85619c3ea44f395f651faf1a5996ee6" from="ecouser.net">
<stream:features>
<auth xmlns="http://jabber.org/features/iq-auth"/>
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls">
<required/>
</starttls>
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
</mechanisms>
</stream:features>
<starttls xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
<proceed xmlns="urn:ietf:params:xml:ns:xmpp-tls"/>
```
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:
<!-- Insert screenshot 510 -->
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] }}}<stream:features><auth xmlns="http://jabber.org/features/iq-auth"/><mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl"><mechanism>PLAIN</mechanism></mechanisms></stream:features>{{{
[2017-11-15 14:49:48.255088-07:00] [(C2S) 192.168.1.227:48607 -> 47.88.66.164:5223] }}}<auth mechanism="PLAIN" xmlns="urn:ietf:params:xml:ns:xmpp-sasl">MjAxNzEwMjU1OWYwZWU2M2M1ODhkADIwMTcxMDI1NTlmMGVlNjNjNTg4ZAAwL2VmY2Y5M2U2L2dyOWhDZkc0Q3NmeVU3QlRQeVRDNkJaWVgxQjFVazhE</auth>{{{
[2017-11-15 14:49:48.444622-07:00] [(S2C) 47.88.66.164:5223 -> 192.168.1.227:48607] }}}<success xmlns="urn:ietf:params:xml:ns:xmpp-sasl"/>{{{
```
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 `<ctl td="Clean"><clean type="auto" speed="standard"/></ctl>`
when I hit the auto-clean button in the app, and `<ctl td="Clean"><clean type="stop" speed="standard"/></ctl>`
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?
+32 -10
View File
@@ -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:
+9 -6
View File
@@ -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')