diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..1ff6677 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,21 @@ +# Only breaking changes in releases will be documented here. + +Update to Zwift_1.0.60239: + * With the update to this Zwift release, zoffline has included significant changes + to fully support a multiplayer mode, pace partners, and more. + * New dependencies required if running from source: pyJWT, flask-login, FlaskSQLAlchemy, gevent + * Install these with ``pip install pyjwt flask-login flask_sqlalchemy gevent`` + * Multi-profile support has been deprecated in favor of full multiplayer support. + * See Step 6 in README.md for how to enable multiplayer support. + * Create an account for each profile you had and upload their profile.bin + and credential files via the new Zwift launcher. + * Do not "upgrade" from an existing single player installation to multiplayer and then + "downgrade" back to single player without backing up your original storage directory. + You will lose your activities, segment results, and goals if you do this and don't + have a backup to restore from. + + +Update to Zwift_1.0.51959: + * Changes were made to Zwift's cacert.pem. You will likely need to re-append + cert-zwift-com.pem to Zwift's cacert.pem in a specific way. + See https://github.com/zoffline/zwift-offline/issues/62 for details. diff --git a/Dockerfile b/Dockerfile index 3e46cd7..3992402 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ MAINTAINER zoffline WORKDIR /usr/src/app -RUN apk add --no-cache git +RUN apk add --no-cache git gcc musl-dev libffi-dev openssl-dev file make RUN pip install flask flask_sqlalchemy flask-login pyjwt gevent protobuf protobuf3_to_dict stravalib garmin-uploader cryptography RUN git clone --depth 1 https://github.com/zoffline/zwift-offline diff --git a/README.md b/README.md index 85e6b26..a90bfa6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # zoffline -zoffline enables the use of [Zwift](http://zwift.com) offline by acting as a partial implementation of a Zwift server. +zoffline enables the use of [Zwift](http://zwift.com) offline by acting as a partial implementation of a Zwift server. By default zoffline is only for a single player. See [Step 6: Enable Multiplayer](#step-6-optional-enable-multiplayer) for how to enable support for multiple users/profiles. zoffline also offers riding against ghosts (your previous rides). Enable this feature by checking "Enable ghosts" in zoffline's launcher. See https://github.com/zoffline/zwift-offline/issues/56 for extra details. @@ -27,19 +27,20 @@ To install zoffline on Windows:
Linux, Windows, or Mac OS X (from source) To install zoffline on Linux, Windows, or Mac OS X: -* Install Python 2 or 3 (https://www.python.org/downloads/) if not already installed +* Install Python 3 if not already installed + * On Windows, installing Python via the Microsoft Store is highly recommend! If using a Python installer, ensure that in the first Python installer screen "Add Python 3.x to PATH" is checked. * Install dependencies: flask, flask_sqlalchemy, flask-login, pyjwt, gevent, python-protobuf, protobuf3_to_dict, stravalib (optional) * e.g., on Linux/Mac: ``pip install flask flask_sqlalchemy flask-login pyjwt gevent protobuf protobuf3_to_dict stravalib`` - * e.g., on Windows in command prompt: ``C:\Python27\Scripts\pip.exe install flask flask_sqlalchemy flask-login pyjwt gevent protobuf protobuf3_to_dict stravalib`` - * Python 3 is installed by default in ``C:\Users\\AppData\Local\Programs\Python\Python38-32`` instead of ``C:\Python27`` + * e.g., on Windows in command prompt: ``pip install flask flask_sqlalchemy flask-login pyjwt gevent protobuf protobuf3_to_dict stravalib`` + * You may need to use ``C:\Users\\AppData\Local\Programs\Python\Python39\Scripts\pip.exe`` instead of just ``pip`` * Clone or download this repo * If you are not running zoffline on the same PC that Zwift is running: create a ``server-ip.txt`` file in the ``storage`` directory containing the IP address of the PC running zoffline. * Run standalone.py before starting Zwift * e.g., on Linux/Mac: ``sudo ./standalone.py`` * sudo is needed because we're binding to the privileged ports 80 and 443. * If using Python 3, but Python 3 is not your system default run ``sudo python3 standalone.py`` - * e.g., on Windows in command prompt: ``C:\Python27\python.exe standalone.py`` - * For Python 3 the command will likely be ``C:\Users\\AppData\Local\Programs\Python\Python38-32\python.exe standalone.py`` + * e.g., on Windows in command prompt: ``python standalone.py`` + * You may need to use ``C:\Users\\AppData\Local\Programs\Python\Python39\python.exe`` instead of just ``python`` * Start Zwift with standalone.py running (__after completing step 2__) * Note: When upgrading zoffline, be sure to retain the ``storage`` directory. It contains your Zwift progress state. @@ -171,6 +172,8 @@ the Zwift system tray icon and exit) and restart Zwift. ### Step 3 [OPTIONAL]: Obtain current Zwift profile +
Expand + If you don't obtain your current Zwift profile before first starting Zwift with zoffline enabled, you will be prompted to create a new profile (height, weight, gender). Your profile can be further customized and changed via the in game @@ -185,27 +188,35 @@ To obtain your current profile: * If multiplayer is enabled, use the upload button in the launcher window to import your file. * If using Docker, move profile.bin into the path you passed to ``-v`` +
-### Step 4 [OPTIONAL]: Obtain Strava API token +### Step 4 [OPTIONAL]: Upload activities to Strava + +
Expand * Install dependencies: stravalib * e.g., on Linux/Mac: ``pip install stravalib`` - * e.g., on Windows in command prompt: ``C:\Python27\Scripts\pip.exe install stravalib`` - * Or, if using the Windows zoffline.exe version without Python installed you can run strava_auth.exe obtained from https://github.com/zoffline/zwift-offline/releases/tag/zoffline_helper in place of ``scripts/strava_auth.py`` below. + * e.g., on Windows in command prompt: ``pip install stravalib`` + * You may need to use ``C:\Users\\AppData\Local\Programs\Python\Python39\Scripts\pip.exe`` instead of just ``pip`` + * Or, if using the Windows zoffline.exe version without Python installed you can run ``strava_auth.exe`` obtained from https://github.com/zoffline/zwift-offline/releases/tag/zoffline_helper in place of ``scripts/strava_auth.py`` below. * [OPTIONAL] Get CLIENT_ID and CLIENT_SECRET from https://www.strava.com/settings/api * Run ``scripts/strava_auth.py --client-id CLIENT_ID --client-secret CLIENT_SECRET`` * Run without arguments to use default values. * Open http://localhost:8000/ and authorize. -* Move the resulting strava_token.txt (saved in whatever directory you ran strava_auth.py in) into the ``storage/`` directory. +* Move the resulting ``strava_token.txt`` (saved in whatever directory you ran ``strava_auth.py`` in) into the ``storage/`` directory. * If multiplayer is enabled, use the upload button in the launcher window to import your file. +
### Step 5 [OPTIONAL]: Upload activities to Garmin Connect +
Expand + * Install dependencies: garmin-uploader, cryptography (optional) * e.g., on Linux/Mac: ``pip install garmin-uploader cryptography`` - * e.g., on Windows in command prompt: ``C:\Python27\Scripts\pip.exe install garmin-uploader cryptography`` -* Create a file garmin_credentials.txt in the ``storage/`` directory containing your login credentials + * e.g., on Windows in command prompt: ``pip install garmin-uploader cryptography`` + * You may need to use ``C:\Users\\AppData\Local\Programs\Python\Python39\Scripts\pip.exe`` instead of just ``pip`` +* Create a file ``garmin_credentials.txt`` in the ``storage/`` directory containing your login credentials ``` @@ -213,16 +224,27 @@ To obtain your current profile: * Note: this is not secure. Only do this if you are comfortable with your login credentials being stored in a clear text file. * If multiplayer is enabled, use the upload button in the launcher window to encrypt the credentials file. +
### Step 6 [OPTIONAL]: Enable multiplayer -* Create a multiplayer.txt file in the ``storage`` directory. -* If you are not running zoffline on the same PC that Zwift is running: create a server-ip.txt file in the ``storage`` directory containing the IP address of the PC running zoffline. +
Expand -
Extra optional steps +To enable support for multiple users perform the steps below. zoffline's previous multi-profile support has been superceded by full multiplayer support. If you were previously using multiple profiles with zoffline you will need to enable multiplayer to continue supporting multiple users. -* To obtain the official map schedule and update files from Zwift server: create a cdn-proxy.txt file in the ``storage`` directory. This can only work if you are running zoffline on a different machine than the Zwift client. -* To enable the password reset feature: create a gmail_credentials.txt file in the ``storage`` directory containing the login credentials of a Gmail account. You need to enable the "Less secure app access" in the account settings and you may need to access https://accounts.google.com/DisplayUnlockCaptcha to allow the login from the server. +* Create a ``multiplayer.txt`` file in the ``storage`` directory. +* If you are not running zoffline on the same PC that Zwift is running: create a ``server-ip.txt`` file in the ``storage`` directory containing the IP address of the PC running zoffline. + * TCP ports 80, 443, 3023 and UDP port 3022 will need to be open on the PC running zoffline if its running remotely. +* Start Zwift and create an account in the new Zwift launcher and upload your ``profile.bin``, ``strava_credentials.txt``, and/or ``garmin_credentials.txt`` if you have them. + * This account will only exist on your zoffline server and has no relation with your actual Zwift account. + +
+ +### Extra optional steps +
Expand + +* To obtain the official map schedule and update files from Zwift server: create a ``cdn-proxy.txt`` file in the ``storage`` directory. This can only work if you are running zoffline on a different machine than the Zwift client. +* To enable the password reset feature when multiplayer is enabled: create a gmail_credentials.txt file in the ``storage`` directory containing the login credentials of a Gmail account. You need to enable the "Less secure app access" in the account settings and you may need to access https://accounts.google.com/DisplayUnlockCaptcha to allow the login from the server. * If the Zwift client is having issues connecting to the Linux server ("The request was aborted: Could not create SSL/TLS secure channel." or "The underlying connection was closed: An unexpected error occurred on a send. Received an unexpected EOF or 0 bytes from the transport stream."): change MinProtocol in /etc/ssl/openssl.cnf to TLSv1.0 ``` [system_default_sect] @@ -231,9 +253,9 @@ To obtain your current profile: ```
-## Discord and zoffline (online) server +## Community Discord and zoffline (online) server -Please join the [Discord](https://discord.gg/GMdn8F8) and our enhanced version of zoffline, hosted Online! +Please join the community supported [Discord](https://discord.gg/GMdn8F8) and their enhanced version of zoffline, hosted Online! Follow the guide in #instructions to create your account and join other Zwifters. @@ -243,7 +265,8 @@ Docker -or- -* Python 2 or 3 (https://www.python.org/downloads/) +* Python 3 (https://www.python.org/downloads/) + * On Windows, installing Python via the Microsoft Store is highly recommend! If using a Python installer, ensure that in the first Python installer screen "Add Python 3.x to PATH" is checked. * Flask (http://flask.pocoo.org/) * ``pip install flask`` * python-protobuf (https://pypi.org/project/protobuf/) @@ -269,7 +292,8 @@ Docker ## Note Future Zwift updates may break zoffline until it's updated. While zoffline is -enabled Zwift updates will not be installed. +enabled Zwift updates will not be installed. If a zoffline update broke +something, check the ``CHANGELOG`` for possible changes that need to be made. Don't expose zoffline to the internet, it was not designed with that in mind. diff --git a/scripts/get_profile.py b/scripts/get_profile.py index 58ad5f7..d93f86d 100755 --- a/scripts/get_profile.py +++ b/scripts/get_profile.py @@ -3,36 +3,38 @@ # # Adapted from https://github.com/jlemon/zlogger/blob/master/get_riders.py # -#The MIT License (MIT) +# The MIT License (MIT) # -#Copyright (c) 2016 Jonathan Lemon +# 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: +# 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 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 NONINFRINGEMENT. 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. +# 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 sys, argparse, getpass -import requests +import argparse +import getpass import json -import os, time, stat -from collections import namedtuple +import os +import requests +import sys + if getattr(sys, 'frozen', False): - # If we're running as a pyinstaller bundle + # 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__)) @@ -45,6 +47,7 @@ except NameError: global args global dbh + def post_credentials(session, username, password): # Credentials POSTing and tokens retrieval # POST https://secure.zwift.com/auth/realms/zwift/tokens/access/codes @@ -67,8 +70,8 @@ def post_credentials(session, username, password): "password": password, "grant_type": "password", }, - allow_redirects = False, - verify = args.verifyCert, + allow_redirects=False, + verify=args.verifyCert, ) if args.verbose: @@ -84,6 +87,11 @@ def post_credentials(session, username, password): except requests.exceptions.RequestException as e: print('HTTP Request failed: %s' % e) + except KeyError as e: + print('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/ @@ -99,7 +107,7 @@ def query_player_profile(session, access_token): "Authorization": "Bearer %s" % access_token, "Accept-Language": "en-us", }, - verify = args.verifyCert, + verify=args.verifyCert, ) if args.verbose: @@ -131,7 +139,7 @@ def logout(session, refresh_token): "client_id": "Zwift_Mobile_Link", "refresh_token": refresh_token, }, - verify = args.verifyCert, + verify=args.verifyCert, ) if args.verbose: print('Response HTTP Status Code: {status_code}'.format( @@ -141,10 +149,12 @@ def logout(session, 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 main(argv): global args global dbh @@ -152,32 +162,39 @@ def main(argv): access_token = None cookies = None - parser = argparse.ArgumentParser(description = 'Zwift Profile Fetcher') + parser = argparse.ArgumentParser(description='Zwift Profile Fetcher') parser.add_argument('-v', '--verbose', action='store_true', - help='Verbose output') + help='Verbose output') parser.add_argument('--dont-check-certificates', action='store_false', - dest='verifyCert', default=True) + dest='verifyCert', default=True) parser.add_argument('-u', '--user', help='Zwift user name') args = parser.parse_args() -# if args.user: -# password = getpass.getpass("Password for %s? " % args.user) -# else: -# file = os.environ['HOME'] + '/.zwift_cred.json' -# with open(file) as f: -# try: -# cred = json.load(f) -# except ValueError, se: -# sys.exit('"%s": %s' % (args.output, se)) -# f.close -# args.user = cred['user'] -# password = cred['pass'] + # if args.user: + # password = getpass.getpass("Password for %s? " % args.user) + # else: + # file = os.environ['HOME'] + '/.zwift_cred.json' + # with open(file) as f: + # try: + # cred = json.load(f) + # except ValueError, se: + # sys.exit('"%s": %s' % (args.output, se)) + # f.close + # args.user = cred['user'] + # password = cred['pass'] if args.user: username = args.user else: username = input("Enter Zwift login (e-mail): ") - password = getpass.getpass("Enter password: ") + if not sys.stdin.isatty(): # This terminal cannot support input without displaying text + print(f'*WARNING* The current shell ({os.name}) cannot support hidden text entry.') + print(f'Your password entry WILL BE VISIBLE.') + print(f'If you are running a bash shell under windows, try executing this program via winpty:') + print(f'>winpty python {argv[0]}') + password = input("Enter password (will be shown):") + else: + password = getpass.getpass("Enter password: ") session = requests.session() @@ -188,10 +205,11 @@ def main(argv): logout(session, refresh_token) + if __name__ == '__main__': try: main(sys.argv) except KeyboardInterrupt: pass except SystemExit as se: - print("ERROR:",se) + print("ERROR:", se) diff --git a/zwift_offline.py b/zwift_offline.py index 7e2ad0f..113ed73 100644 --- a/zwift_offline.py +++ b/zwift_offline.py @@ -1547,6 +1547,7 @@ def before_first_request(): move_old_profile() init_database() db.create_all(app=app) + db.session.commit() # in case create_all created a table check_columns() db.session.close() send_message_thread = threading.Thread(target=send_server_back_online_message)