local http = require "http" local json = require "json" local stdnse = require "stdnse" local table = require "table" _ENV = stdnse.module("mobileme", stdnse.seeall) --- -- A MobileMe web service client that allows discovering Apple devices -- using the "find my iPhone" functionality. -- -- @author Patrik Karlsson -- MobileMe = { -- headers used in all requests headers = { ["Content-Type"] = "application/json; charset=utf-8", ["X-Apple-Find-Api-Ver"] = "2.0", ["X-Apple-Authscheme"] = "UserIdGuest", ["X-Apple-Realm-Support"] = "1.0", ["User-Agent"] = "Find iPhone/1.3 MeKit (iPad: iPhone OS/4.2.1)", ["X-Client-Name"] = "iPad", ["X-Client-UUID"] = "0cf3dc501ff812adb0b202baed4f37274b210853", ["Accept-Language"] = "en-us", ["Connection"] = "keep-alive" }, -- Creates a MobileMe instance -- @param username string containing the Apple ID username -- @param password string containing the Apple ID password -- @return o new instance of MobileMe new = function(self, username, password) local o = { host = "fmipmobile.icloud.com", port = 443, username = username, password = password } setmetatable(o, self) self.__index = self return o end, -- Sends a message to an iOS device -- @param devid string containing the device id to which the message should -- be sent -- @param subject string containing the message subject -- @param message string containing the message body -- @param alarm boolean true if alarm should be sounded, false if not -- @return status true on success, false on failure -- @return err string containing the error message (if status is false) sendMessage = function(self, devid, subject, message, alarm) local data = '{"clientContext":{"appName":"FindMyiPhone","appVersion":\z "1.3","buildVersion":"145","deviceUDID":\z "0000000000000000000000000000000000000000","inactiveTime":5911,\z "osVersion":"3.2","productType":"iPad1,1","selectedDevice":"%s",\z "shouldLocate":false},"device":"%s","serverContext":{\z "callbackIntervalInMS":3000,"clientId":\z "0000000000000000000000000000000000000000","deviceLoadStatus":"203",\z "hasDevices":true,"lastSessionExtensionTime":null,"maxDeviceLoadTime":\z 60000,"maxLocatingTime":90000,"preferredLanguage":"en","prefsUpdateTime":\z 1276872996660,"sessionLifespan":900000,"timezone":{"currentOffset":\z -25200000,"previousOffset":-28800000,"previousTransition":1268560799999,\z "tzCurrentName":"Pacific Daylight Time","tzName":"America/Los_Angeles"},\z "validRegion":true},"sound":%s,"subject":"%s","text":"%s"}' data = data:format(devid, devid, tostring(alarm), subject, message) local url = ("/fmipservice/device/%s/sendMessage"):format(self.username) local auth = { username = self.username, password = self.password } local response = http.post(self.host, self.port, url, { header = self.headers, auth = auth, timeout = 10000 }, nil, data) if ( response.status == 200 ) then local status, resp = json.parse(response.body) if ( not(status) ) then stdnse.debug2("Failed to parse JSON response from server") return false, "Failed to parse JSON response from server" end if ( resp.statusCode ~= "200" ) then stdnse.debug2("Failed to send message to server") return false, "Failed to send message to server" end end return true end, -- Updates location information for all devices controlled by the Apple ID -- @return status true on success, false on failure -- @return json parsed json table or string containing an error message on -- failure update = function(self) local auth = { username = self.username, password = self.password } local url = ("/fmipservice/device/%s/initClient"):format(self.username) local data= '{"clientContext":{"appName":"FindMyiPhone","appVersion":\z "1.3","buildVersion":"145","deviceUDID":\z "0000000000000000000000000000000000000000","inactiveTime":2147483647,\z "osVersion":"4.2.1","personID":0,"productType":"iPad1,1"}}' local retries = 2 local response repeat response = http.post(self.host, self.port, url, { header = self.headers, auth = auth }, nil, data) if ( response.header["x-apple-mme-host"] ) then self.host = response.header["x-apple-mme-host"] end if ( response.status == 401 ) then return false, "Authentication failed" elseif ( response.status ~= 200 and response.status ~= 330 ) then return false, "An unexpected error occurred" end retries = retries - 1 until ( 200 == response.status or 0 == retries) if ( response.status ~= 200 ) then return false, "Received unexpected response from server" end local status, parsed_json = json.parse(response.body) if ( not(status) or parsed_json.statusCode ~= "200" ) then return false, "Failed to parse JSON response from server" end -- cache the parsed_json.content as devices self.devices = parsed_json.content return true, parsed_json end, -- Gets a list of devices -- @return devices table containing a list of devices getDevices = function(self) if ( not(self.devices) ) then self:update() end return self.devices end } Helper = { -- Creates a Helper instance -- @param username string containing the Apple ID username -- @param password string containing the Apple ID password -- @return o new instance of Helper new = function(self, username, password) local o = { mm = MobileMe:new(username, password) } setmetatable(o, self) self.__index = self o.mm:update() return o end, -- Gets the geolocation from each device -- -- @return status true on success, false on failure -- @return result table containing a table of device locations -- the table is indexed based on the name of the device and -- contains a location table with the following fields: -- * longitude - the GPS longitude -- * latitude - the GPS latitude -- * accuracy - the location accuracy -- * timestamp - the time the location was acquired -- * postype - the position type (GPS or WiFi) -- * finished - -- or string containing an error message on failure getLocation = function(self) -- do 3 tries, with a 5 second timeout to allow the location to update -- there are two attributes, locationFinished and isLocating that seem -- to be good candidates to monitor, but so far, I haven't had any -- success with that. local tries, timeout = 3, 5 local result = {} repeat local status, response = self.mm:update() if ( not(status) or not(response) ) then return false, "Failed to retrieve response from server" end for _, device in ipairs(response.content) do if ( device.location ) then result[device.name] = { longitude = device.location.longitude, latitude = device.location.latitude, accuracy = device.location.horizontalAccuracy, timestamp = device.location.timeStamp, postype = device.location.positionType, finished = device.location.locationFinished, } end end tries = tries - 1 if ( tries > 0 ) then stdnse.sleep(timeout) end until( tries == 0 ) return true, result end, -- Gets a list of names and ids of devices associated with the Apple ID -- @return status true on success, false on failure -- @return table of devices containing the following fields: -- name and id getDevices = function(self) local devices = {} for _, dev in ipairs(self.mm:getDevices()) do table.insert(devices, { name = dev.name, id = dev.id }) end return true, devices end, -- Send a message to an iOS Device -- -- @param devid string containing the device id to which the message should -- be sent -- @param subject string containing the message subject -- @param message string containing the message body -- @param alarm boolean true if alarm should be sounded, false if not -- @return status true on success, false on failure -- @return err string containing the error message (if status is false) sendMessage = function(self, ...) return self.mm:sendMessage(...) end } return _ENV;