pyicloud/pyicloud/cmdline.py
Adam Coddington 2f0dcd1ac2 [#64] Adds Keychain-based Authentication
Squashed commit of the following:

commit 0eb23aa87c264152716933e03827f040742e6d70
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Feb 20 14:21:48 2016 -0800

    Updating readme to reflect updated flow.

commit 840268e2db6093b5cb573c6a3e71204bf5b08b48
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Feb 20 14:18:39 2016 -0800

    Dropping python 2.6 support workaround.

commit 9dcbd460482c2925bda490be2be884a2a2526062
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Feb 20 14:18:00 2016 -0800

    Adding additional behavior at @torarnv's request.

commit 6c711bb12beea7c792b5d386203373423b6e56e2
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 15:08:29 2016 -0800

    Workaround for obsolete versions of Python 2.

commit b0765b7b6bf9974348061043da9a110c6bd7d985
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 14:56:53 2016 -0800

    Style changes to avoid line length overage.

commit 4decc576432ef23edae01b9621f2689b4f3c6c84
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 14:01:27 2016 -0800

    Adding documentation; also adding --delete-from-keyring command-line option.

commit a6b0224e93a8bc9159cf06ba5792a384f7fbb060
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 13:44:09 2016 -0800

    Adding functionality allowing authentication using iCloud passwords stored in the system keychain.

    Adds the following new command-line options:

    * `--password-interactive`: Allows you to specify your password
      interactively rather than typing it into the command-line.
    * `--store-in-keychain`: Allows you to store the password in use in the
      system keychain.

    If no password is specified when instantiating `PyiCloudService` or when
    using the command-line utility (via either `--password-interactive` or
    `--password`), the system keychain will be queried for a stored
    password, and an exception will be raised if one was not found.

commit 4ba03fb02d51673dfb7183dde49ab4c0bec4afb3
Author: Adam Coddington <me@adamcoddington.net>
Date:   Sat Jan 23 13:43:39 2016 -0800

    Removing unused imports.
2016-02-23 17:44:03 -08:00

328 lines
9.7 KiB
Python

#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""
A Command Line Wrapper to allow easy use of pyicloud for
command line scripts, and related.
"""
from __future__ import print_function
import argparse
import pickle
import sys
from click import confirm
import pyicloud
from . import utils
DEVICE_ERROR = (
"Please use the --device switch to indicate which device to use."
)
def create_pickled_data(idevice, filename):
"""This helper will output the idevice to a pickled file named
after the passed filename.
This allows the data to be used without resorting to screen / pipe
scrapping. """
data = {}
for x in idevice.content:
data[x] = idevice.content[x]
location = filename
pickle_file = open(location, 'wb')
pickle.dump(data, pickle_file, protocol=pickle.HIGHEST_PROTOCOL)
pickle_file.close()
def main(args=None):
"""Main commandline entrypoint"""
if args is None:
args = sys.argv[1:]
parser = argparse.ArgumentParser(
description="Find My iPhone CommandLine Tool")
parser.add_argument(
"--username",
action="store",
dest="username",
default="",
help="Apple ID to Use"
)
parser.add_argument(
"--password",
action="store",
dest="password",
default="",
help=(
"Apple ID Password to Use; if unspecified, password will be "
"fetched from the system keyring."
)
)
parser.add_argument(
"-n",
"--non-interactive",
action="store_false",
dest="interactive",
default=True,
help="Disable interactive prompts."
)
parser.add_argument(
"--delete-from-keyring",
action="store_true",
dest="delete_from_keyring",
default=False,
help="Delete stored password in system keyring for this username.",
)
parser.add_argument(
"--list",
action="store_true",
dest="list",
default=False,
help="Short Listings for Device(s) associated with account",
)
parser.add_argument(
"--llist",
action="store_true",
dest="longlist",
default=False,
help="Detailed Listings for Device(s) associated with account",
)
parser.add_argument(
"--locate",
action="store_true",
dest="locate",
default=False,
help="Retrieve Location for the iDevice (non-exclusive).",
)
# Restrict actions to a specific devices UID / DID
parser.add_argument(
"--device",
action="store",
dest="device_id",
default=False,
help="Only effect this device",
)
# Trigger Sound Alert
parser.add_argument(
"--sound",
action="store_true",
dest="sound",
default=False,
help="Play a sound on the device",
)
# Trigger Message w/Sound Alert
parser.add_argument(
"--message",
action="store",
dest="message",
default=False,
help="Optional Text Message to display with a sound",
)
# Trigger Message (without Sound) Alert
parser.add_argument(
"--silentmessage",
action="store",
dest="silentmessage",
default=False,
help="Optional Text Message to display with no sounds",
)
# Lost Mode
parser.add_argument(
"--lostmode",
action="store_true",
dest="lostmode",
default=False,
help="Enable Lost mode for the device",
)
parser.add_argument(
"--lostphone",
action="store",
dest="lost_phone",
default=False,
help="Phone Number allowed to call when lost mode is enabled",
)
parser.add_argument(
"--lostpassword",
action="store",
dest="lost_password",
default=False,
help="Forcibly active this passcode on the idevice",
)
parser.add_argument(
"--lostmessage",
action="store",
dest="lost_message",
default="",
help="Forcibly display this message when activating lost mode.",
)
# Output device data to an pickle file
parser.add_argument(
"--outputfile",
action="store_true",
dest="output_to_file",
default="",
help="Save device data to a file in the current directory.",
)
command_line = parser.parse_args(args)
username = command_line.username
password = command_line.password
if username and command_line.delete_from_keyring:
utils.delete_password_in_keyring(username)
failure_count = 0
while True:
# Which password we use is determined by your username, so we
# do need to check for this first and separately.
if not username:
parser.error('No username supplied')
if not password:
password = utils.get_password(
username,
interactive=command_line.interactive
)
if not password:
parser.error('No password supplied')
try:
api = pyicloud.PyiCloudService(
username.strip(),
password.strip()
)
if (
not utils.password_exists_in_keyring(username) and
command_line.interactive and
confirm("Save password in keyring? ")
):
utils.store_password_in_keyring(username, password)
break
except pyicloud.exceptions.PyiCloudFailedLoginException:
# If they have a stored password; we just used it and
# it did not work; let's delete it if there is one.
if utils.password_exists_in_keyring(username):
utils.delete_password_in_keyring(username)
message = "Bad username or password for {username}".format(
username=username,
)
password = None
failure_count += 1
if failure_count >= 3:
raise RuntimeError(message)
print(message, file=sys.stderr)
for dev in api.devices:
if (
not command_line.device_id or
(
command_line.device_id.strip().lower() ==
dev.content["id"].strip().lower()
)
):
# List device(s)
if command_line.locate:
dev.location()
if command_line.output_to_file:
create_pickled_data(
dev,
filename=(
dev.content["name"].strip().lower() + ".fmip_snapshot"
)
)
contents = dev.content
if command_line.longlist:
print("-"*30)
print(contents["name"])
for x in contents:
print("%20s - %s" % (x, contents[x]))
elif command_line.list:
print("-"*30)
print("Name - %s" % contents["name"])
print("Display Name - %s" % contents["deviceDisplayName"])
print("Location - %s" % contents["location"])
print("Battery Level - %s" % contents["batteryLevel"])
print("Battery Status- %s" % contents["batteryStatus"])
print("Device Class - %s" % contents["deviceClass"])
print("Device Model - %s" % contents["deviceModel"])
# Play a Sound on a device
if command_line.sound:
if command_line.device_id:
dev.play_sound()
else:
raise RuntimeError(
"\n\n\t\t%s %s\n\n" % (
"Sounds can only be played on a singular device.",
DEVICE_ERROR
)
)
# Display a Message on the device
if command_line.message:
if command_line.device_id:
dev.display_message(
subject='A Message',
message=command_line.message,
sounds=True
)
else:
raise RuntimeError(
"%s %s" % (
"Messages can only be played "
"on a singular device.",
DEVICE_ERROR
)
)
# Display a Silent Message on the device
if command_line.silentmessage:
if command_line.device_id:
dev.display_message(
subject='A Silent Message',
message=command_line.silentmessage,
sounds=False
)
else:
raise RuntimeError(
"%s %s" % (
"Silent Messages can only be played "
"on a singular device.",
DEVICE_ERROR
)
)
# Enable Lost mode
if command_line.lostmode:
if command_line.device_id:
dev.lost_device(
number=command_line.lost_phone.strip(),
text=command_line.lost_message.strip(),
newpasscode=command_line.lost_password.strip()
)
else:
raise RuntimeError(
"%s %s" % (
"Lost Mode can only be activated "
"on a singular device.",
DEVICE_ERROR
)
)
if __name__ == '__main__':
main()