diff --git a/README.rst b/README.rst index 8d7a8791..397e5b36 100644 --- a/README.rst +++ b/README.rst @@ -34,6 +34,41 @@ PyiCloud is a module which allows pythonistas to interact with iCloud webservice At its core, PyiCloud connects to iCloud using your username and password, then performs calendar and iPhone queries against their API. +.. note:: + + **Linux users:** pyicloud fails out-of-the-box on Linux due to Apple returning + HTTP 421 on the session validation endpoint. This fork includes a fix. + See `Known Issues & Fixes`_ for the explanation and + `Mounting iCloud Drive on Linux (FUSE)`_ for a full setup guide. + + +Known Issues & Fixes +==================== + +HTTP 421 "Misdirected Request" on Linux (non-Apple networks) +------------------------------------------------------------- + +When using pyicloud from Linux or any non-Apple network, the initial call to +``setup.icloud.com/validate`` may return HTTP 421 (Misdirected Request). +This is caused by Apple's HTTP/2 connection coalescing behavior — the server +routes the TCP connection to a different virtual host than expected. It is +**not a real authentication failure**. + +Prior to this fix, pyicloud would treat 421 as an invalid session token and +attempt a full re-login with username + password. This re-login then hit +``idmsa.apple.com`` which returns 503 (rate-limiting), causing a +``PyiCloudFailedLoginException`` even when the session was perfectly valid. + +**The fix** (in ``authenticate()``): when 421 is returned from ``/validate``, +fall through to ``_authenticate_with_token()`` which uses the existing +``session_token`` + ``trust_token`` to call ``/accountLogin`` directly. +This succeeds without requiring a password or 2FA, and without triggering +Apple's rate-limiting on the auth endpoint. + +The fix was confirmed working on: + +- Arch Linux (kernel 6.18, Python 3.14) +- pyicloud 1.0.0 + iCloudDriveFuse FUSE driver Authentication ============== @@ -412,6 +447,137 @@ To download a specific version of the photo asset, pass the version to ``downloa thumb_file.write(download.raw.read()) +Mounting iCloud Drive on Linux (FUSE) +===================================== + +You can mount iCloud Drive as a local filesystem on Linux using +`iCloudDriveFuse `_. + +Prerequisites +------------- + +.. code-block:: console + + # Arch Linux + sudo pacman -S fuse3 python-cachetools + yay -S python-pyicloud-git python-fusepy + + # Debian/Ubuntu (adapt package names as needed) + sudo apt install fuse3 python3-cachetools + pip install pyicloud fusepy + +Step 1 — Authenticate and save session cookies +----------------------------------------------- + +Run the ``icloud`` CLI tool once to authenticate and persist the session: + +.. code-block:: console + + icloud --username your@apple.id + +Enter your Apple ID password when prompted. If 2FA is enabled, approve the +push notification on your Apple device and enter the 6-digit code. + +The session cookies are saved to ``/tmp/pyicloud//`` by default. +Move them to a persistent location so they survive reboots: + +.. code-block:: console + + mkdir -p ~/.config/pyicloud + chmod 700 ~/.config/pyicloud + cp /tmp/pyicloud/$(whoami)/* ~/.config/pyicloud/ + +Step 2 — Store credentials in .netrc +------------------------------------- + +.. code-block:: console + + touch ~/.netrc && chmod 600 ~/.netrc + +Add the following to ``~/.netrc`` (replace with your Apple ID and password): + +.. code-block:: text + + machine icloud + login your@apple.id + password yourpassword + +Step 3 — Clone iCloudDriveFuse and patch cookie directory +---------------------------------------------------------- + +.. code-block:: console + + git clone https://github.com/ixs/iCloudDriveFuse.git ~/iCloudDriveFuse + +Edit ``~/iCloudDriveFuse/iCloudDriveFuse.py`` and change the ``__init__`` +method to pass ``cookie_directory`` pointing to your persistent store: + +.. code-block:: python + + def __init__(self): + self.username, _, self.password = netrc.netrc().authenticators("icloud") + cookie_dir = os.path.expanduser("~/.config/pyicloud") + self._api = PyiCloudService(self.username, self.password, cookie_directory=cookie_dir) + +Step 4 — Mount +-------------- + +.. code-block:: console + + mkdir -p ~/iCloud + python ~/iCloudDriveFuse/iCloudDriveFuse.py ~/iCloud + +Step 5 — Auto-mount via systemd (optional) +------------------------------------------- + +Create ``~/.config/systemd/user/icloud-drive.service``: + +.. code-block:: ini + + [Unit] + Description=iCloud Drive FUSE Mount + After=network-online.target + Wants=network-online.target + + [Service] + Type=simple + ExecStart=/usr/bin/python /home/YOUR_USER/iCloudDriveFuse/iCloudDriveFuse.py /home/YOUR_USER/iCloud + ExecStop=/usr/bin/fusermount -u /home/YOUR_USER/iCloud + Restart=on-failure + RestartSec=10 + + [Install] + WantedBy=default.target + +Then enable it: + +.. code-block:: console + + systemctl --user daemon-reload + systemctl --user enable --now icloud-drive.service + +Maintenance +----------- + +Apple trust tokens expire approximately every two months. When the mount +stops working, re-run the authentication step: + +.. code-block:: console + + icloud --username your@apple.id + cp /tmp/pyicloud/$(whoami)/* ~/.config/pyicloud/ + systemctl --user restart icloud-drive.service + +Note on HTTP 421 errors +----------------------- + +If you see ``Authentication required for Account. (421)`` in the logs, this +is the issue described in the "Known Issues" section above. Make sure you are +using a version of pyicloud that includes the 421 fix in ``authenticate()``. +The fix is included in this fork. See ``pyicloud/base.py:authenticate()`` for +the implementation. + + Code samples ============ diff --git a/pyicloud/base.py b/pyicloud/base.py index 81c91807..4d6b7169 100644 --- a/pyicloud/base.py +++ b/pyicloud/base.py @@ -294,8 +294,27 @@ def authenticate(self, force_refresh=False, service=None): try: self.data = self._validate_token() login_successful = True - except PyiCloudAPIResponseException: - LOGGER.debug("Invalid authentication token, will log in from scratch.") + except PyiCloudAPIResponseException as err: + if getattr(err, "code", None) == 421: + # Apple's setup.icloud.com/validate returns HTTP 421 + # (Misdirected Request) for non-Apple IP connections due to + # HTTP/2 connection coalescing. This is not a real auth + # failure — the existing session_token and trust_token are + # still valid. Fall through to token-based re-auth via + # /accountLogin instead of forcing a full password login. + LOGGER.debug( + "Got 421 on validate (Apple HTTP/2 coalescing issue), " + "attempting token-based re-auth" + ) + try: + self._authenticate_with_token() + login_successful = True + except Exception: + LOGGER.debug( + "Token re-auth failed, will log in from scratch." + ) + else: + LOGGER.debug("Invalid authentication token, will log in from scratch.") if not login_successful and service is not None: app = self.data["apps"][service]