From e3207f52b75c8ffa48bb9d507e7ffdfc2beb0e6c Mon Sep 17 00:00:00 2001 From: forsola Date: Fri, 30 Apr 2021 21:50:19 +0200 Subject: [PATCH 01/31] online_sync Changes mades: 1.- Add a new page to laucher to retrieve Zwift Profile (only if server_ip <> player_ip) 2.- Adding an option to upload zwift_credentials.txt (similar to garmin_credentials.txt) 3.- Adding a button after each app file to removeit (profile.bin, garmin_credentials.txt, zwift_credentials.txt, strava_token.txt ) 4.- Adding an option to open a browser to get strava_auth file --- cdn/static/web/launcher/profile.html | 38 +++++++ cdn/static/web/launcher/upload.html | 20 +++- cdn/static/web/launcher/user_home.html | 4 + scripts/strava_auth.py | 27 +++-- zwift_offline.py | 137 ++++++++++++++++++++++--- 5 files changed, 191 insertions(+), 35 deletions(-) create mode 100644 cdn/static/web/launcher/profile.html 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 %} +
    + {% for message in messages %} +
  • +
    {{ message }}
    +
  • + {% endfor %} +
+ {% 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+ From 64dbba1b7d2d8f5bd67c9bb0e6fca9946edb1f50 Mon Sep 17 00:00:00 2001 From: forsola Date: Fri, 30 Apr 2021 23:41:14 +0200 Subject: [PATCH 02/31] minor_errors minor errors --- scripts/strava_auth.py | 13 ++++++++++++- zwift_offline.py | 9 ++++----- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/scripts/strava_auth.py b/scripts/strava_auth.py index 5210e47..419a937 100755 --- a/scripts/strava_auth.py +++ b/scripts/strava_auth.py @@ -86,8 +86,19 @@ class RequestHandler(BaseHTTPRequestHandler): self.server.logger.error("No code param received.") self.wfile.write(six.b("ERROR: No code param recevied.\n")) else: + SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) + print("Script: %s" %SCRIPT_DIR) + STORAGE_DIR = "%s/../storage" % SCRIPT_DIR + print("Storage: %s" %STORAGE_DIR) + SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR + if os.path.exists(SERVER_IP_FILE): + with open(SERVER_IP_FILE, 'r') as f: + server_ip = f.read().rstrip('\r\n') + else: + server_ip = '127.0.0.1' + url = client.authorization_url(client_id=self.server.client_id, - redirect_uri='http://18.133.120.5:{}/authorization'.format(self.server.server_port), + redirect_uri='http://' + server_ip + ':{}/authorization'.format(self.server.server_port), scope='activity:write') self.send_response(302) diff --git a/zwift_offline.py b/zwift_offline.py index 1adda9b..700c6b4 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -15,10 +15,10 @@ import threading import re import smtplib, ssl -import subprocess +#import subprocess import requests -import protobuf.activity_pb2 as activity_pb2 -import protobuf.profile_pb2 as profile_pb2 +#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 @@ -830,8 +830,7 @@ def api_events_search(): critccw_cat.route_id = 2875658892 critccw_cat.startLocation = cat critccw_cat.label = cat - - return '', 200 + return events.SerializeToString(), 200 @app.route('/api/events/subgroups/signup/', methods=['POST']) def api_events_subgroups_signup_id(event_id): From 0279379ae7ba44ddf83091dcbc24cd28658aa3b0 Mon Sep 17 00:00:00 2001 From: forsola Date: Fri, 30 Apr 2021 23:48:09 +0200 Subject: [PATCH 03/31] Update strava_auth.py restore to previous --- scripts/strava_auth.py | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) mode change 100755 => 100644 scripts/strava_auth.py diff --git a/scripts/strava_auth.py b/scripts/strava_auth.py old mode 100755 new mode 100644 index 419a937..9043ac1 --- a/scripts/strava_auth.py +++ b/scripts/strava_auth.py @@ -62,11 +62,10 @@ class RequestHandler(BaseHTTPRequestHandler): if request_path.startswith('/authorization'): self.send_response(200) - self.send_header("Content-Type", "application/octet-stream") - self.send_header("Content-Disposition", "attachment; filename=strava_token.txt") + self.send_header(six.b("Content-type"), six.b("text/plain")) 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] @@ -76,29 +75,22 @@ class RequestHandler(BaseHTTPRequestHandler): access_token = token_response['access_token'] refresh_token = token_response['refresh_token'] expires_at = token_response['expires_at'] - - 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))) + 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'); else: self.server.logger.error("No code param received.") self.wfile.write(six.b("ERROR: No code param recevied.\n")) else: - SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) - print("Script: %s" %SCRIPT_DIR) - STORAGE_DIR = "%s/../storage" % SCRIPT_DIR - print("Storage: %s" %STORAGE_DIR) - SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR - if os.path.exists(SERVER_IP_FILE): - with open(SERVER_IP_FILE, 'r') as f: - server_ip = f.read().rstrip('\r\n') - else: - server_ip = '127.0.0.1' - url = client.authorization_url(client_id=self.server.client_id, - redirect_uri='http://' + server_ip + ':{}/authorization'.format(self.server.server_port), + redirect_uri='http://localhost:{}/authorization'.format(self.server.server_port), scope='activity:write') self.send_response(302) @@ -127,9 +119,9 @@ if __name__ == "__main__": action='store', type=int, default=8000) parser.add_argument('--client-id', help='Strava API Client ID', - action='store', default='65392') + action='store', default='28117') parser.add_argument('--client-secret', help='Strava API Client Secret', - action='store', default='c6a2af6ebb6306d9b6c9974356bf63be80659ac8') + action='store', default='41b7b7b76d8cfc5dc12ad5f020adfea17da35468') args = parser.parse_args() main(port=args.port, client_id=args.client_id, client_secret=args.client_secret) From e98fefbf174f44297bab3392d945ee4dcf2e57ad Mon Sep 17 00:00:00 2001 From: forsola Date: Fri, 30 Apr 2021 23:56:50 +0200 Subject: [PATCH 04/31] Update strava_auth.py --- scripts/strava_auth.py | 34 +++++++++++++++++++++------------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/scripts/strava_auth.py b/scripts/strava_auth.py index 9043ac1..70a81a3 100644 --- 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,29 @@ 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: + SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) + print("Script: %s" %SCRIPT_DIR) + STORAGE_DIR = "%s/../storage" % SCRIPT_DIR + print("Storage: %s" %STORAGE_DIR) + SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR + if os.path.exists(SERVER_IP_FILE): + with open(SERVER_IP_FILE, 'r') as f: + server_ip = f.read().rstrip('\r\n') + else: + server_ip = '127.0.0.1' + url = client.authorization_url(client_id=self.server.client_id, - redirect_uri='http://localhost:{}/authorization'.format(self.server.server_port), + redirect_uri='http://' + server_ip + ':{}/authorization'.format(self.server.server_port), scope='activity:write') self.send_response(302) From 99c98c10b4c3f5e23f13bf831a3e2ea8860fc851 Mon Sep 17 00:00:00 2001 From: forsola Date: Sat, 1 May 2021 09:08:50 +0200 Subject: [PATCH 05/31] minor_changes --- scripts/online_sync.py | 186 +++++++++++++++++++++++++++++++++++++++++ scripts/strava_auth.py | 3 - zwift_offline.py | 10 ++- 3 files changed, 193 insertions(+), 6 deletions(-) create mode 100755 scripts/online_sync.py diff --git a/scripts/online_sync.py b/scripts/online_sync.py new file mode 100755 index 0000000..d767f58 --- /dev/null +++ b/scripts/online_sync.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python3 + +# +# Adapted from https://github.com/jlemon/zlogger/blob/master/get_riders.py +# +# The MIT License (MIT) +# +# Copyright (c) 2016 Jonathan Lemon +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NON INFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. + +import argparse +import getpass +import json +import os +import requests +import sys +sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +import protobuf.activity_pb2 as activity_pb2 +import protobuf.profile_pb2 as profile_pb2 + + +if getattr(sys, 'frozen', False): + # If we're running as a py installer bundle + SCRIPT_DIR = os.path.dirname(sys.executable) +else: + SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) + +try: + input = raw_input +except NameError: + pass + +global dbh + + +def post_credentials(session, username, password): + # Credentials POSTing and tokens retrieval + # POST https://secure.zwift.com/auth/realms/zwift/tokens/access/codes + + try: + response = session.post( + url="https://secure.zwift.com/auth/realms/zwift/tokens/access/codes", + headers={ + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Content-Type": "application/x-www-form-urlencoded", + "Host": "secure.zwift.com", + "User-Agent": "Zwift/1.5 (iPhone; iOS 9.0.2; Scale/2.00)", + "Accept-Language": "en-US;q=1", + }, + data={ + "client_id": "Zwift_Mobile_Link", + "username": username, + "password": password, + "grant_type": "password", + }, + allow_redirects=False, + ) + + json_dict = json.loads(response.content) + + return (json_dict["access_token"], json_dict["refresh_token"], json_dict["expires_in"]) + + except requests.exceptions.RequestException as e: + print('HTTP Request failed: %s' % e) + flash('HTTP Request failed: %s' % e) + + except KeyError as e: + print('Invalid uname and/or password') + flash('Invalid uname and/or password') + exit(-1) + + +def query_player_profile(session, access_token): + # Query Player Profile + # GET https://us-or-rly101.zwift.com/api/profiles/ + try: + response = session.get( + url="https://us-or-rly101.zwift.com/api/profiles/me", + headers={ + "Accept-Encoding": "gzip, deflate", + "Accept": "application/x-protobuf-lite", + "Connection": "keep-alive", + "Host": "us-or-rly101.zwift.com", + "User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0", + "Authorization": "Bearer %s" % access_token, + "Accept-Language": "en-us", + }, + ) + + return response.content + + except requests.exceptions.RequestException as e: + print('HTTP Request failed: %s' % e) + + +def logout(session, refresh_token): + # Logout + # POST https://secure.zwift.com/auth/realms/zwift/tokens/logout + try: + response = session.post( + url="https://secure.zwift.com/auth/realms/zwift/tokens/logout", + headers={ + "Accept": "*/*", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Content-Type": "application/x-www-form-urlencoded", + "Host": "secure.zwift.com", + "User-Agent": "Zwift/1.5 (iPhone; iOS 9.0.2; Scale/2.00)", + "Accept-Language": "en-US;q=1", + }, + data={ + "client_id": "Zwift_Mobile_Link", + "refresh_token": refresh_token, + }, + ) + + except requests.exceptions.RequestException as e: + print('HTTP Request failed: %s' % e) + + +def login(session, user, password): + access_token, refresh_token, expired_in = post_credentials(session, user, password) + return access_token, refresh_token + +def upload_activity(session, access_token, activity): + try: + response = session.put( + url="https://us-or-rly101.zwift.com/api/profiles/%s/activities/%s" % (activity.player_id, activity.id), + headers={ + "Content-Type": "application/x-protobuf-lite", + "Accept": "application/json", + "Connection": "keep-alive", + "Host": "us-or-rly101.zwift.com", + "User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0", + "Authorization": "Bearer %s" % access_token, + "Accept-Language": "en-us", + }, + data=activity.SerializeToString(), + ) + return response.status_code + + except requests.exceptions.RequestException as e: + print('HTTP Request failed: %s' % e) + + +def get_player_id(session, access_token): + try: + response = session.get( + url="https://us-or-rly101.zwift.com/api/profiles/me", + headers={ + "Accept-Encoding": "gzip, deflate", + "Accept": "application/x-protobuf-lite", + "Connection": "keep-alive", + "Host": "us-or-rly101.zwift.com", + "User-Agent": "Zwift/115 CFNetwork/758.0.2 Darwin/15.0.0", + "Authorization": "Bearer %s" % access_token, + "Accept-Language": "en-us", + }, + ) + + profile = profile_pb2.Profile() + profile.ParseFromString(response.content) + return profile.id + + except requests.exceptions.RequestException as e: + print('HTTP Request failed: %s' % e) + diff --git a/scripts/strava_auth.py b/scripts/strava_auth.py index 70a81a3..15bdf1c 100644 --- a/scripts/strava_auth.py +++ b/scripts/strava_auth.py @@ -86,10 +86,7 @@ class RequestHandler(BaseHTTPRequestHandler): self.server.logger.error("No code param received.") self.wfile.write(six.b("ERROR: No code param recevied.\n")) else: - SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) - print("Script: %s" %SCRIPT_DIR) STORAGE_DIR = "%s/../storage" % SCRIPT_DIR - print("Storage: %s" %STORAGE_DIR) SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR if os.path.exists(SERVER_IP_FILE): with open(SERVER_IP_FILE, 'r') as f: diff --git a/zwift_offline.py b/zwift_offline.py index 700c6b4..90d18ad 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -1124,6 +1124,10 @@ def garmin_upload(player_id, activity): def zwift_upload(player_id): profile_dir = '%s/%s' % (STORAGE_DIR, player_id) + SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR + if not os.path.exists(SERVER_IP_FILE): + logger.info("server_ip.txt missing, skip Zwift activity update") + return try: with open('%s/zwift_credentials.txt' % profile_dir, 'r') as f: if credentials_key is not None: @@ -1197,12 +1201,12 @@ def api_profiles_activities_id(player_id, activity_id): # will occur with these profiles). strava_upload(player_id, activity) garmin_upload(player_id, activity) + # 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) - + 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+ From a7859d64bc04232de6392a1d7f8c74c4a9ed19ea Mon Sep 17 00:00:00 2001 From: forsola Date: Sun, 2 May 2021 20:40:04 +0200 Subject: [PATCH 06/31] strava_auth Added strava_authorization function to zwift_offline.py so no need to start a new socket on port 8000. Added strava.html to show the status or retrieving authorization Changed user:home.html to link strava_auth directly to strava --- cdn/static/web/launcher/strava.html | 27 ++++++++++++++++++ zwift_offline.py | 43 ++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 cdn/static/web/launcher/strava.html diff --git a/cdn/static/web/launcher/strava.html b/cdn/static/web/launcher/strava.html new file mode 100644 index 0000000..b0a9d5e --- /dev/null +++ b/cdn/static/web/launcher/strava.html @@ -0,0 +1,27 @@ + +{% extends "./layout.html" %} +{% block content %} +

    Zwift Launcher

    + {% if username %} +

    Logged in as {{ username }}

    +
    + {% endif %} +
    +
    + {% with messages = get_flashed_messages() %} + {% if messages %} +
      + {% for message in messages %} +
    • +
      {{ message }}
      +
    • + {% endfor %} +
    + {% endif %} + {% endwith %} +
    +
    +{% endblock %} +{% block scripts %} + +{% endblock %} diff --git a/zwift_offline.py b/zwift_offline.py index 90d18ad..9c366c5 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -125,7 +125,13 @@ if os.path.exists("%s/multiplayer.txt" % STORAGE_DIR): f.write(Fernet.generate_key()) with open(GARMIN_KEY_FILE, 'rb') as f: credentials_key = f.read() - +try: + with open('%s/strava-client.txt' % STORAGE_DIR, 'r') as f: + client_id = f.readline().rstrip('\r\n') + client_secret = f.readline().rstrip('\r\n') +except: + client_id = '28117' + client_secret = '41b7b7b76d8cfc5dc12ad5f020adfea17da35468' from tokens import * # Android uses https for cdn @@ -463,6 +469,41 @@ def reset(username): flash("Password changed.") return render_template("reset.html", username=current_user.username) +@app.route("/strava", methods=['GET']) +@login_required +def strava(): + try: + from stravalib.client import Client + except ImportError: + flash("stravalib is not installed. Skipping Strava authorization attempt.") + return redirect('/user/%s/' % current_user.username) + client = Client() + url = client.authorization_url(client_id=client_id, + redirect_uri='https://%s/authorization' % server_ip, + scope='activity:write') + return redirect(url) + + +@app.route("/authorization", methods=["GET", "POST"]) +@login_required +def authorization(): + from stravalib.client import Client + try: + client = Client() + code = request.args.get('code') + token_response = client.exchange_code_for_token(client_id=client_id, client_secret=client_secret, code=code) + with open('%s/strava_token.txt' % os.path.join(STORAGE_DIR, str(current_user.player_id)), 'w') as f: + f.write(client_id + '\n'); + f.write(client_secret + '\n'); + f.write(token_response['access_token'] + '\n'); + f.write(token_response['refresh_token'] + '\n'); + f.write(str(token_response['expires_at']) + '\n'); + flash("Strava authorized. Go to \"Upload\" to remove authorization.") + except: + flash("Strava canceled.") + flash("Please close that window and return to Zwift Launcher.") + return render_template("strava.html", username=current_user.username) + @app.route("/profile//", methods=["GET", "POST"]) @login_required From b99c579d70f77de055710af74da4911631dcf38b Mon Sep 17 00:00:00 2001 From: forsola Date: Sun, 2 May 2021 20:59:56 +0200 Subject: [PATCH 07/31] minor changes. Change user_home.txt to point to strava directly. revert changes to the original strava_auth script move online_sync to root directotry --- cdn/static/web/launcher/user_home.html | 2 +- scripts/online_sync.py => online_sync.py | 0 zwift_offline.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename scripts/online_sync.py => online_sync.py (100%) diff --git a/cdn/static/web/launcher/user_home.html b/cdn/static/web/launcher/user_home.html index 812f590..39822b3 100644 --- a/cdn/static/web/launcher/user_home.html +++ b/cdn/static/web/launcher/user_home.html @@ -15,7 +15,7 @@
    Upload Change password - Strava auth + Strava auth {% if server_ip != "localhost" and server_ip != "127.0.0.1" %} Get Zwift profile {% endif %} diff --git a/scripts/online_sync.py b/online_sync.py similarity index 100% rename from scripts/online_sync.py rename to online_sync.py diff --git a/zwift_offline.py b/zwift_offline.py index 9c366c5..b13fc56 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -19,7 +19,7 @@ import smtplib, ssl import requests #import protobuf.activity_pb2 as activity_pb2 #import protobuf.profile_pb2 as profile_pb2 -import scripts.online_sync as online_sync +import online_sync as online_sync from copy import copy from functools import wraps From c8011d5d61f6ce78f6631c5558c659c0a950a09c Mon Sep 17 00:00:00 2001 From: forsola Date: Sun, 2 May 2021 21:06:54 +0200 Subject: [PATCH 08/31] revert_Strava rollback strava_auth.py to original --- scripts/strava_auth.py | 31 +++++++++++++------------------ 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/scripts/strava_auth.py b/scripts/strava_auth.py index 15bdf1c..9043ac1 100644 --- a/scripts/strava_auth.py +++ b/scripts/strava_auth.py @@ -62,11 +62,10 @@ class RequestHandler(BaseHTTPRequestHandler): if request_path.startswith('/authorization'): self.send_response(200) - self.send_header("Content-Type", "application/octet-stream") - self.send_header("Content-Disposition", "attachment; filename=strava_token.txt") + self.send_header(six.b("Content-type"), six.b("text/plain")) 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] @@ -76,26 +75,22 @@ class RequestHandler(BaseHTTPRequestHandler): access_token = token_response['access_token'] refresh_token = token_response['refresh_token'] expires_at = token_response['expires_at'] - - 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))) + 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'); else: self.server.logger.error("No code param received.") self.wfile.write(six.b("ERROR: No code param recevied.\n")) else: - STORAGE_DIR = "%s/../storage" % SCRIPT_DIR - SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR - if os.path.exists(SERVER_IP_FILE): - with open(SERVER_IP_FILE, 'r') as f: - server_ip = f.read().rstrip('\r\n') - else: - server_ip = '127.0.0.1' - url = client.authorization_url(client_id=self.server.client_id, - redirect_uri='http://' + server_ip + ':{}/authorization'.format(self.server.server_port), + redirect_uri='http://localhost:{}/authorization'.format(self.server.server_port), scope='activity:write') self.send_response(302) From 782f1e1a42effe3df3cf3a7b8d91f4d38d66ae9b Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 07:04:32 +0200 Subject: [PATCH 09/31] ignore_storage Ignore storage folder when commit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 0b33d80..6f8711e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ __pycache__/ build/ dist/ logs/ +storage/ From aa64b6fa91cd93faeae16239bd6237022f65fc40 Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 07:12:00 +0200 Subject: [PATCH 10/31] Update .gitignore ignore some other folders --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 6f8711e..cb5ef60 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,7 @@ build/ dist/ logs/ storage/ +.DS_Store +ZwiftServer.bbprojectd/ +.gitignore + From f137ca1ebcc04b732c6a808258317d7d3a1fcc36 Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 07:34:09 +0200 Subject: [PATCH 11/31] Update .gitignore some test --- .gitignore | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index cb5ef60..0b33d80 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,3 @@ __pycache__/ build/ dist/ logs/ -storage/ -.DS_Store -ZwiftServer.bbprojectd/ -.gitignore - From 20792176f03288f8704b867399eca0350f4f66ef Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 07:49:09 +0200 Subject: [PATCH 12/31] clean comments --- zwift_offline.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/zwift_offline.py b/zwift_offline.py index b13fc56..805a63b 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -14,11 +14,7 @@ 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 online_sync as online_sync from copy import copy From 8fdd69891431eb7e9df479daef9866806932e7d7 Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 07:55:36 +0200 Subject: [PATCH 13/31] Update zwift_offline.py Changing GARMIN_KEY_FILE for CREDENTIALS_KEY_FILE since this is used now to encrypt GARMIN and STRAVA _credentials.txt --- zwift_offline.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/zwift_offline.py b/zwift_offline.py index 805a63b..30df990 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -115,11 +115,11 @@ 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/credentials-key.txt" % STORAGE_DIR - if not os.path.exists(GARMIN_KEY_FILE): - with open(GARMIN_KEY_FILE, 'wb') as f: + CREDENTIALS_KEY_FILE = "%s/credentials-key.txt" % STORAGE_DIR + if not os.path.exists(CREDENTIALS_KEY_FILE): + with open(CREDENTIALS_KEY_FILE, 'wb') as f: f.write(Fernet.generate_key()) - with open(GARMIN_KEY_FILE, 'rb') as f: + with open(CREDENTIALS_KEY_FILE, 'rb') as f: credentials_key = f.read() try: with open('%s/strava-client.txt' % STORAGE_DIR, 'r') as f: From e21d2ec91debb384c6c0c1353ae40975138ef13b Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 10:50:37 +0200 Subject: [PATCH 14/31] Strava_credentials Add the option of saving the credentials of strava when retrieving the stra_auth --- cdn/static/web/launcher/profile.html | 6 +++++- zwift_offline.py | 26 +++++++++++++++++++++----- 2 files changed, 26 insertions(+), 6 deletions(-) diff --git a/cdn/static/web/launcher/profile.html b/cdn/static/web/launcher/profile.html index 28b3dfd..bab951f 100644 --- a/cdn/static/web/launcher/profile.html +++ b/cdn/static/web/launcher/profile.html @@ -14,7 +14,11 @@
    -
    +
    + + +
    +
    diff --git a/zwift_offline.py b/zwift_offline.py index 30df990..de16af4 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -512,6 +512,25 @@ def profile(username): username = request.form['username'] password = request.form['password'] + player_id = current_user.player_id + profile_dir = '%s/%s' % (STORAGE_DIR, str(player_id)) + + try: + safe_zwift = request.form['safe_zwift'] + file_path = os.path.join(profile_dir, 'zwift_credentials.txt') + with open(file_path, 'w') as f: + f.write(username + '\n'); + f.write(password + '\n'); + if credentials_key is not None: + with open(file_path, 'rb') as fr: + zwift_credentials = fr.read() + cipher_suite = Fernet(credentials_key) + ciphered_text = cipher_suite.encrypt(zwift_credentials) + with open(file_path, 'wb') as fw: + fw.write(ciphered_text) + flash("Zwift credentials saved") + except: + logger.warn("Zwift credentials not saved") session = requests.session() try: @@ -521,15 +540,12 @@ def profile(username): 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.") + flash("Zwift profile installed locally.") except: flash("Error downloading profile") except: - flash("Error invalid Username or password") - + flash("Error invalid Username or password") return render_template("profile.html", username=current_user.username) @app.route("/user//") From 5fa9843ed57368992a55495f20613e2e7b870848 Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 3 May 2021 09:54:55 -0300 Subject: [PATCH 15/31] fix indent --- cdn/static/web/launcher/upload.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cdn/static/web/launcher/upload.html b/cdn/static/web/launcher/upload.html index 50cf980..7e79779 100644 --- a/cdn/static/web/launcher/upload.html +++ b/cdn/static/web/launcher/upload.html @@ -10,7 +10,7 @@
      
    Download
    -   
    Delete
    +   
    Delete
    @@ -20,7 +20,7 @@
    -   
    Delete
    +   
    Delete
    @@ -30,7 +30,7 @@
    -   
    Delete
    +   
    Delete
    @@ -40,7 +40,7 @@
    -   
    Delete
    +   
    Delete
    From 45fd1226b76949fedd652b57871ea96d221cc81d Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 3 May 2021 09:56:29 -0300 Subject: [PATCH 16/31] nits --- cdn/static/web/launcher/upload.html | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cdn/static/web/launcher/upload.html b/cdn/static/web/launcher/upload.html index 7e79779..eb64444 100644 --- a/cdn/static/web/launcher/upload.html +++ b/cdn/static/web/launcher/upload.html @@ -44,7 +44,8 @@
    - {% endif %} + {% endif %} +
    From fa7b717e31177fc28ba6d44bd3b1272841c6f2a5 Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 3 May 2021 10:12:34 -0300 Subject: [PATCH 17/31] remove unnecessary stuff --- online_sync.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/online_sync.py b/online_sync.py index d767f58..e0bf98f 100755 --- a/online_sync.py +++ b/online_sync.py @@ -25,31 +25,12 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import argparse -import getpass import json -import os import requests -import sys -sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import protobuf.activity_pb2 as activity_pb2 import protobuf.profile_pb2 as profile_pb2 -if getattr(sys, 'frozen', False): - # If we're running as a py installer bundle - SCRIPT_DIR = os.path.dirname(sys.executable) -else: - SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) - -try: - input = raw_input -except NameError: - pass - -global dbh - - def post_credentials(session, username, password): # Credentials POSTing and tokens retrieval # POST https://secure.zwift.com/auth/realms/zwift/tokens/access/codes @@ -86,7 +67,6 @@ def post_credentials(session, username, password): except KeyError as e: print('Invalid uname and/or password') flash('Invalid uname and/or password') - exit(-1) def query_player_profile(session, access_token): @@ -183,4 +163,3 @@ def get_player_id(session, access_token): except requests.exceptions.RequestException as e: print('HTTP Request failed: %s' % e) - From 88abf157b7cc4058cc06c016e484ab583f7a21ea Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 3 May 2021 10:17:40 -0300 Subject: [PATCH 18/31] fix indent just check if server_ip.txt exists --- cdn/static/web/launcher/user_home.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cdn/static/web/launcher/user_home.html b/cdn/static/web/launcher/user_home.html index 39822b3..aa68442 100644 --- a/cdn/static/web/launcher/user_home.html +++ b/cdn/static/web/launcher/user_home.html @@ -15,8 +15,8 @@
    Upload Change password - Strava auth - {% if server_ip != "localhost" and server_ip != "127.0.0.1" %} + Strava auth + {% if server_ip %} Get Zwift profile {% endif %} Logout From 27d8171a40f095af7ca6d68107a2808b8ff61425 Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 15:27:50 +0200 Subject: [PATCH 19/31] Update zwift_offline.py safe file after authentication sucess! --- zwift_offline.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/zwift_offline.py b/zwift_offline.py index de16af4..c83f82d 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -509,28 +509,10 @@ def profile(username): 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'] player_id = current_user.player_id profile_dir = '%s/%s' % (STORAGE_DIR, str(player_id)) - - try: - safe_zwift = request.form['safe_zwift'] - file_path = os.path.join(profile_dir, 'zwift_credentials.txt') - with open(file_path, 'w') as f: - f.write(username + '\n'); - f.write(password + '\n'); - if credentials_key is not None: - with open(file_path, 'rb') as fr: - zwift_credentials = fr.read() - cipher_suite = Fernet(credentials_key) - ciphered_text = cipher_suite.encrypt(zwift_credentials) - with open(file_path, 'wb') as fw: - fw.write(ciphered_text) - flash("Zwift credentials saved") - except: - logger.warn("Zwift credentials not saved") session = requests.session() try: @@ -544,6 +526,22 @@ def profile(username): flash("Zwift profile installed locally.") except: flash("Error downloading profile") + if request.form.get("safe_zwift", None) != None: + try: + file_path = os.path.join(profile_dir, 'zwift_credentials.txt') + with open(file_path, 'w') as f: + f.write(username + '\n'); + f.write(password + '\n'); + if credentials_key is not None: + with open(file_path, 'rb') as fr: + zwift_credentials = fr.read() + cipher_suite = Fernet(credentials_key) + ciphered_text = cipher_suite.encrypt(zwift_credentials) + with open(file_path, 'wb') as fw: + fw.write(ciphered_text) + flash("Zwift credentials saved") + except: + flash("Error saving 'zwift_credentiasl.txt' file") except: flash("Error invalid Username or password") return render_template("profile.html", username=current_user.username) From 35d3cf283c91bee23b7eb913aef26241df25bf6b Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 3 May 2021 10:41:35 -0300 Subject: [PATCH 20/31] fix indent pass server_ip as boolean --- zwift_offline.py | 154 +++++++++++++++++++++++++---------------------- 1 file changed, 81 insertions(+), 73 deletions(-) diff --git a/zwift_offline.py b/zwift_offline.py index c83f82d..b0124bd 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -15,7 +15,7 @@ import threading import re import smtplib, ssl import requests -import online_sync as online_sync +import online_sync from copy import copy from functools import wraps @@ -47,7 +47,6 @@ 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) @@ -121,6 +120,7 @@ if os.path.exists("%s/multiplayer.txt" % STORAGE_DIR): f.write(Fernet.generate_key()) with open(CREDENTIALS_KEY_FILE, 'rb') as f: credentials_key = f.read() + try: with open('%s/strava-client.txt' % STORAGE_DIR, 'r') as f: client_id = f.readline().rstrip('\r\n') @@ -128,6 +128,7 @@ try: except: client_id = '28117' client_secret = '41b7b7b76d8cfc5dc12ad5f020adfea17da35468' + from tokens import * # Android uses https for cdn @@ -465,6 +466,8 @@ def reset(username): flash("Password changed.") return render_template("reset.html", username=current_user.username) + + @app.route("/strava", methods=['GET']) @login_required def strava(): @@ -485,18 +488,18 @@ def strava(): def authorization(): from stravalib.client import Client try: - client = Client() - code = request.args.get('code') - token_response = client.exchange_code_for_token(client_id=client_id, client_secret=client_secret, code=code) - with open('%s/strava_token.txt' % os.path.join(STORAGE_DIR, str(current_user.player_id)), 'w') as f: - f.write(client_id + '\n'); - f.write(client_secret + '\n'); - f.write(token_response['access_token'] + '\n'); - f.write(token_response['refresh_token'] + '\n'); - f.write(str(token_response['expires_at']) + '\n'); - flash("Strava authorized. Go to \"Upload\" to remove authorization.") + client = Client() + code = request.args.get('code') + token_response = client.exchange_code_for_token(client_id=client_id, client_secret=client_secret, code=code) + with open('%s/strava_token.txt' % os.path.join(STORAGE_DIR, str(current_user.player_id)), 'w') as f: + f.write(client_id + '\n'); + f.write(client_secret + '\n'); + f.write(token_response['access_token'] + '\n'); + f.write(token_response['refresh_token'] + '\n'); + f.write(str(token_response['expires_at']) + '\n'); + flash("Strava authorized. Go to \"Upload\" to remove authorization.") except: - flash("Strava canceled.") + flash("Strava canceled.") flash("Please close that window and return to Zwift Launcher.") return render_template("strava.html", username=current_user.username) @@ -506,51 +509,52 @@ def authorization(): 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); + flash("Zwift credentials can't be empty") + return render_template("profile.html", username=current_user.username) + username = request.form['username'] - password = request.form['password'] + password = request.form['password'] player_id = current_user.player_id profile_dir = '%s/%s' % (STORAGE_DIR, str(player_id)) 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) - os.rename('%s/profile.bin' % SCRIPT_DIR, '%s/profile.bin' % profile_dir) - flash("Zwift profile installed locally.") - except: - flash("Error downloading profile") - if request.form.get("safe_zwift", None) != None: - try: - file_path = os.path.join(profile_dir, 'zwift_credentials.txt') - with open(file_path, 'w') as f: - f.write(username + '\n'); - f.write(password + '\n'); - if credentials_key is not None: - with open(file_path, 'rb') as fr: - zwift_credentials = fr.read() - cipher_suite = Fernet(credentials_key) - ciphered_text = cipher_suite.encrypt(zwift_credentials) - with open(file_path, 'wb') as fw: - fw.write(ciphered_text) - flash("Zwift credentials saved") - except: - flash("Error saving 'zwift_credentiasl.txt' file") + 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) + os.rename('%s/profile.bin' % SCRIPT_DIR, '%s/profile.bin' % profile_dir) + flash("Zwift profile installed locally.") + except: + flash("Error downloading profile") + if request.form.get("safe_zwift", None) != None: + try: + file_path = os.path.join(profile_dir, 'zwift_credentials.txt') + with open(file_path, 'w') as f: + f.write(username + '\n'); + f.write(password + '\n'); + if credentials_key is not None: + with open(file_path, 'rb') as fr: + zwift_credentials = fr.read() + cipher_suite = Fernet(credentials_key) + ciphered_text = cipher_suite.encrypt(zwift_credentials) + with open(file_path, 'wb') as fw: + fw.write(ciphered_text) + flash("Zwift credentials saved") + except: + flash("Error saving 'zwift_credentiasl.txt' file") except: - flash("Error invalid Username or password") + 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,server_ip=server_ip) + online=get_online(), is_admin=current_user.is_admin, restarting=restarting, restarting_in_minutes=restarting_in_minutes, server_ip=os.path.exists(SERVER_IP_FILE)) def send_message_to_all_online(message, sender='Server'): @@ -642,7 +646,7 @@ 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', 'zwift_credentials.txt']: @@ -881,8 +885,10 @@ def api_events_search(): critccw_cat.route_id = 2875658892 critccw_cat.startLocation = cat critccw_cat.label = cat + return events.SerializeToString(), 200 + @app.route('/api/events/subgroups/signup/', methods=['POST']) def api_events_subgroups_signup_id(event_id): return '{"signedUp":true}', 200 @@ -901,6 +907,8 @@ 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. @@ -1138,6 +1146,7 @@ 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 @@ -1173,12 +1182,13 @@ 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) SERVER_IP_FILE = "%s/server-ip.txt" % STORAGE_DIR if not os.path.exists(SERVER_IP_FILE): - logger.info("server_ip.txt missing, skip Zwift activity update") - return + logger.info("server_ip.txt missing, skip Zwift activity update") + return try: with open('%s/zwift_credentials.txt' % profile_dir, 'r') as f: if credentials_key is not None: @@ -1197,29 +1207,29 @@ def zwift_upload(player_id): 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") + 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?") @@ -1252,12 +1262,10 @@ def api_profiles_activities_id(player_id, activity_id): # will occur with these profiles). strava_upload(player_id, activity) garmin_upload(player_id, activity) - # 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()) 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+ From 1c003cf508f032bf4dc37716956923214d35c577 Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 16:04:40 +0200 Subject: [PATCH 21/31] Update profile.html correct class for checkbox input (aligned left) --- cdn/static/web/launcher/profile.html | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/cdn/static/web/launcher/profile.html b/cdn/static/web/launcher/profile.html index bab951f..ea55902 100644 --- a/cdn/static/web/launcher/profile.html +++ b/cdn/static/web/launcher/profile.html @@ -14,9 +14,11 @@
    -
    - - +
    +
    + + +
    From ef7304667734ce6fd2292a343358059e607887cc Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 3 May 2021 12:42:00 -0300 Subject: [PATCH 22/31] add flashed messages in user home --- cdn/static/web/launcher/profile.html | 2 +- cdn/static/web/launcher/strava.html | 4 ---- cdn/static/web/launcher/user_home.html | 11 +++++++++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cdn/static/web/launcher/profile.html b/cdn/static/web/launcher/profile.html index ea55902..95d1515 100644 --- a/cdn/static/web/launcher/profile.html +++ b/cdn/static/web/launcher/profile.html @@ -20,7 +20,7 @@
    - +
    diff --git a/cdn/static/web/launcher/strava.html b/cdn/static/web/launcher/strava.html index b0a9d5e..b9278f2 100644 --- a/cdn/static/web/launcher/strava.html +++ b/cdn/static/web/launcher/strava.html @@ -1,4 +1,3 @@ - {% extends "./layout.html" %} {% block content %}

    Zwift Launcher

    @@ -22,6 +21,3 @@
    {% endblock %} -{% block scripts %} - -{% endblock %} diff --git a/cdn/static/web/launcher/user_home.html b/cdn/static/web/launcher/user_home.html index aa68442..d41a198 100644 --- a/cdn/static/web/launcher/user_home.html +++ b/cdn/static/web/launcher/user_home.html @@ -66,6 +66,17 @@ + {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} {% endblock %} From e34b71f80cb8ffa0ab7526668f3d2996895dac1f Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 3 May 2021 19:19:30 +0200 Subject: [PATCH 23/31] Update strava_auth.py adding +x attribute --- scripts/strava_auth.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 scripts/strava_auth.py diff --git a/scripts/strava_auth.py b/scripts/strava_auth.py old mode 100644 new mode 100755 From be638036def33607bf369e8ce3bb716f6ef1b074 Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 3 May 2021 14:44:44 -0300 Subject: [PATCH 24/31] minor changes --- cdn/static/web/launcher/profile.html | 2 +- zwift_offline.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cdn/static/web/launcher/profile.html b/cdn/static/web/launcher/profile.html index 95d1515..84b5ef6 100644 --- a/cdn/static/web/launcher/profile.html +++ b/cdn/static/web/launcher/profile.html @@ -17,7 +17,7 @@
    - +
    diff --git a/zwift_offline.py b/zwift_offline.py index b0124bd..53efed5 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -15,8 +15,6 @@ import threading import re import smtplib, ssl import requests -import online_sync - from copy import copy from functools import wraps from io import BytesIO @@ -46,6 +44,7 @@ import protobuf.world_pb2 as world_pb2 import protobuf.zfiles_pb2 as zfiles_pb2 import protobuf.hash_seeds_pb2 as hash_seeds_pb2 import protobuf.events_pb2 as events_pb2 +import online_sync logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO")) logger = logging.getLogger('zoffline') @@ -708,6 +707,7 @@ 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): @@ -719,7 +719,6 @@ def delete(filename): return redirect(url_for('upload', username=current_user)) - @app.route("/logout/") @login_required def logout(username): @@ -1233,6 +1232,7 @@ def zwift_upload(player_id): 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 @app.route('/api/profiles//activities/', methods=['PUT']) From a4281305b4e6c0bd8cca89bf408f9096b3e6fffa Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Mon, 3 May 2021 14:50:50 -0300 Subject: [PATCH 25/31] minor change --- zwift_offline.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zwift_offline.py b/zwift_offline.py index 53efed5..c0f292f 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -499,7 +499,7 @@ def authorization(): flash("Strava authorized. Go to \"Upload\" to remove authorization.") except: flash("Strava canceled.") - flash("Please close that window and return to Zwift Launcher.") + flash("Please close this window and return to Zwift Launcher.") return render_template("strava.html", username=current_user.username) From 07c308f616fff4616acf8aae438103c1fb2b1b28 Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Wed, 5 May 2021 16:07:07 -0300 Subject: [PATCH 26/31] align buttons --- cdn/static/web/launcher/upload.html | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/cdn/static/web/launcher/upload.html b/cdn/static/web/launcher/upload.html index eb64444..eef35ec 100644 --- a/cdn/static/web/launcher/upload.html +++ b/cdn/static/web/launcher/upload.html @@ -8,9 +8,9 @@
  • - -   
    Download
    -   
    Delete
    + + +
    Download
    Delete
  • @@ -19,8 +19,8 @@
  • - -   
    Delete
    + +
    Delete
  • @@ -29,8 +29,8 @@
  • - -   
    Delete
    + +
    Delete
  • @@ -39,8 +39,8 @@
  • - -   
    Delete
    + +
    Delete
  • From 420bd21c932705d2ac7af326c52da182ad1aa27f Mon Sep 17 00:00:00 2001 From: oldnapalm <38410858+oldnapalm@users.noreply.github.com> Date: Wed, 5 May 2021 16:10:21 -0300 Subject: [PATCH 27/31] nit --- cdn/static/web/launcher/upload.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cdn/static/web/launcher/upload.html b/cdn/static/web/launcher/upload.html index eef35ec..0fb5bb2 100644 --- a/cdn/static/web/launcher/upload.html +++ b/cdn/static/web/launcher/upload.html @@ -35,7 +35,7 @@ {% endif %} - {% if zwift %} + {% if zwift %}
  • From fceb761b49a0d50f5e233c87ecb7e97e3bde68f8 Mon Sep 17 00:00:00 2001 From: forsola Date: Sun, 9 May 2021 09:01:39 +0200 Subject: [PATCH 28/31] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 07e4992..76a2080 100644 --- a/README.md +++ b/README.md @@ -161,6 +161,7 @@ Why: We need to redirect Zwift to use zoffline and convince OS X and Zwift to accept zoffline's self signed certificates for Zwift's domain names. Feel free to generate your own certificates and do the same. +For MacOS BigSur please read and apply: https://github.com/zoffline/zwift-offline/issues/132
    Android (non-rooted device) From c921dd594aa541cddec475c1535fc4fde4a29682 Mon Sep 17 00:00:00 2001 From: forsola Date: Sun, 9 May 2021 09:03:19 +0200 Subject: [PATCH 29/31] Update README.md Add support for MacOS BigSur --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 76a2080..98bd45e 100644 --- a/README.md +++ b/README.md @@ -152,6 +152,8 @@ to generate your own certificates and do the same. ``` +* For MacOS BigSur please read and apply: https://github.com/zoffline/zwift-offline/issues/132 + * Using a text editor (with admin privileges) open ``/etc/hosts`` * Append this line: `` us-or-rly101.zwift.com secure.zwift.com cdn.zwift.com launcher.zwift.com``
    (Where ```` is the ip address of the machine running zoffline. If @@ -161,7 +163,6 @@ Why: We need to redirect Zwift to use zoffline and convince OS X and Zwift to accept zoffline's self signed certificates for Zwift's domain names. Feel free to generate your own certificates and do the same. -For MacOS BigSur please read and apply: https://github.com/zoffline/zwift-offline/issues/132
    Android (non-rooted device) From f90f225750e44649b195f479073db10d01dabee0 Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 10 May 2021 11:43:45 +0200 Subject: [PATCH 30/31] Revert "Update README.md" This reverts commit c921dd594aa541cddec475c1535fc4fde4a29682. --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 98bd45e..76a2080 100644 --- a/README.md +++ b/README.md @@ -152,8 +152,6 @@ to generate your own certificates and do the same. ``` -* For MacOS BigSur please read and apply: https://github.com/zoffline/zwift-offline/issues/132 - * Using a text editor (with admin privileges) open ``/etc/hosts`` * Append this line: `` us-or-rly101.zwift.com secure.zwift.com cdn.zwift.com launcher.zwift.com``
    (Where ```` is the ip address of the machine running zoffline. If @@ -163,6 +161,7 @@ Why: We need to redirect Zwift to use zoffline and convince OS X and Zwift to accept zoffline's self signed certificates for Zwift's domain names. Feel free to generate your own certificates and do the same. +For MacOS BigSur please read and apply: https://github.com/zoffline/zwift-offline/issues/132
    Android (non-rooted device) From 9c2c81d977309feaf2c8f9540c557b6e93e1e843 Mon Sep 17 00:00:00 2001 From: forsola Date: Mon, 10 May 2021 11:44:12 +0200 Subject: [PATCH 31/31] Revert "Update README.md" This reverts commit fceb761b49a0d50f5e233c87ecb7e97e3bde68f8. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 76a2080..07e4992 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,6 @@ Why: We need to redirect Zwift to use zoffline and convince OS X and Zwift to accept zoffline's self signed certificates for Zwift's domain names. Feel free to generate your own certificates and do the same. -For MacOS BigSur please read and apply: https://github.com/zoffline/zwift-offline/issues/132
    Android (non-rooted device)