Add a web interface for passing 2FA code
This commit is contained in:
parent
0d7da04edd
commit
29fa0ea435
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
config.ini*
|
config.ini*
|
||||||
|
__pycache__
|
||||||
|
@ -4,12 +4,82 @@ 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 #
|
||||||
|
###############
|
||||||
|
|
||||||
def load_config(config_str=None):
|
def load_config(config_str=None):
|
||||||
"""
|
"""
|
||||||
Load and parse config from string provided. Defaults to reading from stdin.
|
Load and parse config from string provided. Defaults to reading from stdin.
|
||||||
@ -25,16 +95,20 @@ 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']
|
||||||
api = PyiCloudService(email, password)
|
api = PyiCloudService(email, password)
|
||||||
|
|
||||||
if api.requires_2fa:
|
if api.requires_2fa:
|
||||||
print("Two-factor authentication required.")
|
print(
|
||||||
code = input(
|
"Two-factor authentication required. "
|
||||||
"Enter the code you received of one of your approved devices: "
|
"Head over to http://localhost:8080 and fill-in the 2FA code."
|
||||||
)
|
)
|
||||||
result = api.validate_2fa_code(code)
|
server = StoppableCherootServer(port=8080)
|
||||||
|
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:
|
||||||
@ -92,6 +166,10 @@ def store_location_in_nextcloud(config, iphone_location, iphone_status):
|
|||||||
"""
|
"""
|
||||||
Store provided iPhone location to Nextcloud.
|
Store provided iPhone location to Nextcloud.
|
||||||
"""
|
"""
|
||||||
|
if iphone_location is None:
|
||||||
|
print('Could not retrieved iPhone location. Try again.')
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
nextcloud_location_args = {
|
nextcloud_location_args = {
|
||||||
"user_agent": iphone_status['name'],
|
"user_agent": iphone_status['name'],
|
||||||
"lat": iphone_location['latitude'],
|
"lat": iphone_location['latitude'],
|
||||||
|
@ -1 +1,3 @@
|
|||||||
|
bottle
|
||||||
|
cheroot
|
||||||
pyicloud
|
pyicloud
|
||||||
|
Loading…
Reference in New Issue
Block a user