News | Profile | Code | Photography | Looking Glass | Projects | System Statistics | Uncategorized |
Blog |
I came across the Withings Wi-Fi Scale on ThinkGeek awhile back, but only recently decided to purchase it. Over the past few months, I've (on & off, I might add) been keeping a record of my mass each morning, using a piece of paper and pen, then transferring it to digital form when it seemed necessary. I figured a change was needed.
The Withings scale was easy to setup, but only afterwards did I realize I'd be forced to use a crappy Flash-based web portal (or iOS app.) to view reports and graphs on my mass, BMI, and body fat. However, I was able to hack the communication between the scale and the web portal, and actually keep track of the data myself.
I'll try to describe the process I took, without getting into too much detail.
Let's start from the beginning. I received the Withings scale in a rather huge cardboard box. I'm not sure what ThinkGeek was smoking, but the size of the scale (in its box) was fairly tiny compared to the person-sized cardboard shipping container it arrived in. I opened the box to find no manuals, just a USB cable, batteries, the scale, and a single sheet of paper that directed me to the Withings' web site to sign up and setup my scale.
This is where I started worrying that I might have to use a web portal to view things, but I kept going anyway. The site instructed me to plug in my scale via USB, and then download an installer executable. Strangely enough there was one for Linux, so I grabbed that one, and used it to setup the Wi-Fi connectivity. It also did a firmware upgrade, supposedly.
I logged into the web portal, and.. yep, Iceweasel crashed instantly. Honestly, that's rare for me, so I had to use Windows 7 in the meantime (turns out it was time for me to ditch the amd64 build of the Adobe Flash 10.0 plugin, and upgrade to 10.1 w/nspluginwrapper). After filling out my name, nickname, current mass, height, age, and sex, I tried out the scale. The LCD display lit up as I stood on it. After a few seconds of repositioning myself with the aid of the arrows that lit up on the screen, it displayed my mass and then BMI.
The web portal accurately reflected these values, with the exception of body fat, which I figured would show up after a few more data points.
The web portal was clunky, and generally difficult to use. To make matters worse, it was horribly slow. Sometimes it would take 10-20 seconds for the points to display on the graph.
Irritated by the sluggishness of the web portal, I did a trace to my.withings.net, and a whois on the associated IPs (88.191.98.77 and 88.191.97.104):
[...] 4 paix-ny.proxad.net (198.32.118.197) 0.576 ms 5 londres-6k-1-po103.intf.routers.proxad.net (212.27.58.205) 81.819 ms 6 bzn-crs16-1-be1102.intf.routers.proxad.net (212.27.51.185) 82.700 ms 7 dedibox-2-p.intf.routers.proxad.net (212.27.50.162) 82.367 ms 8 88.191.2.57 (88.191.2.57) 82.428 ms 9 s1.withings.net (88.191.98.77) 82.162 ms inetnum: 88.191.3.0 - 88.191.248.255 netname: FR-DEDIBOX descr: Dedibox SAS descr: Customers descr: Paris, France descr: NCC#2007023902 remarks: trouble: Information: http://www.dedibox.fr/ remarks: trouble: Spam/Abuse requests: http://www.dedibox.fr/abuse/ remarks: trouble: Spam/Abuse requests: mailto:abuse@support.dedibox.fr country: FR admin-c: ACP23-RIPE tech-c: TCP8-RIPE status: ASSIGNED PA mnt-by: PROXAD-MNT source: RIPE # Filtered
Well, that's why it's slow. It's in Paris, in addition to the Flash content. It's also hosted at a dedicated server provider, Dedibox (now online.net?), which is well-known for cheap hosting solutions (that usually means it's crawling with botnet controllers and warez).
I then took a tcpdump of the traffic from the scale, and found that it was talking to another destination in that same network range, scalews.withings.net. [88.191.224.77]. In fact, the communication wasn't encrypted and fairly basic. After following the TCP stream with Wireshark, I figured out the basic order of operations:
For your viewing pleasure, the entire conversation can be found here. As with the rest of the stuff I'm posting here, don't think I've kept all the values intact.. I'm not an idiot!
The first thing that jumped out at me was that the server runs Ubuntu, and spills all of its version information. Apache 2.2.8, PHP 5.2.4-2ubuntu5.10. Jeez, 5.10 uh.. "Breezy Badger" is back from 2005! I'd say they need to upgrade.
The 4x HTTP POST requests are pretty simple:
POST /cgi-bin/once: Sending a simple "action=get" apparently just asks the server for a random ID or cookie value. It returns back this value along with a status (0, which apparently is equivalent to "no error"). I don't think the scale cares if it gets the same one every time.
POST /cgi-bin/session: The scale then sends an "action=new" along with the MAC address of the scale, an MD5 hash, firmware version, battery level, and two other values that I haven't been able to figure out (duration & zreboot). The server then responds with a larger JSON-encoded string:
{"status":0,"body":{"sessionid":"546-4c818c8e-3b918091","sp":{"users":[{"id":101010,"sn":"PRX","wt":66.3,"re":565,"ri":2337,"ht":1.7,"agt":30.2,"sx":0,"fm":1,"cr":1283558096,"att":0}]},"ind":{"lg":"en_GB","imt":1,"stp":0,"f":0,"g":97973},"syp":{"utc":1283558546},"ctp":{"goff":-14400,"dst":1289109600,"ngoff":-18000}}}
Let's go what I've been able to figure out of the values, here:
POST /cgi-bin/measure: This is the good stuff. The scale then POSTs a whole bunch of data that we care about:
action=store&sessionid=546-4c818c8e-3b918091&macaddress=00:24:e4:ff:fc:01&userid=101010&meastime=1283558512&devtype=1&attribstatus=0&measures=%7B%22measures%22%3A%5B%7B%22value%22%3A66850%2C%22type%22%3A1%2C%22unit%22%3A%2D3%7D%5D%7D
The real JSON of the values looks like this:
{"measures":[{"value":68950,"type":1,"unit":-3},{"value":583,"type":2,"unit":0},{"value":2292,"type":3,"unit":0}]}
Apparently, though, not all measurements may be given. I think sometimes the scale can't determine bioelectrical impedance (the re & ri values), if the user is wearing shoes (duh).
POST /cgi-bin/session: The scale just deletes the session it had, here.
So, certainly hackable!
So, my first thought was getting the scale to talk to one of my webservers instead of scalews.withings.net. Easy enough! A few lines in the named.conf of my local DNS cache and a small zone file did the trick:
// Withings zone "scalews.withings.net" { type master; file "/etc/bind/prolixium/scalews.withings.net"; };
Zone file:
$ORIGIN . $TTL 3600 ; 1 hour scalews.withings.net IN SOA atlantis.prolixium.com. hostmaster.prolixium.com. ( 2010091101 ; serial 7200 ; refresh (2 hours) 1800 ; retry (30 minutes) 1209600 ; expire (2 weeks) 3600 ; minimum (1 hour) ) NS atlantis.prolixium.com. A 10.3.4.6
atlantis is my local DNS server and 10.3.4.6 is the webserver (dax) I want scalews.withings.net to resolve to.
destiny% host scalews.withings.net. scalews.withings.net has address 10.3.4.6
Good enough! I then wrote a Python script to respond to each of the queries, store the bioelectrical impedance, mass, timestamps, MAC address, and battery levels in a MySQL database. It took a little bit to get everything working right (mostly fighting with Python modules), but I eventually got it returning responses that made the scale happy.
Rather than creating a bunch of separate Python scripts, I just made symlinks to the various URLs that receive POST requests:
lrwxr-xr-x 1 root wheel 11 2010-09-11 17:19 measure -> withings.py lrwxr-xr-x 1 root wheel 11 2010-09-11 17:19 once -> withings.py lrwxr-xr-x 1 root wheel 11 2010-09-11 17:19 session -> withings.py -rwxr-xr-x 1 root wheel 3983 2010-09-11 22:02 withings.py
The MySQL schema looks like this:
CREATE TABLE `measure` ( `sourceport` int(11) DEFAULT NULL, `sourceip` varchar(15) DEFAULT NULL, `mac` varchar(20) DEFAULT NULL, `battery` int(11) DEFAULT NULL, `firmware` int(11) DEFAULT NULL, `id` int(11) NOT NULL AUTO_INCREMENT, `recvtime` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1; CREATE TABLE `value` ( `value` int(11) DEFAULT NULL, `unit` int(11) DEFAULT NULL, `type` int(11) DEFAULT NULL, `id` int(11) NOT NULL AUTO_INCREMENT, `measid` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `measid` (`measid`) ) ENGINE=MyISAM DEFAULT CHARSET=latin1
I tested it for a bit, and realized that sometimes the scale won't send the measurements to the server after every step on it. I'm not sure why, maybe it's a bug - it'll queue it up and then send two POSTs to /cgi-bin/measure on the next connect. Also, it seems very inconsistent on whether it'll actually report the bioelectrical impedance, or not. Even barefooted, it seems hit or miss.
Also, I think that my script isn't perfect, or it's sending some wrong values to the scale, because sometimes my BMI is 99.9, which means I'm as big as a house, or something:
Also, because I have this love affair with MRTG, I wrote another Python script to provide MRTG with a snapshot of the last reported values (mass, battery, re, ei) to store in a couple RRDs. Here's the graph for mass (via drraw):
You can grab all of my scripts from here. To use them, make sure you change the variables (MySQL credentials, user profile, etc.) and the MD5 hash value and cookie returned from the "once" script. They may be related, and the values in the scripts are just examples, they will not work most likely. This means you'll have to do a packet capture yourself. Sorry.
The scale itself takes 4x AAA batteries, and eats them like crazy! It came with 4x off-brand batteries that were depleted after the initial setup and about 10-12 measures. I replaced them with some Energizer "advanced" lithium batteries, and it seems to now only decrease by 2-4% for every measure. Still quite a hog, considering it's only on for a few seconds during and after the measurements.
I wish the top of the scale wasn't a mirror. It feels like glass, and is very reflective, so any footprints mess with my OCD.
I'm a little worried that this thing is going to do an automatic firmware upgrade on its own, and break the scripts. Watch out for it! Maybe applying a firewall rule to the scale would be a good idea to prevent such things from happening.
I created a live dashboard of the data I've been graphing. Yeah, my mass seems to fluctuate quite a bit…
This is pure genius! Really, lots of good ideas in here!
Thankx. Usefull information. I have redirected scalews.withings.net and s5.withings.net to a local server, which receives the initial POST. But, after sending the "once" the scale doesn't answer with the further POST's.
Any idea?
greez
Hi Rapsoelfabrik. I had to sniff the first couple connections to the real scalews.withings.net before being able to correctly respond to the POST to /cgi-bin/once. I then replayed the ID or cookie value that's sent from the real server. I don't think just any value will work. The values shown in my blog entry are only samples, they are slightly modified from what I saw on the wire, and probably won't work if copied verbatim.
The problem were the correct HTTP-Headers. As I use IIS, that sends its own Microsoft IIS 7 specific header values, the scale refuses any following POSTs. I had explicitly to set the "Ubuntu" Values in the HTTP-Headers. The Scale obviously checks the Headers. Cheap trick :)
Rapsoelfabrik, wow.. that is indeed a cheap way of doing validation. I guess I didn't run into it because I replayed the "server" header along with the other values, without even thinking! I guess I got lucky, thanks for the info.!
Oh dear. I was completely wrong. The Server name in the header doesn't matter at all. "transfer-encoding" needs to be "chunked", so the byte counts is important. With IIS just always put a Response.Flush() at the end of your "once" and "session" and IIS will automatically add the correct transfer-encoding and length info.
Hello, I'm doing this experiment, but I'm new to python script. Could anyone tell me or share the python scripts which respond to each of the queries. Thanks a lot.
Oh.. how blind I am, I found the author's python script at the end of the post. Anyway, I tried PHP to get the POST and write it to a file using "print_r($_POST)", but it got nothing. Does anyone have a clue?
We got it in PHP. Here is my steps. Assuming your DNS (Bind9) and webserver (Apache2) is working.
1. Create "cgi-bin" folder in /var/www/
2. Comment out original "cgi-bin" configuration in "/etc/apache2/sites-enabled/000-default", like #ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
3. In "cgi-bin" folder, crate .htaccess, withings.php and 3 softlinks as follow:
-------.htaccess--------
Options +FollowSymLinks
RewriteEngine on
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
RewriteRule [b]^/([/b]([^/]+/)*[^.]+)$ /$1.php [L]
-------- withings.php -----
<?php
print '{"status":0,"body":{"once":"21112cb3-1b433eef"}}';
$myFile = "output.txt";
$fh = fopen($myFile, 'a') or die("can't open file");
foreach ( $_POST as $key => $value ) {
fwrite($fh, $key . " " . "=" . " " . $value . "\n");
}
fclose($fh);
-------- 3 links -------
measure.php -> withings.php
once.php -> withings.php
session.php -> withings.php
If succeed, you'll get it in output.txt
We got it in PHP. Here is my steps. Assuming your DNS (Bind9) and webserver (Apache2) is working.
1. Create "cgi-bin" folder in /var/www/
2. Comment out original "cgi-bin" configuration in "/etc/apache2/sites-enabled/000-default", like #ScriptAlias /cgi-bin/ /usr/lib/cgi-bin/
3. In "cgi-bin" folder, crate .htaccess, withings.php and 3 softlinks as follow:
-------.htaccess--------
Options +FollowSymLinks
RewriteEngine on
RewriteCond %{DOCUMENT_ROOT}/$1.php -f
RewriteRule [b]^/([/b]([^/]+/)*[^.]+)$ /$1.php [L]
-------- withings.php -----
<?php
print '{"status":0,"body":{"once":"21112cb3-1b433eef"}}';
$myFile = "output.txt";
$fh = fopen($myFile, 'a') or die("can't open file");
foreach ( $_POST as $key => $value ) {
fwrite($fh, $key . " " . "=" . " " . $value . "\n");
}
fclose($fh);
-------- 3 links -------
measure.php -> withings.php
once.php -> withings.php
session.php -> withings.php
If succeed, you'll get it in output.txt
New comments are currently disabled for this entry.
This HTML for this page was generated in 0.002 seconds. |
Very great post. I will definitely buy this product ;)