Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update to pyicloud-1.0.0 to support 2fa verification #528

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ Options:
-u, --username <username> Your iCloud username or email address
-p, --password <password> Your iCloud password (default: use PyiCloud
keyring or prompt for password)
--china-mainland The country/region setting of your Apple ID is China mainland(中国大陆)
--cookie-directory </cookie/directory>
Directory to store cookies for
authentication (default: ~/.pyicloud)
Expand Down
2 changes: 1 addition & 1 deletion icloud.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python
from pyicloud_ipd.cmdline import main
from pyicloud.cmdline import main

if __name__ == "__main__":
main()
47 changes: 36 additions & 11 deletions icloudpd/authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import sys
import click
import pyicloud_ipd
import pyicloud
from pyicloud.exceptions import PyiCloudNoStoredPasswordAvailableException
from icloudpd.logger import setup_logger


Expand All @@ -18,36 +19,60 @@ def authenticate(
password,
cookie_directory=None,
raise_error_on_2sa=False,
client_id=None
client_id=None,
china_mainland=False,
):
"""Authenticate with iCloud username and password"""
logger = setup_logger()
logger.debug("Authenticating...")
try:
# If password not provided on command line variable will be set to None
# and PyiCloud will attempt to retrieve from its keyring
icloud = pyicloud_ipd.PyiCloudService(
# and PyiCloud will attempt to retrieve from it's keyring
icloud = pyicloud.PyiCloudService(
username, password,
cookie_directory=cookie_directory,
client_id=client_id)
except pyicloud_ipd.exceptions.NoStoredPasswordAvailable:
client_id=client_id,
china_mainland=china_mainland,
)
except PyiCloudNoStoredPasswordAvailableException:
# Prompt for password if not stored in PyiCloud's keyring
password = click.prompt("iCloud Password", hide_input=True)
icloud = pyicloud_ipd.PyiCloudService(
icloud = pyicloud.PyiCloudService(
username, password,
cookie_directory=cookie_directory,
client_id=client_id)

if icloud.requires_2sa:
if icloud.requires_2fa:
if raise_error_on_2sa:
raise TwoStepAuthRequiredError(
"Two-factor authentication is required!"
)
logger.info("Two-factor authentication is required!")
request_2fa(icloud, logger)
elif icloud.requires_2sa:
if raise_error_on_2sa:
raise TwoStepAuthRequiredError(
"Two-step/two-factor authentication is required!"
"Two-step authentication is required!"
)
logger.info("Two-step/two-factor authentication is required!")
logger.info("Two-step authentication is required!")
request_2sa(icloud, logger)
return icloud


def request_2fa(icloud, logger):
"""Request two-factor authentication."""
code = click.prompt("Please enter two-factor authentication code")
if not icloud.validate_2fa_code(code):
logger.error("Failed to verify two-factor authentication code")
sys.exit(1)
logger.info(
"Great, you're all set up. The script can now be run without "
"user interaction until 2FA expires.\n"
"You can set up email notifications for when "
"the two-factor authentication expires.\n"
"(Use --help to view information about SMTP options.)"
)


def request_2sa(icloud, logger):
"""Request two-step authentication. Prompts for SMS or device"""
devices = icloud.trusted_devices
Expand Down
10 changes: 8 additions & 2 deletions icloudpd/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from tqdm import tqdm
from tzlocal import get_localzone

from pyicloud_ipd.exceptions import PyiCloudAPIResponseError
from pyicloud.exceptions import PyiCloudAPIResponseException

from icloudpd.logger import setup_logger
from icloudpd.authentication import authenticate, TwoStepAuthRequiredError
Expand Down Expand Up @@ -50,6 +50,10 @@
"(default: use PyiCloud keyring or prompt for password)",
metavar="<password>",
)
@click.option("--china-mainland",
help="The country/region setting of your Apple ID is China mainland(中国大陆)",
is_flag=True,
)
@click.option(
"--cookie-directory",
help="Directory to store cookies for authentication "
Expand Down Expand Up @@ -200,6 +204,7 @@ def main(
directory,
username,
password,
china_mainland,
cookie_directory,
size,
live_photo_size,
Expand Down Expand Up @@ -258,6 +263,7 @@ def main(
cookie_directory,
raise_error_on_2sa,
client_id=os.environ.get("CLIENT_ID"),
china_mainland=china_mainland,
)
except TwoStepAuthRequiredError:
if notification_script is not None:
Expand All @@ -279,7 +285,7 @@ def main(
# case exit.
try:
photos = icloud.photos.albums[album]
except PyiCloudAPIResponseError as err:
except PyiCloudAPIResponseException as err:
# For later: come up with a nicer message to the user. For now take the
# exception text
print(err)
Expand Down
4 changes: 2 additions & 2 deletions icloudpd/download.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import logging
from tzlocal import get_localzone
from requests.exceptions import ConnectionError # pylint: disable=redefined-builtin
from pyicloud_ipd.exceptions import PyiCloudAPIResponseError
from pyicloud.exceptions import PyiCloudAPIResponseException
from icloudpd.logger import setup_logger

# Import the constants object so that we can mock WAIT_SECONDS in tests
Expand Down Expand Up @@ -66,7 +66,7 @@ def download_media(icloud, photo, download_path, size):
)
break

except (ConnectionError, socket.timeout, PyiCloudAPIResponseError) as ex:
except (ConnectionError, socket.timeout, PyiCloudAPIResponseException) as ex:
if "Invalid global session" in str(ex):
logger.tqdm_write(
"Session error, re-authenticating...",
Expand Down
4 changes: 2 additions & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
pyicloud_ipd==0.10.1
pyicloud==1.0.0
schema==0.7.5
click==6.7
click==7.1.2
python_dateutil==2.8.2
requests==2.28.2
tqdm==4.64.1
Expand Down
4 changes: 2 additions & 2 deletions tests/test_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from vcr import VCR
import pytest
from click.testing import CliRunner
import pyicloud_ipd
import pyicloud
from icloudpd.base import main
from icloudpd.authentication import authenticate, TwoStepAuthRequiredError
import inspect
Expand All @@ -19,7 +19,7 @@ def inject_fixtures(self, caplog):
def test_failed_auth(self):
with vcr.use_cassette("tests/vcr_cassettes/failed_auth.yml"):
with self.assertRaises(
pyicloud_ipd.exceptions.PyiCloudFailedLoginException
pyicloud.exceptions.PyiCloudFailedLoginException
) as context:
authenticate(
"bad_username",
Expand Down
6 changes: 3 additions & 3 deletions tests/test_download_live_photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from click.testing import CliRunner
import piexif
from piexif._exceptions import InvalidImageDataError
from pyicloud_ipd.services.photos import PhotoAsset, PhotoAlbum, PhotosService
from pyicloud_ipd.base import PyiCloudService
from pyicloud_ipd.exceptions import PyiCloudAPIResponseError
from pyicloud.services.photos import PhotoAsset, PhotoAlbum, PhotosService
from pyicloud.base import PyiCloudService
from pyicloud.exceptions import PyiCloudAPIResponseException
from requests.exceptions import ConnectionError
from icloudpd.base import main
from tests.helpers.print_result_exception import print_result_exception
Expand Down
12 changes: 6 additions & 6 deletions tests/test_download_photos.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
from click.testing import CliRunner
import piexif
from piexif._exceptions import InvalidImageDataError
from pyicloud_ipd.services.photos import PhotoAsset, PhotoAlbum, PhotosService
from pyicloud_ipd.base import PyiCloudService
from pyicloud_ipd.exceptions import PyiCloudAPIResponseError
from pyicloud.services.photos import PhotoAsset, PhotoAlbum, PhotosService
from pyicloud.base import PyiCloudService
from pyicloud.exceptions import PyiCloudAPIResponseException
from requests.exceptions import ConnectionError
from icloudpd.base import main
from tests.helpers.print_result_exception import print_result_exception
Expand Down Expand Up @@ -454,7 +454,7 @@ def test_handle_session_error_during_download(self):
# Pass fixed client ID via environment variable

def mock_raise_response_error(arg):
raise PyiCloudAPIResponseError("Invalid global session", 100)
raise PyiCloudAPIResponseException("Invalid global session", 100)

with mock.patch("time.sleep") as sleep_mock:
with mock.patch.object(PhotoAsset, "download") as pa_download:
Expand Down Expand Up @@ -522,7 +522,7 @@ def test_handle_session_error_during_photo_iteration(self):
# Pass fixed client ID via environment variable

def mock_raise_response_error(offset):
raise PyiCloudAPIResponseError("Invalid global session", 100)
raise PyiCloudAPIResponseException("Invalid global session", 100)

with mock.patch("time.sleep") as sleep_mock:
with mock.patch.object(PhotoAlbum, "photos_request") as pa_photos_request:
Expand Down Expand Up @@ -655,7 +655,7 @@ def test_handle_albums_error(self):
# Pass fixed client ID via environment variable

def mock_raise_response_error():
raise PyiCloudAPIResponseError("Api Error", 100)
raise PyiCloudAPIResponseException("Api Error", 100)

with mock.patch.object(PhotosService, "_fetch_folders") as pa_photos_request:
pa_photos_request.side_effect = mock_raise_response_error
Expand Down
2 changes: 1 addition & 1 deletion tests/test_two_step_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import click
from click.testing import CliRunner
from icloudpd.base import main
from pyicloud_ipd import PyiCloudService
from pyicloud import PyiCloudService
import inspect
import shutil

Expand Down