pyicloud/base.py

110 lines
3.8 KiB
Python

import time
import uuid
import hashlib
import json
import requests
from exceptions import PyiCloudFailedLoginException
from services import FindMyiPhoneService, CalendarService
class PyiCloudService(object):
"""
A base authentication class for the iCloud service. Handles the
validation and authentication required to access iCloud services.
Usage:
from pyicloud import PyiCloudService
pyicloud = PyiCloudService('username@apple.com', 'password')
pyicloud.iphone.location()
"""
def __init__(self, apple_id, password):
self.discovery = None
self.client_id = str(uuid.uuid1()).upper()
self.user = {'apple_id': apple_id, 'password': password}
self._home_endpoint = 'https://www.icloud.com'
self._setup_endpoint = 'https://p12-setup.icloud.com/setup/ws/1'
self._push_endpoint = 'https://p12-pushws.icloud.com'
self._base_login_url = '%s/login' % self._setup_endpoint
self._base_validate_url = '%s/validate' % self._setup_endpoint
self._base_system_url = '%s/system/version.json' % self._home_endpoint
self._base_webauth_url = '%s/refreshWebAuth' % self._push_endpoint
self.session = requests.Session()
self.session.verify = False
self.session.headers.update({
'host': 'setup.icloud.com',
'origin': self._home_endpoint,
'referer': '%s/' % self._home_endpoint,
'User-Agent': 'Opera/9.52 (X11; Linux i686; U; en)'
})
self.refresh_version()
self.params = {
'clientId': self.client_id,
'clientBuildNumber': self.build_id
}
self.authenticate()
def refresh_version(self):
"""
Retrieves the buildNumber from the /version endpoint.
This is used by almost all request query strings.
"""
req = requests.get(self._base_system_url)
self.build_id = req.json()['buildNumber']
def refresh_validate(self):
"""
Queries the /validate endpoint and fetches two key values we need:
1. "dsInfo" is a nested object which contains the "dsid" integer.
This object doesn't exist until *after* the login has taken place,
the first request will compain about a X-APPLE-WEBAUTH-TOKEN cookie
2. "instance" is an int which is used to build the "id" query string.
This is, pseudo: sha1(email + "instance") to uppercase.
"""
req = self.session.get(self._base_validate_url, params=self.params)
resp = req.json()
if 'dsInfo' in resp:
dsid = resp['dsInfo']['dsid']
self.params.update({'dsid': dsid})
instance = resp['instance']
sha = hashlib.sha1(self.user.get('apple_id') + instance)
self.params.update({'id': sha.hexdigest().upper()})
def authenticate(self):
"""
Handles the full authentication steps, validating,
authenticating and then validating again.
"""
self.refresh_validate()
data = dict(self.user)
data.update({'id': self.params['id'], 'extended_login': False})
req = self.session.post(
self._base_login_url,
params=self.params,
data=json.dumps(data)
)
if not req.ok:
msg = 'Invalid email/password combination.'
raise PyiCloudFailedLoginException(msg)
self.refresh_validate()
self.discovery = req.json()
self.webservices = self.discovery['webservices']
@property
def iphone(self):
service_root = self.webservices['findme']['url']
return FindMyiPhoneService(service_root, self.session, self.params)
@property
def calendar(self):
service_root = self.webservices['calendar']['url']
return CalendarService(service_root, self.session, self.params)