Compare commits

..

No commits in common. "127774a1de5bcd9c24d193c3d9f6fa8446f40437" and "1744e3bbdf9a3f87e897ad25ae47b18d04703f97" have entirely different histories.

4 changed files with 102 additions and 26 deletions

View File

@ -44,18 +44,10 @@ Run `icloud` program a first time to ensure everything is running smooth:
### Using 2FA ### Using 2FA
If you enabled 2FA on your Apple iCloud account, you can request the 2FA code If you enabled 2FA on your Apple iCloud account, the script will run a simple
beforehand by : webserver and point you to the URL on localhost to pass it the 2FA code you
If your device is online: will get from one of your iDevices. This makes the script trustable for some
Go to Settings > [your name]. time and this operation is only required once in a while (every month or so).
Tap Sign-In & Security > Two Factor Authentication.
Tap Get Verification Code
If your device is offline:
Go to Settings > [your name].
Tap Sign-In & Security.
A message says "Account Details Unavailable." Tap Get Verification Code.
Put this 2FA code in the config.ini file, you have one minute to run the command line in the next section.
## Usage ## Usage

View File

@ -1,11 +1,13 @@
[apple] [apple]
email = apple_icloud_email email = apple_icloud_email
password = apple_icloud_password password = apple_icloud_password
code_2fa = 2fa_code
iPhone_name = iPhone
cookie_directory = COOKIE_DIRECTORY cookie_directory = COOKIE_DIRECTORY
[nextcloud] [nextcloud]
server = https://cloud.example.com server = https://cloud.example.com
user = nextcloud_user user = nextcloud_user
password = nextcloud_password password = nextcloud_password
[webserver]
host = localhost
port = 8080

View File

@ -4,12 +4,78 @@ import logging
import sys import sys
import urllib.parse import urllib.parse
import bottle
import requests import requests
from pyicloud import PyiCloudService from pyicloud import PyiCloudService
from requests.auth import HTTPBasicAuth from requests.auth import HTTPBasicAuth
class StoppableCherootServer(bottle.ServerAdapter):
"""
We need a stoppable HTTP server, which can be stopped from within a route.
This is not doable out of the box in bottle and is quite hacky using plain
WSGIRef. This is easier and cleaner with Cheroot (formally CherryPy)
backend.
"""
def run(self, handler): # pragma: no cover
from cheroot import wsgi
self.options['bind_addr'] = (self.host, self.port)
self.options['wsgi_app'] = handler
self.server = wsgi.Server(**self.options)
try:
self.server.start()
finally:
self.server.stop()
############################################
# Web app to fetch 2FA code from the user. #
############################################
code_2fa = None # Global for passing 2FA code from web app to main script
app = bottle.Bottle()
server = None
@app.route('/')
def get_2fa():
"""
Main HTTP route, display an HTML form to fetch 2FA code from user.
"""
return """
<!doctype html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>iCloud 2FA protection</title>
</head>
<body>
<form method="post" action="/2fa">
<p>
<label for="2FA">2FA password?</label>
<input type="text" id="2FA" name="2FA"/>
</p>
<input type="submit"/>
</form>
</body>
</html>"""
@app.post('/2fa')
def set_2fa():
"""
Handle form submission and store 2FA code to pass along the rest of the
code.
"""
global code_2fa
global server
code_2fa = bottle.request.forms.get('2FA')
server.server.stop()
return "OK"
############### ###############
# Main script # # Main script #
############### ###############
@ -29,16 +95,23 @@ def get_icloud_location(config):
""" """
Fetch latest iPhone location from iCloud Fetch latest iPhone location from iCloud
""" """
global server
global code_2fa
email = config['apple']['email'] email = config['apple']['email']
password = config['apple']['password'] password = config['apple']['password']
code_2fa = config['apple']['code_2fa']
api = PyiCloudService(email, password=password, cookie_directory=config['apple']['cookie_directory']) api = PyiCloudService(email, password=password, cookie_directory=config['apple']['cookie_directory'])
if api.requires_2fa: if api.requires_2fa:
print("Two-factor authentication required.") print(
code = code_2fa "Two-factor authentication required. "
result = api.validate_2fa_code(code) f"Head over to http://{config['webserver']['host']}:{config['webserver']['port']} and fill-in the 2FA code."
)
server = StoppableCherootServer(
host=config['webserver']['host'],
port=int(config['webserver']['port'])
)
app.run(server=server)
result = api.validate_2fa_code(code_2fa)
print("Code validation result: %s" % result) print("Code validation result: %s" % result)
if not result: if not result:
@ -51,7 +124,11 @@ def get_icloud_location(config):
print("Session trust result %s" % result) print("Session trust result %s" % result)
if not result: if not result:
print("Failed to request trust. You will likely be prompted for the code again in the coming weeks") print(
"Failed to request trust. "
"You will likely be prompted for the code again "
"in the coming weeks"
)
elif api.requires_2sa: elif api.requires_2sa:
import click import click
print("Two-step authentication required. Your trusted devices are:") print("Two-step authentication required. Your trusted devices are:")
@ -59,8 +136,11 @@ def get_icloud_location(config):
devices = api.trusted_devices devices = api.trusted_devices
for i, device in enumerate(devices): for i, device in enumerate(devices):
print( print(
" %s: %s" % (i, device.get('deviceName', " %s: %s" % (
"SMS to %s" % device.get('phoneNumber'))) i, device.get(
'deviceName', "SMS to %s" % device.get('phoneNumber')
)
)
) )
device = click.prompt('Which device would you like to use?', default=0) device = click.prompt('Which device would you like to use?', default=0)
@ -75,10 +155,10 @@ def get_icloud_location(config):
sys.exit(1) sys.exit(1)
iphone = next( iphone = next(
device device
for device in api.devices for device in api.devices
if config['apple']['iPhone_name'] in device.status()['name'] if 'iPhone' in device.status()['name']
) )
iphone_location = iphone.location() iphone_location = iphone.location()
iphone_status = iphone.status() iphone_status = iphone.status()

View File

@ -1 +1,3 @@
bottle
cheroot
pyicloud pyicloud