tylerhall / sosumi

A MobileMe web scraper that exposes Apple's Find My iPhone service to the command line. This allows you to programmatically retrieve your phone's current location and push messages (and an optional alarm) to the remote device.

This URL has Read+Write access

tylerhall (author)
Fri Oct 16 13:15:30 -0700 2009
commit  30d89c8577d55aed9c869e0e1839f0501d2ae177
tree    f5558eb2c805744ca7a3a1a68985f7b0a4c35e15
parent  22577fc9891432e134129568a2340b903d71a07c
sosumi / class.sosumi.php
100644 241 lines (200 sloc) 10.955 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
<?PHP
 
    // Sosumi - a Find My iPhone web scraper.
    //
    // June 22, 2009
    // Tyler Hall <tylerhall@gmail.com>
    // http://github.com/tylerhall/sosumi/tree/master
    //
    // Usage:
    // $ssm = new Sosumi('username', 'password');
    // $location_info = $ssm->locate();
    // $ssm->sendMessage('Daisy, daisy...');
    //
    // TODO: Need to see how many HTTP requests we can remove. The current
    // implementation hasn't been minified yet.
 
    class Sosumi
    {
        public $devices; // An array of all devices on this MobileMe account
        private $lastURL; // The previous URL as visited by curl
        private $tmpFile; // Where we store our cookies
        private $lsc; // Associative array of Apple auth tokens
        private $deviceId; // The device ID to ping
 
        public function __construct($mobile_me_username, $mobile_me_password)
        {
            $this->tmpFile = tempnam('/tmp', 'sosumi');
            $this->lsc = array();
            $this->devices = array();
 
            // Load the HTML login page and also get the init cookies set
            $html = $this->curlGet("https://auth.me.com/authenticate?service=account&ssoNamespace=primary-me&reauthorize=Y&returnURL=aHR0cHM6Ly9zZWN1cmUubWUuY29tL2FjY291bnQvI2ZpbmRteWlwaG9uZQ==&anchor=findmyiphone");
 
            // Parse out the hidden fields
            preg_match_all('!hidden.*?name=["\'](.*?)["\'].*?value=["\'](.*?)["\']!ms', $html, $hidden);
 
            // Build the form post data
            $post = '';
            for($i = 0; $i < count($hidden[1]); $i++)
                $post .= $hidden[1][$i] . '=' . urlencode($hidden[2][$i]) . '&';
            $post .= 'username=' . urlencode($mobile_me_username) . '&password=' . urlencode($mobile_me_password);
 
            // Login
            $action_url = $this->match('!action=["\'](.*?)["\']!ms', $html, 1);
            $html = $this->curlPost('https://auth.me.com/authenticate', $post, $this->lastURL);
            $html = $this->curlGet('https://secure.me.com/account/', $this->lastURL);
 
            $headers = array('X-Mobileme-Version: 1.0');
            $html = $this->curlGet('https://secure.me.com/wo/WebObjects/Account2.woa?lang=en&anchor=findmyiphone', $this->lastURL, $headers);
 
            $this->getDevices();
        }
 
        public function __destruct()
        {
            if(file_exists($this->tmpFile))
                unlink($this->tmpFile);
        }
 
        // Returns a stdClass object of location information. Example...
        // stdClass Object
        // (
        // [isLocationAvailable] => 1
        // [longitude] => -121.010392
        // [accuracy] => 47.421634
        // [time] => 9:24 PM
        // [isOldLocationResult] => 1
        // [isRecent] => 1
        // [statusString] => locate status available
        // [status] => 1
        // [isLocateFinished] =>
        // [latitude] => 38.319117
        // [date] => June 22, 2009
        // [isAccurate] =>
        // )
        public function locate($the_device = null)
        {
            // Grab the first device is none is specified
            if(is_null($the_device))
            {
                reset($this->devices);
                $the_device = current($this->devices);
            }
 
            $arr = array('deviceId' => $the_device['deviceId'], 'deviceOsVersion' => $the_device['deviceOsVersion']);
 
            $post = 'postBody=' . json_encode($arr);
 
            $headers = array('Accept: text/javascript, text/html, application/xml, text/xml, */*',
                             'X-Requested-With: XMLHttpRequest',
                             'X-Prototype-Version: 1.6.0.3',
                             'Content-Type: application/json; charset=UTF-8',
                             'X-Mobileme-Version: 1.0',
                             'X-Mobileme-Isc: ' . $this->lsc['secure.me.com']);
            $html = $this->curlPost('https://secure.me.com/wo/WebObjects/DeviceMgmt.woa/wa/LocateAction/locateStatus', $post, 'https://secure.me.com/account/', $headers);
            $json = json_decode(array_pop(explode("\n", $html)));
            return $json;
        }
 
        // Send a message to the device with an optional alarm sound
        public function sendMessage($msg, $alarm = false, $the_device = null)
        {
            // Grab the first device is none is specified
            if(is_null($the_device))
            {
                reset($this->devices);
                $the_device = current($this->devices);
            }
 
            $arr = array('deviceId' => $the_device['deviceId'],
                         'message' => $msg,
                         'playAlarm' => $alarm ? 'Y' : 'N',
                         'deviceType' => $the_device['deviceType'],
                         'deviceClass' => $the_device['deviceClass'],
                         'deviceOsVersion' => $the_device['deviceOsVersion']);
 
            $post = 'postBody=' . json_encode($arr);
 
            $headers = array('Accept: text/javascript, text/html, application/xml, text/xml, */*',
                             'X-Requested-With: XMLHttpRequest',
                             'X-Prototype-Version: 1.6.0.3',
                             'Content-Type: application/json; charset=UTF-8',
                             'X-Mobileme-Version: 1.0',
                             'X-Mobileme-Isc: ' . $this->lsc['secure.me.com']);
 
            $html = $this->curlPost('https://secure.me.com/wo/WebObjects/DeviceMgmt.woa/wa/SendMessageAction/sendMessage', $post, 'https://secure.me.com/account/', $headers);
 
            $json = json_decode(array_pop(explode("\n", $html)));
            return ($json !== false) && isset($json->statusString) && ($json->statusString == 'message sent');
        }
 
        public function remoteWipe()
        {
            // Remotely wiping a device is an exercise best
            // left to the reader.
        }
 
        // Grab the details for each device on the MobileMe account
        // (We could also use this opportunity to parse out the last know lat/lng of the device
        // and save a couple round trips in the future.)
        private function getDevices()
        {
            $headers = array('Accept: text/javascript, text/html, application/xml, text/xml, */*',
                             'X-Requested-With: XMLHttpRequest',
                             'X-Prototype-Version: 1.6.0.3',
                             'X-Mobileme-Version: 1.0',
                             'X-Mobileme-Isc: ' . $this->lsc['secure.me.com']);
            $html = $this->curlPost('https://secure.me.com/device_mgmt/en', null, 'https://secure.me.com/account/', $headers);
 
            $headers = array('Accept: text/javascript, text/html, application/xml, text/xml, */*',
                             'X-Requested-With: XMLHttpRequest',
                             'X-Prototype-Version: 1.6.0.3',
                             'X-Mobileme-Version: 1.0',
                             'X-Mobileme-Isc: ' . $this->lsc['secure.me.com']);
            $html = $this->curlPost('https://secure.me.com/wo/WebObjects/DeviceMgmt.woa/?lang=en', null, 'https://secure.me.com/account/', $headers);
 
            // Grab all of the devices
            preg_match_all('/new Device\((.*?)\)/ms', $html, $matches);
            for($i = 0; $i < count($matches[0]); $i++)
            {
                $values = str_replace("'", '', $matches[1][$i]);
                list($unknown, $id, $type, $class, $os) = explode(',', $values);
                $this->devices[$id] = array('deviceId' => $id, 'deviceType' => $type, 'deviceClass' => $class, 'deviceOsVersion' => $os);
            }
        }
 
        private function curlGet($url, $referer = null, $headers = null)
        {
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_COOKIEFILE, $this->tmpFile);
            curl_setopt($ch, CURLOPT_COOKIEJAR, $this->tmpFile);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_AUTOREFERER, true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-us) AppleWebKit/531.9 (KHTML, like Gecko) Version/4.0.3 Safari/531.9");
            if(!is_null($referer)) curl_setopt($ch, CURLOPT_REFERER, $referer);
            if(!is_null($headers)) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 
            curl_setopt($ch, CURLOPT_HEADER, true);
            // curl_setopt($ch, CURLOPT_VERBOSE, true);
 
            $html = curl_exec($ch);
 
            if(curl_errno($ch) != 0)
            {
                throw new Exception("Error during GET of '$url': " . curl_error($ch));
            }
 
            $this->lastURL = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
 
            preg_match_all('/[li]sc-(.*?)=([a-f0-9]+);/i', $html, $matches);
            for($i = 0; $i < count($matches[0]); $i++)
                $this->lsc[$matches[1][$i]] = $matches[2][$i];
 
            return $html;
        }
 
        private function curlPost($url, $post_vars = null, $referer = null, $headers = null)
        {
            if(is_null($post_vars))
                $post_vars = '';
 
            $ch = curl_init($url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_COOKIEFILE, $this->tmpFile);
            curl_setopt($ch, CURLOPT_COOKIEJAR, $this->tmpFile);
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
            curl_setopt($ch, CURLOPT_AUTOREFERER, true);
            curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
            curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10_6_1; en-us) AppleWebKit/531.9 (KHTML, like Gecko) Version/4.0.3 Safari/531.9");
            if(!is_null($referer)) curl_setopt($ch, CURLOPT_REFERER, $referer);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, $post_vars);
            if(!is_null($headers)) curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
 
            curl_setopt($ch, CURLOPT_HEADER, true);
            // curl_setopt($ch, CURLOPT_VERBOSE, true);
 
            $html = curl_exec($ch);
 
            if(curl_errno($ch) != 0)
            {
                throw new Exception("Error during POST of '$url': " . curl_error($ch));
            }
 
            $this->lastURL = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
 
            preg_match_all('/[li]sc-(.*?)=([a-f0-9]+);/i', $html, $matches);
            for($i = 0; $i < count($matches[0]); $i++)
                $this->lsc[$matches[1][$i]] = $matches[2][$i];
 
            return $html;
        }
 
        private function match($regex, $str, $i = 0)
        {
            return preg_match($regex, $str, $match) == 1 ? $match[$i] : false;
        }
    }