def __init__(self, user=None, key=None): """Initialize our client API instance Keyword Arguments: user -- User ID to authenticate against key -- Corresponding authentication KEY supplied by Reincubate """ self.user = user if user else settings.get('auth', 'user') self.key = key if key else settings.get('auth', 'key') self.auth = (self.user, self.key) # These are only ever stored between 2FA requests to prevent # having to ask the user's details again. self.apple_id = None self.password = None self.session_key = None self.devices = {} # If 2FA is active on this account, then the trusted device list # will be populated once a login is attempted self.trusted_devices = [] # Data from iCloud, populated once logged in and request_data # has been called. self.data = {} self.backup_client = RiCloud._backup_client_class(self)
def test_download_everything(self): """Can we download all the things?""" register_valid_responses() api = ICloudApi(user=settings.get('test', 'user'), key=settings.get('test', 'key')) api.login(apple_id=settings.get('test', 'apple_id'), password=settings.get('test', 'password')) for device_id in api.devices.keys(): data = api.backup_client.request_data(device_id=device_id) assert data is not None # Dump the data to our workspace folder for perusal filename = '%s.json' % api.devices[device_id]['device_name'] with open(os.path.join(WORKSPACE_ROOT, filename), 'wb') as out: json.dump(data, out, indent=4) if len(data['photos']) > 0 and data['photos'][0] != 'Upgrade to view this data.': # We have some photos, let's download them too for photo in data['photos']: filename = photo['filename'] file_id = photo['file_id'] with open(os.path.join(WORKSPACE_ROOT, filename), 'wb') as out: api.backup_client.download_file(device_id=device_id, file_id=file_id, out=out)
def test_ensure_default_credentials_are_set(self, mock_os_path_expanduser): mock_os_path_expanduser.return_value = '' settings = get_config() assert 'your-ricloud-api-access-token-here' == settings.get('auth', 'token') assert 'your-aschannel-stream-name-here' == settings.get('stream', 'stream_endpoint')
def test_ricloud_stream_thread_property(self, mock_Thread, mock_Api, mock_Listener, mock_Stream, stream_endpoints): """ Are we creating the Stream properly? """ ricloud_client = RiCloud() mock_Stream.assert_called_once_with( endpoint=stream_endpoints[0], listener=ricloud_client.listener, stream=settings.get('stream', 'stream_endpoint'), token=settings.get('auth', 'token') ) mock_Stream.return_value.go.assert_called_once_with() assert mock_Thread.daemon mock_Thread.start.assert_called_once_with()
def test_2fa_required(self): """What happens when 2FA is enabled?""" register_2fa_responses() api = ICloudApi(user=settings.get('test', 'user'), key=settings.get('test', 'key')) try: api.login(apple_id=settings.get('test', 'apple_id'), password=settings.get('test', 'password')) raise pytest.skip('2FA is not enabled for this account.') except TwoFactorAuthenticationRequired: pass # The trusted devices fields should now be populated assert len(api.trusted_devices) > 0
def test_ricloud_stream_thread_property(self, mock_Thread, mock_Api, mock_Listener, mock_Stream, stream_endpoints): """ Are we creating the Stream properly? """ ricloud_client = RiCloud() mock_Stream.assert_called_once_with( endpoint=stream_endpoints[0], listener=ricloud_client.listener, stream=settings.get('stream', 'stream_endpoint'), token=settings.get('auth', 'token')) mock_Stream.return_value.go.assert_called_once_with() assert mock_Thread.daemon mock_Thread.start.assert_called_once_with()
def download_file(self, device_id, file_id, out=None): """Download an individual file from the iCloud Backup Arguments: device_id -- Device ID to pull file from file_id -- File ID representing the file we want to download Keyword Arguments: out -- File like object to write response to. If not provided we will write the object to memory. """ if not out: out = StringIO() post_data = { 'key': self.api.session_key, 'device': device_id, 'file': file_id, } response = requests.post(settings.get('endpoints', 'download_file'), auth=self.api.auth, data=post_data, stream=True, headers=self.api.headers) for chunk in response.iter_content(chunk_size=1024): if chunk: out.write(chunk) out.flush() return out
def register_2fa_responses(): responses.add( method=responses.POST, url=settings.get('endpoints', 'login'), body=r"""{ "error": "2fa-required", "message": "This account has Two Factor authentication enabled, please select a device to challenge.", "data": { "trustedDevices": ["********02"], "key": "ae354ef8-b7b6-4a40-843f-96dddff3c64f" } } """, status=409 )
def submit_2fa_challenge(self, code): """Submit a user supplied 2FA challenge code""" data = { 'code': code, 'key': self.session_key, } response = requests.post(settings.get('endpoints', 'submit_2fa'), auth=self.auth, data=data, headers=self.headers) if response.ok: # Retry login return self.login(apple_id=self.apple_id, password=self.password) else: # Unhandled respnose response.raise_for_status()
def login(self, apple_id, password): """Log into the iCloud Keyword Arguments: apple_id -- User's apple ID password -- User's apple password """ data = { "email": apple_id, "password": password, } if self.session_key: data['key'] = self.session_key response = requests.post(settings.get('endpoints', 'login'), auth=self.auth, data=data, headers=self.headers) if response.ok: # We've logged in successfully data = response.json() self.session_key = data['key'] self.devices = data['devices'] # Clear memory cache of apple credentials # These may or may not be set, but better to be on the safe side. self.apple_id = None self.password = None elif response.status_code == 409: data = response.json() error = data['error'] if error == '2fa-required': # 2fa has been activated on this account self.trusted_devices = data['data']['trustedDevices'] self.session_key = data['data']['key'] self.apple_id = apple_id self.password = password raise TwoFactorAuthenticationRequired( 'This user has 2FA enabled, please select a device ' 'and request a challenge.' ) else: # Unhandled response response.raise_for_status()
def request_2fa_challenge(self, challenge_device): """Request a 2FA challenge to the supplied trusted device""" data = { 'challenge': challenge_device, 'key': self.session_key, } response = requests.post(settings.get('endpoints', 'challenge_2fa'), auth=self.auth, data=data, headers=self.headers) if response.ok: # The challenge has been processed, we now need to wait # for the user's submission pass else: # Unhandled respnose response.raise_for_status()
def request_data(self, device_id, data_mask=None, since=None): """Pull data from iCloud from a given point in time. Arguments: device_id -- Device ID to pull data for Keyword Arguments: data_mask -- Bit Mask representing what data to download since -- Datetime to retrieve data from (i.e. SMS received after this point) """ assert self.api.session_key is not None, 'Session key is required, please log in.' if not data_mask: # No mask has been set, so use everything data_mask = 0 for mask, _ in BackupClient.AVAILABLE_DATA: data_mask |= mask if not since: since = BackupClient.MIN_REQUEST_DATE # The start date cannot be below the min request date, may as # well check it now (server will just send an error anyway) assert since >= BackupClient.MIN_REQUEST_DATE post_data = { 'key': self.api.session_key, 'mask': data_mask, 'since': since.strftime('%Y-%m-%d %H:%M:%S.%f'), 'device': device_id, } response = requests.post(settings.get('endpoints', 'download_data'), auth=self.api.auth, data=post_data, headers=self.api.headers) if not response.ok: # Unhandled respnose response.raise_for_status() return response.json()
def register_valid_responses(): print settings.get('endpoints', 'login') responses.add( method=responses.POST, url=settings.get('endpoints', 'login'), body=r""" { "key": "ae354ef8-b7b6-4a40-843f-96dddff3c64f", "devices": { "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa": { "latest-backup": "2014-06-25 22:19:31.000000", "model": "N88AP", "device_name": "John Appleseed's iPhone", "colour": "white", "name": "iPhone 3GS" } } } """ ) responses.add( method=responses.POST, url=settings.get('endpoints', 'download_data'), body=r""" { "call_history": [ { "duration": 0.0, "answered": false, "from_me": false, "date": "2014-10-11 10:16:28.659735", "address": "07123456789" }, { "duration": 48.0, "answered": true, "from_me": false, "date": "2014-10-11 10:40:48.496651", "address": null } ], "contacts": [ { "records": [ { "type": "Phone", "value": "07123 456789" } ], "first_name": "Test", "last_name": "User" } ], "sms": [ { "date": "2014-10-08 00:39:38.000000", "text": "Hi this is a test text", "from_me": false, "number": "+447123456789", "attachments": [] }, { "date": "2014-10-11 09:58:14.000000", "text": "Your WhatsApp code is 416-741 but you can simply tap on this link to verify your device:\n\nv.whatsapp.com/416741", "from_me": false, "number": "99999", "attachments": [] }, { "date": "2014-10-11 10:43:50.000000", "text": "Foor and bar", "from_me": true, "number": "+447123456789", "attachments": [] }, { "date": "2014-10-11 10:52:44.000000", "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur non tortor dolor. Maecenas pretium sapien lorem, nec tristique arcu malesuada eget. Vestibulum at pretium augue. Fusce tempor vehicula cursus. Ut luctus nisi nec neque mattis malesuada. Sed dignissim, lectus maximus fringilla sodales, nunc tortor convallis lectus, vel fermentum augue est et orci. Nulla ut quam dictum, eleifend neque in, dignissim turpis. Duis vel arcu tortor. Nam eget malesuada eros, eu pharetra risus. Nulla facilisi. Aliquam erat volutpat. Nullam porta urna eu lorem consequat, eget tempus turpis sodales. Praesent cursus magna a consectetur venenatis. Phasellus dignissim libero nec purus sodales mattis pellentesque blandit arcu. Quisque neque nisi, viverra et interdum eu, vestibulum non magna. In sed viverra neque. Morbi sollicitudin lacus in elit porta, sollicitudin tincidunt lectus efficitur. Vestibulum venenatis euismod nunc, quis pretium nunc dignissim a. Nam nec dictum libero, vel aliquet risus. Sed aliquet lorem ut libero ultrices dictum.", "from_me": false, "number": "+4471234567890", "attachments": [] } ], "photos": [ { "filename": "IMG_0001.JPG", "file_id": "343e26971dfe9c395c425c0ccf799df63ae6261e" } ], "browser_history": [ { "url": "https://www.google.co.uk/search?q=test&ie=UTF-8&oe=UTF-8&hl=en&client=safari", "last_visit": "2014-10-11 20:00:28.417141", "title": "test - Google Search" }, { "url": "http://m.bbc.co.uk/news/", "last_visit": "2014-10-12 09:40:47.248116", "title": "Home - BBC News" } ], "installed_apps": [ { "name": "Google Maps", "description": "The Google Maps app for iPhone and iPad makes navigating your world faster and easier. Find the best spots in town and the information you need to get there.\n\n\u2022 Comprehensive, accurate maps in 220 countries and territories\n\u2022 Voice-guided GPS navigation for driving, biking, and walking\n\u2022 Transit directions and maps for over 15,000 cities and towns\n\u2022 Live traffic conditions, incident reports, and automatic rerouting to find the best route\n\u2022 Detailed information on more than 100 million places\n\u2022 Street View and indoor imagery for restaurants, museums, and more\n\n* Some features not available in all countries\n* Continued use of GPS running in the background can dramatically decrease battery life.", "advisory-rating": "12+", "author": "Google, Inc." }, { "name": "Spotify Music", "description": "\ufeffSpotify is the best way to listen to music on mobile or tablet. \n\nSearch for any track, artist or album and listen for free. Make and share playlists. Build your biggest, best ever music collection. \n\nGet inspired with personal recommendations, and readymade playlists for just about everything.\n\nListen absolutely free with ads, or get Spotify Premium.\n\nFree on mobile\n\u2022 Play any artist, album, or playlist in shuffle mode.\n\nFree on tablet\n\u2022 Play any song, any time.\n\nPremium features\n\u2022 Play any song, any time on any device: mobile, tablet or computer.\n\u2022 Enjoy ad-free music. \n\u2022 Listen offline. \n\u2022 Get better sound quality.\n\nLove Spotify?\u00a0\nLike us on Facebook: http://www.facebook.com/spotify\u00a0\nFollow us on Twitter: http://twitter.com/spotify", "advisory-rating": "12+", "author": "Spotify Ltd." } ] } """ ) responses.add( method=responses.POST, url=settings.get('endpoints', 'download_file'), body="I am a file..." )
def test_log_in(self): """Can we log in successfull to the API?""" register_valid_responses() api = ICloudApi(user=settings.get('test', 'user'), key=settings.get('test', 'key')) api.login(apple_id=settings.get('test', 'apple_id'), password=settings.get('test', 'password'))