From b3aee79dcb21f3ac6379cf447f1f2ca68a244c06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Thu, 29 Oct 2020 09:26:12 +0000 Subject: [PATCH 01/15] Added support for 2FA --- pyicloud/base.py | 281 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 237 insertions(+), 44 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 510bc2f..3f907d9 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -63,7 +63,12 @@ class PyiCloudSession(Session): if self.service.password_filter not in request_logger.filters: request_logger.addFilter(self.service.password_filter) - request_logger.debug("%s %s %s", method, url, kwargs.get("data", "")) + request_logger.debug( + "%s %s %s", + method, + url, + kwargs.get("data", ""), + ) has_retried = kwargs.get("retried") kwargs.pop("retried", None) @@ -72,6 +77,40 @@ class PyiCloudSession(Session): content_type = response.headers.get("Content-Type", "").split(";")[0] json_mimetypes = ["application/json", "text/json"] + if response.headers.get("X-Apple-ID-Session-Id"): + self.service.session_data["session_id"] = response.headers.get( + "X-Apple-ID-Session-Id" + ) + + if response.headers.get("X-Apple-Session-Token"): + self.service.session_data["session_token"] = response.headers.get( + "X-Apple-Session-Token" + ) + + if response.headers.get("X-Apple-ID-Account-Country"): + self.service.session_data["account_country"] = response.headers.get( + "X-Apple-ID-Account-Country" + ) + + if response.headers.get("scnt"): + self.service.session_data["scnt"] = response.headers.get("scnt") + + if response.headers.get("X-Apple-TwoSV-Trust-Token"): + self.service.session_data["trust_token"] = response.headers.get( + "X-Apple-TwoSV-Trust-Token" + ) + + # Save session_data to file + with open(self.service._get_sessiondata_path(), "w") as outfile: + json.dump(self.service.session_data, outfile) + LOGGER.debug("Saved session data to file") + + # Save cookies to file + if not path.exists(self.service._cookie_directory): + mkdir(self.service._cookie_directory) + self.cookies.save(ignore_discard=True, ignore_expires=True) + LOGGER.debug("Cookies saved to %s", self.service._get_cookiejar_path()) + if not response.ok and content_type not in json_mimetypes: if has_retried is None and response.status_code == 450: api_error = PyiCloudAPIResponseException( @@ -106,8 +145,8 @@ class PyiCloudSession(Session): if not code and data.get("serverErrorCode"): code = data.get("serverErrorCode") - if reason: - self._raise_error(code, reason) + if reason: + self._raise_error(code, reason) return response @@ -148,6 +187,7 @@ class PyiCloudService(object): pyicloud.iphone.location() """ + AUTH_ENDPOINT = "https://idmsa.apple.com/appleauth/auth" HOME_ENDPOINT = "https://www.icloud.com" SETUP_ENDPOINT = "https://setup.icloud.com/setup/ws/1" @@ -156,6 +196,7 @@ class PyiCloudService(object): apple_id, password=None, cookie_directory=None, + session_directory=None, verify=True, client_id=None, with_family=True, @@ -163,36 +204,52 @@ class PyiCloudService(object): if password is None: password = get_password_from_keyring(apple_id) + self.user = {"accountName": apple_id, "password": password} self.data = {} - self.client_id = client_id or str(uuid1()).upper() + self.params = {} + self.client_id = client_id or f"auth-{str(uuid1()).lower()}" self.with_family = with_family - self.user = {"apple_id": apple_id, "password": password} + + self.session_data = {} + if session_directory: + self._session_directory = session_directory + else: + self._session_directory = path.join( + gettempdir(), "pyicloud-session" + ) + LOGGER.debug(f"Using session file {self._get_sessiondata_path()}") + + try: + with open(self._get_sessiondata_path()) as session_f: + self.session_data = json.load(session_f) + except: + LOGGER.warning("Session file does not exist") + + if not path.exists(self._session_directory): + mkdir(self._session_directory) self.password_filter = PyiCloudPasswordFilter(password) LOGGER.addFilter(self.password_filter) - self._base_login_url = "%s/login" % self.SETUP_ENDPOINT - if cookie_directory: self._cookie_directory = path.expanduser(path.normpath(cookie_directory)) else: self._cookie_directory = path.join(gettempdir(), "pyicloud") + if self.session_data.get("client_id"): + self.client_id = self.session_data.get("client_id") + self.session = PyiCloudSession(self) self.session.verify = verify self.session.headers.update( - { - "Origin": self.HOME_ENDPOINT, - "Referer": "%s/" % self.HOME_ENDPOINT, - "User-Agent": "Opera/9.52 (X11; Linux i686; U; en)", - } + {"Origin": self.HOME_ENDPOINT, "Referer": f"{self.HOME_ENDPOINT}/"} ) cookiejar_path = self._get_cookiejar_path() self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path) if path.exists(cookiejar_path): try: - self.session.cookies.load() + self.session.cookies.load(ignore_discard=True, ignore_expires=True) LOGGER.debug("Read cookies from %s", cookiejar_path) except: # pylint: disable=bare-except # Most likely a pickled cookiejar from earlier versions. @@ -200,14 +257,6 @@ class PyiCloudService(object): # successful authentication. LOGGER.warning("Failed to read cookiejar %s", cookiejar_path) - self.params = { - "clientBuildNumber": "17DHotfix5", - "clientMasteringNumber": "17DHotfix5", - "ckjsBuildVersion": "17DProjectDev77", - "ckjsVersion": "2.0.5", - "clientId": self.client_id, - } - self.authenticate() self._drive = None @@ -216,52 +265,126 @@ class PyiCloudService(object): def authenticate(self): """ - Handles authentication, and persists the X-APPLE-WEB-KB cookie so that + Handles authentication, and persists cookies so that subsequent logins will not cause additional e-mails from Apple. """ - LOGGER.info("Authenticating as %s", self.user["apple_id"]) + login_successful = False + if self.session_data.get("session_token"): + LOGGER.info("Checking session token validity") + try: + req = self.session.post(f"{self.SETUP_ENDPOINT}/validate", data="null") + LOGGER.info("Session token is still valid") + self.data = req.json() + login_successful = True + except: + msg = "Invalid authentication token, will log in from scratch." - data = dict(self.user) + if not login_successful: + LOGGER.info("Authenticating as %s", self.user["accountName"]) - # We authenticate every time, so "remember me" is not needed - data.update({"extended_login": False}) + data = dict(self.user) + + data["rememberMe"] = False + data["trustTokens"] = [] + if self.session_data.get("trust_token"): + data["trustTokens"] = [self.session_data.get("trust_token")] + + headers = { + "Accept": "*/*", + "Content-Type": "application/json", + "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + "X-Apple-OAuth-Client-Type": "firstPartyAuth", + "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", + "X-Apple-OAuth-Require-Grant-Code": "true", + "X-Apple-OAuth-Response-Mode": "web_message", + "X-Apple-OAuth-Response-Type": "code", + "X-Apple-OAuth-State": self.client_id, + "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + } + + if self.session_data.get("scnt"): + headers["scnt"] = self.session_data.get("scnt") + + if self.session_data.get("session_id"): + headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") + + try: + req = self.session.post( + f"{self.AUTH_ENDPOINT}/signin", + params={"isRememberMeEnabled": "true"}, + data=json.dumps(data), + headers=headers, + ) + except PyiCloudAPIResponseException as error: + msg = "Invalid email/password combination." + raise PyiCloudFailedLoginException(msg, error) + + self._authenticate_with_token() + + self._webservices = self.data["webservices"] + + LOGGER.info("Authentication completed successfully") + + def _authenticate_with_token(self): + """Authenticate using session token.""" + data = { + "accountCountryCode": self.session_data.get("account_country"), + "dsWebAuthToken": self.session_data.get("session_token"), + "extended_login": False, + "trustToken": self.session_data.get("trust_token", ""), + } try: req = self.session.post( - self._base_login_url, params=self.params, data=json.dumps(data) + f"{self.SETUP_ENDPOINT}/accountLogin", data=json.dumps(data) ) except PyiCloudAPIResponseException as error: - msg = "Invalid email/password combination." + msg = "Invalid authentication token." raise PyiCloudFailedLoginException(msg, error) self.data = req.json() - self.params.update({"dsid": self.data["dsInfo"]["dsid"]}) - self._webservices = self.data["webservices"] - - if not path.exists(self._cookie_directory): - mkdir(self._cookie_directory) - self.session.cookies.save() - LOGGER.debug("Cookies saved to %s", self._get_cookiejar_path()) - - LOGGER.info("Authentication completed successfully") - LOGGER.debug(self.params) - + def _get_cookiejar_path(self): """Get path for cookiejar file.""" return path.join( self._cookie_directory, - "".join([c for c in self.user.get("apple_id") if match(r"\w", c)]), + "".join([c for c in self.user.get("accountName") if match(r"\w", c)]), + ) + + def _get_sessiondata_path(self): + """Get path for session data file.""" + return path.join( + self._session_directory, + "".join([c for c in self.user.get("accountName") if match(r"\w", c)]), ) @property def requires_2sa(self): """Returns True if two-step authentication is required.""" return ( - self.data.get("hsaChallengeRequired", False) - and self.data["dsInfo"].get("hsaVersion", 0) >= 1 + self.data["dsInfo"].get("hsaVersion", 0) >= 1 + and ( + self.data.get("hsaChallengeRequired", False) + or not self.is_trusted_session + ) ) - # FIXME: Implement 2FA for hsaVersion == 2 # pylint: disable=fixme + + @property + def requires_2fa(self): + """Returns True if two-factor authentication is required.""" + return ( + self.data["dsInfo"].get("hsaVersion", 0) == 2 + and ( + self.data.get("hsaChallengeRequired", False) + or not self.is_trusted_session + ) + ) + + @property + def is_trusted_session(self): + """Returns True if the session is trusted.""" + return self.data.get("hsaTrustedBrowser", False) @property def trusted_devices(self): @@ -304,6 +427,76 @@ class PyiCloudService(object): return not self.requires_2sa + def validate_2fa_code(self, code): + """Verifies a verification code received via Apple's 2FA system (HSA2).""" + data = {"securityCode": {"code": code}} + + headers = { + "Accept": "application/json", + "Content-Type": "application/json", + "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + "X-Apple-OAuth-Client-Type": "firstPartyAuth", + "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", + "X-Apple-OAuth-Require-Grant-Code": "true", + "X-Apple-OAuth-Response-Mode": "web_message", + "X-Apple-OAuth-Response-Type": "code", + "X-Apple-OAuth-State": self.client_id, + "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + } + + if self.session_data.get("scnt"): + headers["scnt"] = self.session_data.get("scnt") + + if self.session_data.get("session_id"): + headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") + + try: + req = self.session.post( + f"{self.AUTH_ENDPOINT}/verify/trusteddevice/securitycode", + data=json.dumps(data), + headers=headers, + ) + except PyiCloudAPIResponseException as error: + LOGGER.error("Code verification failed.") + return False + + LOGGER.debug("Code verification successful.") + + self.trust_session() + + return not self.requires_2sa + + def trust_session(self): + """Request session trust to avoid user log in going forward.""" + headers = { + "Accept": "*/*", + "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + "X-Apple-OAuth-Client-Type": "firstPartyAuth", + "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", + "X-Apple-OAuth-Require-Grant-Code": "true", + "X-Apple-OAuth-Response-Mode": "web_message", + "X-Apple-OAuth-Response-Type": "code", + "X-Apple-OAuth-State": self.client_id, + "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + } + + if self.session_data.get("scnt"): + headers["scnt"] = self.session_data.get("scnt") + + if self.session_data.get("session_id"): + headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") + + try: + req = self.session.get( + f"{self.AUTH_ENDPOINT}/2sv/trust", + headers=headers, + ) + self._authenticate_with_token() + return True + except PyiCloudAPIResponseException as error: + LOGGER.error("Session trust failed.") + return False + def _get_webservice_url(self, ws_key): """Get webservice URL, raise an exception if not exists.""" if self._webservices.get(ws_key) is None: @@ -378,7 +571,7 @@ class PyiCloudService(object): return self._drive def __unicode__(self): - return "iCloud API: %s" % self.user.get("apple_id") + return "iCloud API: %s" % self.user.get("accountName") def __str__(self): as_unicode = self.__unicode__() From 4adbfb32ec54f8a7838e2c90c690f06f33d71705 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Thu, 29 Oct 2020 16:51:08 +0000 Subject: [PATCH 02/15] Added tests --- pyicloud/base.py | 63 ++++++++++++++++++++----------------------- pyicloud/cmdline.py | 17 +++++++++++- tests/__init__.py | 55 ++++++++++++++++++++++++++++--------- tests/const.py | 9 +++++-- tests/const_login.py | 6 ++++- tests/test_cmdline.py | 8 +++--- 6 files changed, 104 insertions(+), 54 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 3f907d9..349cd7e 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -101,15 +101,13 @@ class PyiCloudSession(Session): ) # Save session_data to file - with open(self.service._get_sessiondata_path(), "w") as outfile: + with open(self.service.session_path, "w") as outfile: json.dump(self.service.session_data, outfile) LOGGER.debug("Saved session data to file") # Save cookies to file - if not path.exists(self.service._cookie_directory): - mkdir(self.service._cookie_directory) self.cookies.save(ignore_discard=True, ignore_expires=True) - LOGGER.debug("Cookies saved to %s", self.service._get_cookiejar_path()) + LOGGER.debug("Cookies saved to %s", self.service.cookiejar_path) if not response.ok and content_type not in json_mimetypes: if has_retried is None and response.status_code == 450: @@ -214,16 +212,14 @@ class PyiCloudService(object): if session_directory: self._session_directory = session_directory else: - self._session_directory = path.join( - gettempdir(), "pyicloud-session" - ) - LOGGER.debug(f"Using session file {self._get_sessiondata_path()}") + self._session_directory = path.join(gettempdir(), "pyicloud-session") + LOGGER.debug("Using session file %s", self.session_path) try: - with open(self._get_sessiondata_path()) as session_f: + with open(self.session_path) as session_f: self.session_data = json.load(session_f) - except: - LOGGER.warning("Session file does not exist") + except: # pylint: disable=bare-except + LOGGER.info("Session file does not exist") if not path.exists(self._session_directory): mkdir(self._session_directory) @@ -236,6 +232,9 @@ class PyiCloudService(object): else: self._cookie_directory = path.join(gettempdir(), "pyicloud") + if not path.exists(self._cookie_directory): + mkdir(self._cookie_directory) + if self.session_data.get("client_id"): self.client_id = self.session_data.get("client_id") @@ -245,7 +244,7 @@ class PyiCloudService(object): {"Origin": self.HOME_ENDPOINT, "Referer": f"{self.HOME_ENDPOINT}/"} ) - cookiejar_path = self._get_cookiejar_path() + cookiejar_path = self.cookiejar_path self.session.cookies = cookielib.LWPCookieJar(filename=cookiejar_path) if path.exists(cookiejar_path): try: @@ -277,7 +276,7 @@ class PyiCloudService(object): LOGGER.info("Session token is still valid") self.data = req.json() login_successful = True - except: + except PyiCloudAPIResponseException: msg = "Invalid authentication token, will log in from scratch." if not login_successful: @@ -344,15 +343,17 @@ class PyiCloudService(object): raise PyiCloudFailedLoginException(msg, error) self.data = req.json() - - def _get_cookiejar_path(self): + + @property + def cookiejar_path(self): """Get path for cookiejar file.""" return path.join( self._cookie_directory, "".join([c for c in self.user.get("accountName") if match(r"\w", c)]), ) - def _get_sessiondata_path(self): + @property + def session_path(self): """Get path for session data file.""" return path.join( self._session_directory, @@ -362,23 +363,15 @@ class PyiCloudService(object): @property def requires_2sa(self): """Returns True if two-step authentication is required.""" - return ( - self.data["dsInfo"].get("hsaVersion", 0) >= 1 - and ( - self.data.get("hsaChallengeRequired", False) - or not self.is_trusted_session - ) + return self.data.get("dsInfo", {}).get("hsaVersion", 0) >= 1 and ( + self.data.get("hsaChallengeRequired", False) or not self.is_trusted_session ) @property def requires_2fa(self): """Returns True if two-factor authentication is required.""" - return ( - self.data["dsInfo"].get("hsaVersion", 0) == 2 - and ( - self.data.get("hsaChallengeRequired", False) - or not self.is_trusted_session - ) + return self.data["dsInfo"].get("hsaVersion", 0) == 2 and ( + self.data.get("hsaChallengeRequired", False) or not self.is_trusted_session ) @property @@ -451,19 +444,21 @@ class PyiCloudService(object): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") try: - req = self.session.post( + self.session.post( f"{self.AUTH_ENDPOINT}/verify/trusteddevice/securitycode", data=json.dumps(data), headers=headers, ) except PyiCloudAPIResponseException as error: - LOGGER.error("Code verification failed.") - return False + if error.code == -21669: + # Wrong verification code + LOGGER.error("Code verification failed.") + return False + raise LOGGER.debug("Code verification successful.") self.trust_session() - return not self.requires_2sa def trust_session(self): @@ -487,13 +482,13 @@ class PyiCloudService(object): headers["X-Apple-ID-Session-Id"] = self.session_data.get("session_id") try: - req = self.session.get( + self.session.get( f"{self.AUTH_ENDPOINT}/2sv/trust", headers=headers, ) self._authenticate_with_token() return True - except PyiCloudAPIResponseException as error: + except PyiCloudAPIResponseException: LOGGER.error("Session trust failed.") return False diff --git a/pyicloud/cmdline.py b/pyicloud/cmdline.py index a6bbcf7..8137cda 100644 --- a/pyicloud/cmdline.py +++ b/pyicloud/cmdline.py @@ -201,7 +201,22 @@ def main(args=None): ): utils.store_password_in_keyring(username, password) - if api.requires_2sa: + if api.requires_2fa: + # fmt: off + print( + "\nTwo-step authentication required.", + "\nPlease enter validation code" + ) + # fmt: on + + code = input("(string) --> ") + if not api.validate_2fa_code(code): + print("Failed to verify verification code") + sys.exit(1) + + print("") + + elif api.requires_2sa: # fmt: off print( "\nTwo-step authentication required.", diff --git a/tests/__init__.py b/tests/__init__.py index 828a5dd..d36a28d 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -9,13 +9,19 @@ from pyicloud.services.findmyiphone import FindMyiPhoneServiceManager, AppleDevi from .const import ( AUTHENTICATED_USER, - REQUIRES_2SA_USER, + REQUIRES_2FA_USER, + REQUIRES_2FA_TOKEN, + VALID_TOKEN, VALID_USERS, VALID_PASSWORD, + VALID_COOKIE, + VALID_2FA_CODE, + VALID_TOKENS, ) from .const_login import ( + AUTH_OK, LOGIN_WORKING, - LOGIN_2SA, + LOGIN_2FA, TRUSTED_DEVICES, TRUSTED_DEVICE_1, VERIFICATION_CODE_OK, @@ -41,6 +47,7 @@ class ResponseMock(Response): self.result = result self.status_code = status_code self.raw = kwargs.get("raw") + self.headers = kwargs.get("headers", {}) @property def text(self): @@ -52,21 +59,16 @@ class PyiCloudSessionMock(base.PyiCloudSession): def request(self, method, url, **kwargs): params = kwargs.get("params") + headers = kwargs.get("headers") data = json.loads(kwargs.get("data", "{}")) # Login if self.service.SETUP_ENDPOINT in url: - if "login" in url and method == "POST": - if ( - data.get("apple_id") not in VALID_USERS - or data.get("password") != VALID_PASSWORD - ): + if "accountLogin" in url and method == "POST": + if data.get("dsWebAuthToken") not in VALID_TOKENS: self._raise_error(None, "Unknown reason") - if ( - data.get("apple_id") == REQUIRES_2SA_USER - and data.get("password") == VALID_PASSWORD - ): - return ResponseMock(LOGIN_2SA) + if data.get("dsWebAuthToken") == REQUIRES_2FA_TOKEN: + return ResponseMock(LOGIN_2FA) return ResponseMock(LOGIN_WORKING) if "listDevices" in url and method == "GET": @@ -84,6 +86,35 @@ class PyiCloudSessionMock(base.PyiCloudSession): return ResponseMock(VERIFICATION_CODE_OK) self._raise_error(None, "FOUND_CODE") + if "validate" in url and method == "POST": + if headers.get("X-APPLE-WEBAUTH-TOKEN") == VALID_COOKIE: + return ResponseMock(LOGIN_WORKING) + self._raise_error(None, "Session expired") + + if self.service.AUTH_ENDPOINT in url: + if "signin" in url and method == "POST": + if ( + data.get("accountName") not in VALID_USERS + or data.get("password") != VALID_PASSWORD + ): + self._raise_error(None, "Unknown reason") + if data.get("accountName") == REQUIRES_2FA_USER: + self.service.session_data["session_token"] = REQUIRES_2FA_TOKEN + return ResponseMock(AUTH_OK) + + self.service.session_data["session_token"] = VALID_TOKEN + return ResponseMock(AUTH_OK) + + if "securitycode" in url and method == "POST": + if data.get("securityCode", {}).get("code") != VALID_2FA_CODE: + self._raise_error(None, "Incorrect code") + + self.service.session_data["session_token"] = VALID_TOKEN + return ResponseMock("", status_code=204) + + if "trust" in url and method == "GET": + return ResponseMock("", status_code=204) + # Account if "device/getDevices" in url and method == "GET": return ResponseMock(ACCOUNT_DEVICES_WORKING) diff --git a/tests/const.py b/tests/const.py index 3b69842..1dbd27c 100644 --- a/tests/const.py +++ b/tests/const.py @@ -4,8 +4,13 @@ from .const_account_family import PRIMARY_EMAIL, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL # Base AUTHENTICATED_USER = PRIMARY_EMAIL -REQUIRES_2SA_USER = "requires_2sa_user" -VALID_USERS = [AUTHENTICATED_USER, REQUIRES_2SA_USER, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL] +REQUIRES_2FA_TOKEN = "requires_2fa_token" +REQUIRES_2FA_USER = "requires_2fa_user" +VALID_USERS = [AUTHENTICATED_USER, REQUIRES_2FA_USER, APPLE_ID_EMAIL, ICLOUD_ID_EMAIL] VALID_PASSWORD = "valid_password" +VALID_COOKIE = "valid_cookie" +VALID_TOKEN = "valid_token" +VALID_2FA_CODE = "000000" +VALID_TOKENS = [VALID_TOKEN, REQUIRES_2FA_TOKEN] CLIENT_ID = "client_id" diff --git a/tests/const_login.py b/tests/const_login.py index 25ec7c0..352c2ae 100644 --- a/tests/const_login.py +++ b/tests/const_login.py @@ -16,6 +16,10 @@ A_DS_ID = "123456-12-12345678-1234-1234-1234-123456789012" + PERSON_ID WIDGET_KEY = "widget_key" + PERSON_ID # Data +AUTH_OK = { + "authType": "hsa2" +} + LOGIN_WORKING = { "dsInfo": { "lastName": LAST_NAME, @@ -209,7 +213,7 @@ LOGIN_WORKING = { } # Setup data -LOGIN_2SA = { +LOGIN_2FA = { "dsInfo": { "lastName": LAST_NAME, "iCDPEnabled": False, diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 1a80239..42c2509 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -2,7 +2,7 @@ """Cmdline tests.""" from pyicloud import cmdline from . import PyiCloudServiceMock -from .const import AUTHENTICATED_USER, REQUIRES_2SA_USER, VALID_PASSWORD +from .const import AUTHENTICATED_USER, REQUIRES_2FA_USER, VALID_PASSWORD, VALID_2FA_CODE from .const_findmyiphone import FMI_FAMILY_WORKING import os @@ -74,16 +74,16 @@ class TestCmdline(TestCase): @patch("keyring.get_password", return_value=None) @patch("pyicloud.cmdline.input") - def test_username_password_requires_2sa( + def test_username_password_requires_2fa( self, mock_input, mock_get_password ): # pylint: disable=unused-argument """Test username and password commands.""" # Valid connection for the first time - mock_input.return_value = "0" + mock_input.return_value = VALID_2FA_CODE with pytest.raises(SystemExit, match="0"): # fmt: off self.main([ - '--username', REQUIRES_2SA_USER, + '--username', REQUIRES_2FA_USER, '--password', VALID_PASSWORD, '--non-interactive', ]) From c6fecebde6c02cb17abe8878f3b1d763d1c90e73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Thu, 29 Oct 2020 17:06:39 +0000 Subject: [PATCH 03/15] Removed repetitive code --- pyicloud/base.py | 93 +++++++++++++++++++----------------------------- 1 file changed, 36 insertions(+), 57 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 349cd7e..79f7720 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -31,6 +31,14 @@ from pyicloud.utils import get_password_from_keyring LOGGER = logging.getLogger(__name__) +HEADER_DATA = { + "X-Apple-ID-Account-Country": "account_country", + "X-Apple-ID-Session-Id": "session_id", + "X-Apple-Session-Token": "session_token", + "X-Apple-TwoSV-Trust-Token": "trust_token", + "scnt": "scnt", +} + class PyiCloudPasswordFilter(logging.Filter): """Password log hider.""" @@ -77,28 +85,12 @@ class PyiCloudSession(Session): content_type = response.headers.get("Content-Type", "").split(";")[0] json_mimetypes = ["application/json", "text/json"] - if response.headers.get("X-Apple-ID-Session-Id"): - self.service.session_data["session_id"] = response.headers.get( - "X-Apple-ID-Session-Id" - ) - - if response.headers.get("X-Apple-Session-Token"): - self.service.session_data["session_token"] = response.headers.get( - "X-Apple-Session-Token" - ) - - if response.headers.get("X-Apple-ID-Account-Country"): - self.service.session_data["account_country"] = response.headers.get( - "X-Apple-ID-Account-Country" - ) - - if response.headers.get("scnt"): - self.service.session_data["scnt"] = response.headers.get("scnt") - - if response.headers.get("X-Apple-TwoSV-Trust-Token"): - self.service.session_data["trust_token"] = response.headers.get( - "X-Apple-TwoSV-Trust-Token" - ) + for header in HEADER_DATA: + if response.headers.get(header): + session_arg = HEADER_DATA[header] + self.service.session_data.update( + {session_arg: response.headers.get(header)} + ) # Save session_data to file with open(self.service.session_path, "w") as outfile: @@ -237,6 +229,8 @@ class PyiCloudService(object): if self.session_data.get("client_id"): self.client_id = self.session_data.get("client_id") + else: + self.session_data.update({"client_id": self.client_id}) self.session = PyiCloudSession(self) self.session.verify = verify @@ -289,18 +283,7 @@ class PyiCloudService(object): if self.session_data.get("trust_token"): data["trustTokens"] = [self.session_data.get("trust_token")] - headers = { - "Accept": "*/*", - "Content-Type": "application/json", - "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", - "X-Apple-OAuth-Client-Type": "firstPartyAuth", - "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", - "X-Apple-OAuth-Require-Grant-Code": "true", - "X-Apple-OAuth-Response-Mode": "web_message", - "X-Apple-OAuth-Response-Type": "code", - "X-Apple-OAuth-State": self.client_id, - "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", - } + headers = self._get_auth_headers() if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") @@ -344,6 +327,23 @@ class PyiCloudService(object): self.data = req.json() + def _get_auth_headers(self, overrides=None): + headers = { + "Accept": "*/*", + "Content-Type": "application/json", + "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + "X-Apple-OAuth-Client-Type": "firstPartyAuth", + "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", + "X-Apple-OAuth-Require-Grant-Code": "true", + "X-Apple-OAuth-Response-Mode": "web_message", + "X-Apple-OAuth-Response-Type": "code", + "X-Apple-OAuth-State": self.client_id, + "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", + } + if overrides: + headers.update(overrides) + return headers + @property def cookiejar_path(self): """Get path for cookiejar file.""" @@ -424,18 +424,7 @@ class PyiCloudService(object): """Verifies a verification code received via Apple's 2FA system (HSA2).""" data = {"securityCode": {"code": code}} - headers = { - "Accept": "application/json", - "Content-Type": "application/json", - "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", - "X-Apple-OAuth-Client-Type": "firstPartyAuth", - "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", - "X-Apple-OAuth-Require-Grant-Code": "true", - "X-Apple-OAuth-Response-Mode": "web_message", - "X-Apple-OAuth-Response-Type": "code", - "X-Apple-OAuth-State": self.client_id, - "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", - } + headers = self._get_auth_headers({"Accept": "application/json"}) if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") @@ -463,17 +452,7 @@ class PyiCloudService(object): def trust_session(self): """Request session trust to avoid user log in going forward.""" - headers = { - "Accept": "*/*", - "X-Apple-OAuth-Client-Id": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", - "X-Apple-OAuth-Client-Type": "firstPartyAuth", - "X-Apple-OAuth-Redirect-URI": "https://www.icloud.com", - "X-Apple-OAuth-Require-Grant-Code": "true", - "X-Apple-OAuth-Response-Mode": "web_message", - "X-Apple-OAuth-Response-Type": "code", - "X-Apple-OAuth-State": self.client_id, - "X-Apple-Widget-Key": "d39ba9916b7251055b22c7f910e2ea796ee65e98b2ddecea8f5dde8d9d1a815d", - } + headers = self._get_auth_headers() if self.session_data.get("scnt"): headers["scnt"] = self.session_data.get("scnt") From fc833555ac0ff120437a650f091a7803cffe4ee3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Thu, 5 Nov 2020 08:02:05 +0000 Subject: [PATCH 04/15] Added new trust token support for old 2SA method --- pyicloud/base.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 79f7720..2bbfdce 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -414,9 +414,7 @@ class PyiCloudService(object): return False raise - # Re-authenticate, which will both update the HSA data, and - # ensure that we save the X-APPLE-WEBAUTH-HSA-TRUST cookie. - self.authenticate() + self.trust_session() return not self.requires_2sa From 8f1bd9473a907d7c87afeb4182521512dee2ad3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Thu, 5 Nov 2020 08:33:12 +0000 Subject: [PATCH 05/15] Updated logging levels --- pyicloud/base.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 2bbfdce..8afe54b 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -264,17 +264,17 @@ class PyiCloudService(object): login_successful = False if self.session_data.get("session_token"): - LOGGER.info("Checking session token validity") + LOGGER.debug("Checking session token validity") try: req = self.session.post(f"{self.SETUP_ENDPOINT}/validate", data="null") - LOGGER.info("Session token is still valid") + LOGGER.debug("Session token is still valid") self.data = req.json() login_successful = True except PyiCloudAPIResponseException: - msg = "Invalid authentication token, will log in from scratch." + LOGGER.debug("Invalid authentication token, will log in from scratch.") if not login_successful: - LOGGER.info("Authenticating as %s", self.user["accountName"]) + LOGGER.debug("Authenticating as %s", self.user["accountName"]) data = dict(self.user) @@ -306,7 +306,7 @@ class PyiCloudService(object): self._webservices = self.data["webservices"] - LOGGER.info("Authentication completed successfully") + LOGGER.debug("Authentication completed successfully") def _authenticate_with_token(self): """Authenticate using session token.""" From 6f0aa0360a830adfaae48b0b38b660df55e0f034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Mon, 9 Nov 2020 09:11:14 +0000 Subject: [PATCH 06/15] Added support to force auth refresh --- pyicloud/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 8afe54b..d7b8070 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -256,14 +256,14 @@ class PyiCloudService(object): self._files = None self._photos = None - def authenticate(self): + def authenticate(self, force_refresh=False): """ Handles authentication, and persists cookies so that subsequent logins will not cause additional e-mails from Apple. """ login_successful = False - if self.session_data.get("session_token"): + if self.session_data.get("session_token") and not force_refresh: LOGGER.debug("Checking session token validity") try: req = self.session.post(f"{self.SETUP_ENDPOINT}/validate", data="null") From 8e55d638f16d819daee0d7cc4c9e558285d4bc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Tue, 10 Nov 2020 19:42:46 +0000 Subject: [PATCH 07/15] Added retry for error codes 421 and 500 --- pyicloud/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index d7b8070..e6977ae 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -102,7 +102,7 @@ class PyiCloudSession(Session): LOGGER.debug("Cookies saved to %s", self.service.cookiejar_path) if not response.ok and content_type not in json_mimetypes: - if has_retried is None and response.status_code == 450: + if has_retried is None and response.status_code in [421, 450, 500]: api_error = PyiCloudAPIResponseException( response.reason, response.status_code, retry=True ) From 1675a8dc11b06ab2e8f622d7600a2452fa47892c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Thu, 12 Nov 2020 08:37:29 +0000 Subject: [PATCH 08/15] Improved support for 421s --- pyicloud/base.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index e6977ae..8f5e126 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -101,7 +101,8 @@ class PyiCloudSession(Session): self.cookies.save(ignore_discard=True, ignore_expires=True) LOGGER.debug("Cookies saved to %s", self.service.cookiejar_path) - if not response.ok and content_type not in json_mimetypes: + if not response.ok and (content_type not in json_mimetypes + or response.status_code in [421, 450, 500]): if has_retried is None and response.status_code in [421, 450, 500]: api_error = PyiCloudAPIResponseException( response.reason, response.status_code, retry=True @@ -160,6 +161,8 @@ class PyiCloudSession(Session): reason + ". Please wait a few minutes then try again." "The remote servers might be trying to throttle requests." ) + if code in [421, 450, 500]: + reason = "Authentication required for Account." api_error = PyiCloudAPIResponseException(reason, code) LOGGER.error(api_error) From e14d22908d32ca60de6c5b3b6babf5f66147b41d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Tue, 17 Nov 2020 08:36:08 +0000 Subject: [PATCH 09/15] Set remember me to true to avoid Apple emails --- pyicloud/base.py | 4 ++-- tests/const_login.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 8f5e126..f69e1a0 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -281,7 +281,7 @@ class PyiCloudService(object): data = dict(self.user) - data["rememberMe"] = False + data["rememberMe"] = True data["trustTokens"] = [] if self.session_data.get("trust_token"): data["trustTokens"] = [self.session_data.get("trust_token")] @@ -316,7 +316,7 @@ class PyiCloudService(object): data = { "accountCountryCode": self.session_data.get("account_country"), "dsWebAuthToken": self.session_data.get("session_token"), - "extended_login": False, + "extended_login": True, "trustToken": self.session_data.get("trust_token", ""), } diff --git a/tests/const_login.py b/tests/const_login.py index 352c2ae..bb87eb6 100644 --- a/tests/const_login.py +++ b/tests/const_login.py @@ -188,7 +188,7 @@ LOGIN_WORKING = { "settings", ], "version": 2, - "isExtendedLogin": False, + "isExtendedLogin": True, "pcsServiceIdentitiesIncluded": True, "hsaChallengeRequired": False, "requestInfo": {"country": "FR", "timeZone": "GMT+1", "region": "IDF"}, @@ -381,7 +381,7 @@ LOGIN_2FA = { "settings", ], "version": 2, - "isExtendedLogin": False, + "isExtendedLogin": True, "pcsServiceIdentitiesIncluded": False, "hsaChallengeRequired": True, "requestInfo": {"country": "FR", "timeZone": "GMT+1", "region": "IDF"}, From 6fa52c63372fb2ed726f0f1e44c5438eb3875914 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Mon, 23 Nov 2020 06:47:09 +0000 Subject: [PATCH 10/15] Restored python2.7 compatibility --- pyicloud/base.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index f69e1a0..597bbab 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -200,7 +200,7 @@ class PyiCloudService(object): self.user = {"accountName": apple_id, "password": password} self.data = {} self.params = {} - self.client_id = client_id or f"auth-{str(uuid1()).lower()}" + self.client_id = client_id or ("auth-%s" % str(uuid1()).lower()) self.with_family = with_family self.session_data = {} @@ -238,7 +238,7 @@ class PyiCloudService(object): self.session = PyiCloudSession(self) self.session.verify = verify self.session.headers.update( - {"Origin": self.HOME_ENDPOINT, "Referer": f"{self.HOME_ENDPOINT}/"} + {"Origin": self.HOME_ENDPOINT, "Referer": "%s/" % self.HOME_ENDPOINT} ) cookiejar_path = self.cookiejar_path @@ -269,7 +269,7 @@ class PyiCloudService(object): if self.session_data.get("session_token") and not force_refresh: LOGGER.debug("Checking session token validity") try: - req = self.session.post(f"{self.SETUP_ENDPOINT}/validate", data="null") + req = self.session.post("%s/validate" % self.SETUP_ENDPOINT, data="null") LOGGER.debug("Session token is still valid") self.data = req.json() login_successful = True @@ -296,7 +296,7 @@ class PyiCloudService(object): try: req = self.session.post( - f"{self.AUTH_ENDPOINT}/signin", + "%s/signin" % self.AUTH_ENDPOINT, params={"isRememberMeEnabled": "true"}, data=json.dumps(data), headers=headers, @@ -322,7 +322,7 @@ class PyiCloudService(object): try: req = self.session.post( - f"{self.SETUP_ENDPOINT}/accountLogin", data=json.dumps(data) + "%s/accountLogin" % self.SETUP_ENDPOINT, data=json.dumps(data) ) except PyiCloudAPIResponseException as error: msg = "Invalid authentication token." @@ -435,7 +435,7 @@ class PyiCloudService(object): try: self.session.post( - f"{self.AUTH_ENDPOINT}/verify/trusteddevice/securitycode", + "%s/verify/trusteddevice/securitycode" % self.AUTH_ENDPOINT, data=json.dumps(data), headers=headers, ) @@ -463,7 +463,7 @@ class PyiCloudService(object): try: self.session.get( - f"{self.AUTH_ENDPOINT}/2sv/trust", + "%s/2sv/trust" % self.AUTH_ENDPOINT, headers=headers, ) self._authenticate_with_token() From 285a114a64f99d7ab192923b872ad3fc3f12c7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Tue, 24 Nov 2020 07:20:49 +0000 Subject: [PATCH 11/15] Fixed %s formatting --- pyicloud/base.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 597bbab..a0c5d3a 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -72,10 +72,11 @@ class PyiCloudSession(Session): request_logger.addFilter(self.service.password_filter) request_logger.debug( - "%s %s %s", - method, - url, - kwargs.get("data", ""), + "%s %s %s" % ( + method, + url, + kwargs.get("data", "") + ) ) has_retried = kwargs.get("retried") @@ -208,7 +209,7 @@ class PyiCloudService(object): self._session_directory = session_directory else: self._session_directory = path.join(gettempdir(), "pyicloud-session") - LOGGER.debug("Using session file %s", self.session_path) + LOGGER.debug("Using session file %s" % self.session_path) try: with open(self.session_path) as session_f: @@ -246,12 +247,12 @@ class PyiCloudService(object): if path.exists(cookiejar_path): try: self.session.cookies.load(ignore_discard=True, ignore_expires=True) - LOGGER.debug("Read cookies from %s", cookiejar_path) + LOGGER.debug("Read cookies from %s" % cookiejar_path) except: # pylint: disable=bare-except # Most likely a pickled cookiejar from earlier versions. # The cookiejar will get replaced with a valid one after # successful authentication. - LOGGER.warning("Failed to read cookiejar %s", cookiejar_path) + LOGGER.warning("Failed to read cookiejar %s" % cookiejar_path) self.authenticate() @@ -277,7 +278,7 @@ class PyiCloudService(object): LOGGER.debug("Invalid authentication token, will log in from scratch.") if not login_successful: - LOGGER.debug("Authenticating as %s", self.user["accountName"]) + LOGGER.debug("Authenticating as %s" % self.user["accountName"]) data = dict(self.user) From 9190c62a805f9011a3d839abf3aae6bd3c75cc3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Wed, 9 Dec 2020 08:03:19 +0000 Subject: [PATCH 12/15] Added service specific log in --- pyicloud/base.py | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index a0c5d3a..dda080b 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -260,7 +260,7 @@ class PyiCloudService(object): self._files = None self._photos = None - def authenticate(self, force_refresh=False): + def authenticate(self, force_refresh=False, service=None): """ Handles authentication, and persists cookies so that subsequent logins will not cause additional e-mails from Apple. @@ -273,10 +273,19 @@ class PyiCloudService(object): req = self.session.post("%s/validate" % self.SETUP_ENDPOINT, data="null") LOGGER.debug("Session token is still valid") self.data = req.json() + LOGGER.debug(req.json()) login_successful = True except PyiCloudAPIResponseException: LOGGER.debug("Invalid authentication token, will log in from scratch.") + if not login_successful and service != None: + LOGGER.debug("Authenticating as %s for %s" % (self.user["accountName"], service)) + try: + self._authenticate_with_credentials_service(service) + login_successful = True + except: + LOGGER.debug("Could not log into service. Attempting brand new login.") + if not login_successful: LOGGER.debug("Authenticating as %s" % self.user["accountName"]) @@ -302,6 +311,11 @@ class PyiCloudService(object): data=json.dumps(data), headers=headers, ) + LOGGER.debug(req.headers) + try: + LOGGER.debug(req.json()) + except: + LOGGER.debug(req) except PyiCloudAPIResponseException as error: msg = "Invalid email/password combination." raise PyiCloudFailedLoginException(msg, error) @@ -325,11 +339,33 @@ class PyiCloudService(object): req = self.session.post( "%s/accountLogin" % self.SETUP_ENDPOINT, data=json.dumps(data) ) + self.data = req.json() except PyiCloudAPIResponseException as error: msg = "Invalid authentication token." raise PyiCloudFailedLoginException(msg, error) - self.data = req.json() + def _authenticate_with_credentials_service(self, service): + """Authenticate to a specific service using credentials.""" + data = { + "appName": service, + "apple_id": self.user["accountName"], + "password": self.user["password"] + } + + try: + req = self.session.post( + "%s/accountLogin" % self.SETUP_ENDPOINT, data=json.dumps(data) + ) + + self.data = req.json() + LOGGER.debug(req.headers) + try: + LOGGER.debug(req.json()) + except: + LOGGER.debug(req) + except PyiCloudAPIResponseException as error: + msg = "Invalid email/password combination." + raise PyiCloudFailedLoginException(msg, error) def _get_auth_headers(self, overrides=None): headers = { From cb302d58f5b40f9147d6187db59ae34a3cf336de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Wed, 9 Dec 2020 08:14:40 +0000 Subject: [PATCH 13/15] Removed a lot of logging --- pyicloud/base.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index dda080b..ad6983e 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -311,11 +311,6 @@ class PyiCloudService(object): data=json.dumps(data), headers=headers, ) - LOGGER.debug(req.headers) - try: - LOGGER.debug(req.json()) - except: - LOGGER.debug(req) except PyiCloudAPIResponseException as error: msg = "Invalid email/password combination." raise PyiCloudFailedLoginException(msg, error) @@ -358,11 +353,6 @@ class PyiCloudService(object): ) self.data = req.json() - LOGGER.debug(req.headers) - try: - LOGGER.debug(req.json()) - except: - LOGGER.debug(req) except PyiCloudAPIResponseException as error: msg = "Invalid email/password combination." raise PyiCloudFailedLoginException(msg, error) From e0f11158e10ccc820078968fe82f6e9280f19dca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Fri, 11 Dec 2020 09:36:48 +0000 Subject: [PATCH 14/15] Added support for reauth if FMIP requires it --- pyicloud/base.py | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index ad6983e..7abb914 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -104,13 +104,24 @@ class PyiCloudSession(Session): if not response.ok and (content_type not in json_mimetypes or response.status_code in [421, 450, 500]): - if has_retried is None and response.status_code in [421, 450, 500]: + if has_retried is None and response.status_code == 450 and self.service._get_webservice_url("findme") in url: + # Handle re-authentication for Find My iPhone + LOGGER.debug("Re-authenticating Find My iPhone service") + try: + self.service.authenticate(True, "find") + except PyiCloudAPIResponseException: + LOGGER.debug("Re-authentication failed") + kwargs["retried"] = True + return self.request(method, url, **kwargs) + + elif has_retried is None and response.status_code in [421, 450, 500]: api_error = PyiCloudAPIResponseException( response.reason, response.status_code, retry=True ) request_logger.debug(api_error) kwargs["retried"] = True return self.request(method, url, **kwargs) + self._raise_error(response.status_code, response.reason) if content_type not in json_mimetypes: @@ -270,21 +281,20 @@ class PyiCloudService(object): if self.session_data.get("session_token") and not force_refresh: LOGGER.debug("Checking session token validity") try: - req = self.session.post("%s/validate" % self.SETUP_ENDPOINT, data="null") - LOGGER.debug("Session token is still valid") - self.data = req.json() - LOGGER.debug(req.json()) + self.data = self._validate_token() login_successful = True except PyiCloudAPIResponseException: LOGGER.debug("Invalid authentication token, will log in from scratch.") if not login_successful and service != None: - LOGGER.debug("Authenticating as %s for %s" % (self.user["accountName"], service)) - try: - self._authenticate_with_credentials_service(service) - login_successful = True - except: - LOGGER.debug("Could not log into service. Attempting brand new login.") + app = self.data["apps"][service] + if "canLaunchWithOneFactor" in app and app["canLaunchWithOneFactor"] == True: + LOGGER.debug("Authenticating as %s for %s" % (self.user["accountName"], service)) + try: + self._authenticate_with_credentials_service(service) + login_successful = True + except: + LOGGER.debug("Could not log into service. Attempting brand new login.") if not login_successful: LOGGER.debug("Authenticating as %s" % self.user["accountName"]) @@ -348,15 +358,26 @@ class PyiCloudService(object): } try: - req = self.session.post( + self.session.post( "%s/accountLogin" % self.SETUP_ENDPOINT, data=json.dumps(data) ) - self.data = req.json() + self.data = self._validate_token() except PyiCloudAPIResponseException as error: msg = "Invalid email/password combination." raise PyiCloudFailedLoginException(msg, error) + def _validate_token(self): + """Checks if the current access token is still valid.""" + LOGGER.debug("Checking session token validity") + try: + req = self.session.post("%s/validate" % self.SETUP_ENDPOINT, data="null") + LOGGER.debug("Session token is still valid") + return req.json() + except PyiCloudAPIResponseException as err: + LOGGER.debug("Invalid authentication token") + raise err + def _get_auth_headers(self, overrides=None): headers = { "Accept": "*/*", From 94f8ef8aaa884cc16d93d1dff7f059316bdbf062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niccolo=CC=80=20Zapponi?= Date: Fri, 11 Dec 2020 09:41:35 +0000 Subject: [PATCH 15/15] Fixed bug --- pyicloud/base.py | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/pyicloud/base.py b/pyicloud/base.py index 7abb914..5081b96 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -104,17 +104,21 @@ class PyiCloudSession(Session): if not response.ok and (content_type not in json_mimetypes or response.status_code in [421, 450, 500]): - if has_retried is None and response.status_code == 450 and self.service._get_webservice_url("findme") in url: - # Handle re-authentication for Find My iPhone - LOGGER.debug("Re-authenticating Find My iPhone service") - try: - self.service.authenticate(True, "find") - except PyiCloudAPIResponseException: - LOGGER.debug("Re-authentication failed") - kwargs["retried"] = True - return self.request(method, url, **kwargs) + try: + fmip_url = self.service._get_webservice_url("findme") + if has_retried is None and response.status_code == 450 and fmip_url in url: + # Handle re-authentication for Find My iPhone + LOGGER.debug("Re-authenticating Find My iPhone service") + try: + self.service.authenticate(True, "find") + except PyiCloudAPIResponseException: + LOGGER.debug("Re-authentication failed") + kwargs["retried"] = True + return self.request(method, url, **kwargs) + except Exception: + pass - elif has_retried is None and response.status_code in [421, 450, 500]: + if has_retried is None and response.status_code in [421, 450, 500]: api_error = PyiCloudAPIResponseException( response.reason, response.status_code, retry=True )