diff --git a/cdn/static/web/launcher/profile.html b/cdn/static/web/launcher/profile.html new file mode 100644 index 0000000..28b3dfd --- /dev/null +++ b/cdn/static/web/launcher/profile.html @@ -0,0 +1,38 @@ +{% extends "./layout.html" %} +{% block content %} +

Download Zwift profile

+

Logged in as {{ username }}

+
+
+
+
+
+ + +
+
+ + +
+
+
+
+ + Back +
+
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} +
+
+{% endblock %} diff --git a/cdn/static/web/launcher/upload.html b/cdn/static/web/launcher/upload.html index c05761e..50cf980 100644 --- a/cdn/static/web/launcher/upload.html +++ b/cdn/static/web/launcher/upload.html @@ -2,7 +2,7 @@ {% block content %}

Logged in as {{ username }} ({{ name }})

-
+
    {% if profile %}
  • @@ -10,6 +10,7 @@
      
    Download
    +   
    Delete
@@ -19,6 +20,7 @@
+   
Delete
@@ -28,21 +30,31 @@
+   
Delete
{% endif %} - + {% if zwift %} +
  • +
    +
    + +   
    Delete
    +
    +
    +
  • + {% endif %}
    -
    +

    Upload file

    - +
    diff --git a/cdn/static/web/launcher/user_home.html b/cdn/static/web/launcher/user_home.html index 3170543..812f590 100644 --- a/cdn/static/web/launcher/user_home.html +++ b/cdn/static/web/launcher/user_home.html @@ -15,6 +15,10 @@
    Upload Change password + Strava auth + {% if server_ip != "localhost" and server_ip != "127.0.0.1" %} + Get Zwift profile + {% endif %} Logout {% if is_admin and not restarting %} Restart server diff --git a/scripts/strava_auth.py b/scripts/strava_auth.py index 9043ac1..5210e47 100755 --- a/scripts/strava_auth.py +++ b/scripts/strava_auth.py @@ -62,10 +62,11 @@ class RequestHandler(BaseHTTPRequestHandler): if request_path.startswith('/authorization'): self.send_response(200) - self.send_header(six.b("Content-type"), six.b("text/plain")) + self.send_header("Content-Type", "application/octet-stream") + self.send_header("Content-Disposition", "attachment; filename=strava_token.txt") self.end_headers() - self.wfile.write(six.b("Authorization Handler\n\n")) + #self.wfile.write(six.b("Authorization Handler\n\n")) code = urlparse.parse_qs(parsed_path.query).get('code') if code: code = code[0] @@ -75,22 +76,18 @@ class RequestHandler(BaseHTTPRequestHandler): access_token = token_response['access_token'] refresh_token = token_response['refresh_token'] expires_at = token_response['expires_at'] - self.server.logger.info("Exchanged code {} for access token {}".format(code, access_token)) - self.wfile.write(six.b("Access Token: {}\n".format(access_token))) - self.wfile.write(six.b("Refresh Token: {}\n".format(refresh_token))) - self.wfile.write(six.b("Expires at: {}\n".format(expires_at))) - with open('%s/strava_token.txt' % SCRIPT_DIR, 'w') as f: - f.write(self.server.client_id + '\n'); - f.write(self.server.client_secret + '\n'); - f.write(access_token + '\n'); - f.write(refresh_token + '\n'); - f.write(str(expires_at) + '\n'); + + self.wfile.write(six.b("{}\n".format(self.server.client_id))) + self.wfile.write(six.b("{}\n".format(self.server.client_secret))) + self.wfile.write(six.b("{}\n".format(access_token))) + self.wfile.write(six.b("{}\n".format(refresh_token))) + self.wfile.write(six.b("{}\n".format(expires_at))) else: self.server.logger.error("No code param received.") self.wfile.write(six.b("ERROR: No code param recevied.\n")) else: url = client.authorization_url(client_id=self.server.client_id, - redirect_uri='http://localhost:{}/authorization'.format(self.server.server_port), + redirect_uri='http://18.133.120.5:{}/authorization'.format(self.server.server_port), scope='activity:write') self.send_response(302) @@ -119,9 +116,9 @@ if __name__ == "__main__": action='store', type=int, default=8000) parser.add_argument('--client-id', help='Strava API Client ID', - action='store', default='28117') + action='store', default='65392') parser.add_argument('--client-secret', help='Strava API Client Secret', - action='store', default='41b7b7b76d8cfc5dc12ad5f020adfea17da35468') + action='store', default='c6a2af6ebb6306d9b6c9974356bf63be80659ac8') args = parser.parse_args() main(port=args.port, client_id=args.client_id, client_secret=args.client_secret) diff --git a/zwift_offline.py b/zwift_offline.py index 44c0694..1adda9b 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -14,6 +14,13 @@ import math import threading import re import smtplib, ssl + +import subprocess +import requests +import protobuf.activity_pb2 as activity_pb2 +import protobuf.profile_pb2 as profile_pb2 +import scripts.online_sync as online_sync + from copy import copy from functools import wraps from io import BytesIO @@ -44,6 +51,7 @@ import protobuf.zfiles_pb2 as zfiles_pb2 import protobuf.hash_seeds_pb2 as hash_seeds_pb2 import protobuf.events_pb2 as events_pb2 + logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) logger = logging.getLogger('zoffline') logger.setLevel(logging.DEBUG) @@ -92,7 +100,7 @@ else: SECRET_KEY_FILE = "%s/secret-key.txt" % STORAGE_DIR ENABLEGHOSTS_FILE = "%s/enable_ghosts.txt" % STORAGE_DIR MULTIPLAYER = False -garmin_key = None +credentials_key = None if os.path.exists("%s/multiplayer.txt" % STORAGE_DIR): MULTIPLAYER = True try: @@ -111,12 +119,12 @@ if os.path.exists("%s/multiplayer.txt" % STORAGE_DIR): logger.warn("cryptography is not installed. Uploaded garmin_credentials.txt will not be encrypted.") encrypt = False if encrypt: - GARMIN_KEY_FILE = "%s/garmin-key.txt" % STORAGE_DIR + GARMIN_KEY_FILE = "%s/credentials-key.txt" % STORAGE_DIR if not os.path.exists(GARMIN_KEY_FILE): with open(GARMIN_KEY_FILE, 'wb') as f: f.write(Fernet.generate_key()) with open(GARMIN_KEY_FILE, 'rb') as f: - garmin_key = f.read() + credentials_key = f.read() from tokens import * @@ -456,12 +464,42 @@ def reset(username): return render_template("reset.html", username=current_user.username) +@app.route("/profile//", methods=["GET", "POST"]) +@login_required +def profile(username): + if request.method == "POST": + if request.form['username'] == "" or request.form['password'] == "": + flash("Zwift credentials can't be empty") + return render_template("profile.html", username=current_user.username) + exit(-1); + + username = request.form['username'] + password = request.form['password'] + session = requests.session() + + try: + access_token, refresh_token = online_sync.login(session, username, password) + try: + profile = online_sync.query_player_profile(session, access_token) + with open('%s/profile.bin' % SCRIPT_DIR, 'wb') as f: + f.write(profile) + online_sync.logout(session, refresh_token) + player_id = current_user.player_id + profile_dir = '%s/%s' % (STORAGE_DIR, str(player_id)) + os.rename('%s/profile.bin' % SCRIPT_DIR, '%s/profile.bin' % profile_dir) + flash("Zwift profile installed locally.") + except: + flash("Error downloading profile") + except: + flash("Error invalid Username or password") + + return render_template("profile.html", username=current_user.username) @app.route("/user//") @login_required def user_home(username): return render_template("user_home.html", username=current_user.username, enable_ghosts=bool(current_user.enable_ghosts), - online=get_online(), is_admin=current_user.is_admin, restarting=restarting, restarting_in_minutes=restarting_in_minutes) + online=get_online(), is_admin=current_user.is_admin, restarting=restarting, restarting_in_minutes=restarting_in_minutes,server_ip=server_ip) def send_message_to_all_online(message, sender='Server'): @@ -553,19 +591,26 @@ def upload(username): except IOError as e: logger.error("failed to create profile dir (%s): %s", profile_dir, str(e)) return '', 500 - + if request.method == 'POST': uploaded_file = request.files['file'] - if uploaded_file.filename in ['profile.bin', 'strava_token.txt', 'garmin_credentials.txt']: + if uploaded_file.filename in ['profile.bin', 'strava_token.txt', 'garmin_credentials.txt', 'zwift_credentials.txt']: file_path = os.path.join(profile_dir, uploaded_file.filename) uploaded_file.save(file_path) - if uploaded_file.filename == 'garmin_credentials.txt' and garmin_key is not None: + if uploaded_file.filename == 'garmin_credentials.txt' and credentials_key is not None: with open(file_path, 'rb') as fr: garmin_credentials = fr.read() - cipher_suite = Fernet(garmin_key) + cipher_suite = Fernet(credentials_key) ciphered_text = cipher_suite.encrypt(garmin_credentials) with open(file_path, 'wb') as fw: fw.write(ciphered_text) + if uploaded_file.filename == 'zwift_credentials.txt' and credentials_key is not None: + with open(file_path, 'rb') as fr: + garmin_credentials = fr.read() + cipher_suite = Fernet(credentials_key) + ciphered_text = cipher_suite.encrypt(garmin_credentials) + with open(file_path, 'wb') as fw: + fw.write(ciphered_text) flash("File %s uploaded." % uploaded_file.filename) else: flash("Invalid file name.") @@ -590,8 +635,13 @@ def upload(username): if os.path.isfile(garmin_file): stat = os.stat(garmin_file) garmin = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime)) + zwift = None + zwift_file = os.path.join(profile_dir, 'zwift_credentials.txt') + if os.path.isfile(zwift_file): + stat = os.stat(zwift_file) + zwift = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(stat.st_mtime)) - return render_template("upload.html", username=current_user.username, profile=profile, name=name, token=token, garmin=garmin) + return render_template("upload.html", username=current_user.username, profile=profile, name=name, token=token, garmin=garmin, zwift=zwift) @app.route("/download/profile.bin", methods=["GET"]) @@ -603,6 +653,17 @@ def download(): if os.path.isfile(profile_file): return send_file(profile_file, attachment_filename='profile.bin') +@app.route("/delete/", methods=["GET"]) +@login_required +def delete(filename): + player_id = current_user.player_id + profile_dir = os.path.join(STORAGE_DIR, str(player_id)) + delete_file = os.path.join(profile_dir, filename) + if os.path.isfile(delete_file): + os.remove("%s" %delete_file) + return redirect(url_for('upload', username=current_user)) + + @app.route("/logout/") @login_required @@ -770,8 +831,7 @@ def api_events_search(): critccw_cat.startLocation = cat critccw_cat.label = cat - return events.SerializeToString(), 200 - + return '', 200 @app.route('/api/events/subgroups/signup/', methods=['POST']) def api_events_subgroups_signup_id(event_id): @@ -791,8 +851,6 @@ def api_events_subgroups_entrants_id(event_id): @app.route('/relay/race/event_starting_line/', methods=['POST']) def relay_race_event_starting_line_id(event_id): return '', 204 - - @app.route('/api/zfiles', methods=['POST']) def api_zfiles(): # Don't care about zfiles, but shuts up some errors in Zwift log. @@ -1030,7 +1088,6 @@ def strava_upload(player_id, activity): except: logger.warn("Strava upload failed. No internet?") - def garmin_upload(player_id, activity): try: from garmin_uploader.workflow import Workflow @@ -1040,8 +1097,8 @@ def garmin_upload(player_id, activity): profile_dir = '%s/%s' % (STORAGE_DIR, player_id) try: with open('%s/garmin_credentials.txt' % profile_dir, 'r') as f: - if garmin_key is not None: - cipher_suite = Fernet(garmin_key) + if credentials_key is not None: + cipher_suite = Fernet(credentials_key) ciphered_text = f.read() unciphered_text = (cipher_suite.decrypt(ciphered_text.encode(encoding='UTF-8'))) unciphered_text = unciphered_text.decode(encoding='UTF-8') @@ -1066,6 +1123,51 @@ def garmin_upload(player_id, activity): except: logger.warn("Garmin upload failed. No internet?") +def zwift_upload(player_id): + profile_dir = '%s/%s' % (STORAGE_DIR, player_id) + try: + with open('%s/zwift_credentials.txt' % profile_dir, 'r') as f: + if credentials_key is not None: + cipher_suite = Fernet(credentials_key) + ciphered_text = f.read() + unciphered_text = (cipher_suite.decrypt(ciphered_text.encode(encoding='UTF-8'))) + unciphered_text = unciphered_text.decode(encoding='UTF-8') + split_credentials = unciphered_text.splitlines() + username = split_credentials[0] + password = split_credentials[1] + else: + username = f.readline().rstrip('\r\n') + password = f.readline().rstrip('\r\n') + except: + logger.warn("Failed to read %s/zwift_credentials.txt. Skipping Zwift upload attempt." % profile_dir) + return + + try: + session = requests.session() + try: + activity = activity_pb2.Activity() + access_token, refresh_token = online_sync.login(session, username, password) + activity.player_id = online_sync.get_player_id(session, access_token) + player_id = current_user.player_id + profile_dir = '%s/%s' % (STORAGE_DIR, str(player_id)) + activity_file = '%s/last_activity.bin' % profile_dir + if not os.path.isfile(activity_file): + print('Activity file not found') + with open(activity_file, 'rb') as fd: + try: + activity.ParseFromString(fd.read()) + except: + print('Could not parse activity file') + res = online_sync.upload_activity(session, access_token, activity) + if res == 200: + logger.info("Zwift activity upload succesfull") + else: + logger.warn("Zwift activity upload failed:%s:" %res) + online_sync.logout(session, refresh_token) + except: + logger.warn("Error uploading activity to Zwift Server") + except: + logger.warn("Zwift upload failed. No internet?") # With 64 bit ids Zwift can pass negative numbers due to overflow, which the flask int # converter does not handle so it's a string argument @@ -1099,6 +1201,9 @@ def api_profiles_activities_id(player_id, activity_id): # For using with upload_activity.py (to upload zoffline activity to Zwift server) with open('%s/%s/last_activity.bin' % (STORAGE_DIR, player_id), 'wb') as f: f.write(activity.SerializeToString()) + if server_ip != "localhost" and server_ip != "127.0.0.1": + zwift_upload(player_id) + return response, 200 @app.route('/api/profiles//activities/0/rideon', methods=['POST']) #activity_id Seem to always be 0, even when giving ride on to ppl with 30km+