mirror of
https://github.com/zoffline/zwift-offline.git
synced 2025-12-05 20:40:03 -08:00
Initial code drop
This commit is contained in:
2
.dockerignore
Normal file
2
.dockerignore
Normal file
@@ -0,0 +1,2 @@
|
||||
.git
|
||||
.gitignore
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
*.pyc
|
||||
protobuf/*_pb2.py
|
||||
storage/*
|
||||
!storage/force_dir_in_git.txt
|
||||
16
Dockerfile
Normal file
16
Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM httpd:2.4
|
||||
MAINTAINER zoffline <zoffline@mailinator.com>
|
||||
|
||||
RUN apt-get update && apt-get install -y python-dev python-flask libapache2-mod-wsgi python-pip protobuf-compiler git
|
||||
RUN pip install --upgrade six
|
||||
RUN pip install protobuf protobuf_to_dict stravalib
|
||||
RUN ln -s /usr/lib/apache2/modules/mod_wsgi.so /usr/local/apache2/modules/
|
||||
|
||||
RUN git clone --depth 1 https://github.com/zoffline/zoffline /usr/local/apache2/htdocs/zwift-offline
|
||||
RUN cd /usr/local/apache2/htdocs/zwift-offline/protobuf && make
|
||||
RUN chown -R www-data.www-data /usr/local/apache2/htdocs/zwift-offline
|
||||
COPY apache/docker-httpd.conf /usr/local/apache2/conf/httpd.conf
|
||||
|
||||
EXPOSE 443 80
|
||||
|
||||
VOLUME /usr/local/apache2/htdocs/zwift-offline/storage
|
||||
115
README.md
115
README.md
@@ -1,2 +1,115 @@
|
||||
# zoffline
|
||||
Use Zwift offline
|
||||
|
||||
zoffline enables the use of [Zwift](http://zwift.com) offline by acting as a partial implementation
|
||||
of a Zwift server. Currently it's designed for only a single user and the UDP
|
||||
game node is not implemented.
|
||||
|
||||
## Install
|
||||
|
||||
### Step 1: Install zoffline
|
||||
|
||||
The easiest way to install zoffline is through
|
||||
[Docker](https://www.docker.com/). zoffline can either be installed on the same
|
||||
machine as Zwift or another local machine.
|
||||
|
||||
* Install Docker
|
||||
* Create the docker container with:<br>
|
||||
``docker create --name zwift-offline -p 443:443 -p 80:80 -v </path/to/host/storage>:/usr/local/apache2/htdocs/zwift-offline/storage -e TZ=<timezone> zoffline/zoffline``
|
||||
* You can optionally exclude ``-v </path/to/host/storage>:/usr/local/apache2/htdocs/zwift-offline/storage`` if you don't care if zoffline data is persistent across zoffline updates.
|
||||
* The path you pass to ``-v`` will likely need to be world readable and writable.
|
||||
* A list of valid ``<timezone>`` values (e.g. America/New_York) can be found [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
|
||||
* Adding ``--restart unless-stopped`` will make zoffline start on boot if you have Docker v1.9.0 or greater.
|
||||
* Start zoffline with:
|
||||
``docker start zwift-offline``
|
||||
|
||||
If you don't use the Docker container you will need to set up an Apache server (or
|
||||
write your own nginx/uWSGI configuration and use nginx) and install the
|
||||
dependencies listed below. The necessary Apache configuration is inside the
|
||||
``apache`` subdir. You'll also need to run ``make`` inside the ``protobuf``
|
||||
subdirectory.
|
||||
|
||||
|
||||
### Step 2: Configure Zwift client to use zoffline
|
||||
I've only done this with Windows 10 so your mileage may vary on other versions.
|
||||
|
||||
* Install Zwift
|
||||
* If your Zwift version is newer than 1.0.21832 you may have to uninstall, then reinstall after installing zoffline.
|
||||
* If your Zwift version is 1.0.21832, you're all set.
|
||||
* If Zwift is not installed install it after installing zoffline (1.0.21832 will be installed instead of the latest).
|
||||
* On your Windows machine running Zwift, copy the following files in this repo to a known location:
|
||||
* ``ssl/cert-us-or.p12``
|
||||
* ``ssl/cert-zwift-secure.p12``
|
||||
* ``ssl/cert-us-or.pem``
|
||||
* ``ssl/cert-zwift-secure.pem``
|
||||
* Open Command Prompt as an admin, cd to that location and run
|
||||
* ``certutil.exe -importpfx Root cert-us-or.p12``
|
||||
* ``certutil.exe -importpfx Root cert-zwift-secure.p12``
|
||||
* Open Notepad as an admin and open ``C:\Program Files (x86)\Zwift\data\cacert.pem``
|
||||
* Append the contents of ``ssl/cert-us-or.pem`` to cacert.pem
|
||||
* Append the contents of ``ssl/cert-zwift-secure.pem`` to cacert.pem
|
||||
* Open Notepad as an admin and open ``C:\Windows\System32\Drivers\etc\hosts``
|
||||
* Append this line: ``<zoffline ip> us-or-rly101.zwift.com secure.zwift.com cdn.zwift.com``
|
||||
<br />(Where ``<zoffline ip>`` is the ip address of the machine running zoffline. If
|
||||
it's running on the same machine as Zwift, use ``127.0.0.1`` as the ip.)
|
||||
|
||||
Why: We need to redirect Zwift to use zoffline and convince Windows 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.
|
||||
|
||||
#### Enabling/Disabling zoffline
|
||||
|
||||
To use Zwift online like normal, comment out or remove the line added to the ``hosts``
|
||||
file before starting Zwift.
|
||||
|
||||
|
||||
### Step 3 [OPTIONAL]: Obtain current Zwift profile
|
||||
|
||||
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 (name, weight,
|
||||
height, etc.)
|
||||
|
||||
To obtain your current profile:
|
||||
* Run ``scripts/get_profile.py -u <your_zwift_username>``
|
||||
* Move the resulting profile.bin (saved in whatever directory you ran get_profile.py in) into the ``storage`` directory.
|
||||
* If using Docker, move profile.bin into the path you passed to ``-v``
|
||||
|
||||
|
||||
## Dependencies
|
||||
|
||||
Docker
|
||||
|
||||
-or-
|
||||
|
||||
* protobuf compiler
|
||||
* ``apt-get install protobuf-compiler`` (on Debian/Ubuntu)
|
||||
* GNU Make
|
||||
* ``apt-get install make`` (on Debian/Ubuntu)
|
||||
* python-protobuf
|
||||
* ``pip install python-protobuf``
|
||||
* protobuf_to_dict (https://github.com/benhodgson/protobuf-to-dict)
|
||||
* ``pip install protobuf_to_dict``
|
||||
* OPTIONAL: stravalib (https://github.com/hozn/stravalib)
|
||||
* ``pip install stravalib``
|
||||
* Add your Strava API token to ``storage/strava_token.txt``
|
||||
|
||||
|
||||
## Known issues
|
||||
|
||||
* Segment results always show up as having just occurred.
|
||||
|
||||
|
||||
## Note
|
||||
|
||||
Future Zwift updates may break zoffline until it's updated. While zoffline is
|
||||
enabled Zwift updates will not be installed.
|
||||
|
||||
Don't expose zoffline to the internet, it was not designed with that in mind.
|
||||
|
||||
|
||||
## Disclaimer
|
||||
|
||||
Zwift is a trademark of Zwift, Inc., which is not affiliated with the maker of
|
||||
this project and does not endorse this project.
|
||||
|
||||
All product and company names are trademarks of their respective holders. Use of
|
||||
them does not imply any affiliation with or endorsement by them.
|
||||
|
||||
19
apache/cdn.zwift.com.conf
Normal file
19
apache/cdn.zwift.com.conf
Normal file
@@ -0,0 +1,19 @@
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /var/www/zwift-offline/cdn
|
||||
ServerName cdn.zwift.com
|
||||
|
||||
<Directory /var/www/zwift-offline/cdn/>
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
allow from all
|
||||
</Directory>
|
||||
|
||||
#ProxyPass "/gameassets/MapSchedule.xml" "http://cdn.zwift.com/gameassets/MapSchedule.xml"
|
||||
|
||||
ProxyPassMatch "/gameassets/Zwift_Updates_Root/(Zwift_[0-9]+.[0-9]+.[0-9]+)/(.*)" "http://cdn.zwift.com/gameassets/Zwift_Updates_Root/$1/$2"
|
||||
|
||||
LogLevel warn
|
||||
CustomLog /var/log/apache2/cdn_zwift_access.log combined
|
||||
ErrorLog /var/log/apache2/cdn_zwift_error.log
|
||||
</VirtualHost>
|
||||
601
apache/docker-httpd.conf
Normal file
601
apache/docker-httpd.conf
Normal file
@@ -0,0 +1,601 @@
|
||||
#
|
||||
# This is the main Apache HTTP server configuration file. It contains the
|
||||
# configuration directives that give the server its instructions.
|
||||
# See <URL:http://httpd.apache.org/docs/2.4/> for detailed information.
|
||||
# In particular, see
|
||||
# <URL:http://httpd.apache.org/docs/2.4/mod/directives.html>
|
||||
# for a discussion of each configuration directive.
|
||||
#
|
||||
# Do NOT simply read the instructions in here without understanding
|
||||
# what they do. They're here only as hints or reminders. If you are unsure
|
||||
# consult the online docs. You have been warned.
|
||||
#
|
||||
# Configuration and logfile names: If the filenames you specify for many
|
||||
# of the server's control files begin with "/" (or "drive:/" for Win32), the
|
||||
# server will use that explicit path. If the filenames do *not* begin
|
||||
# with "/", the value of ServerRoot is prepended -- so "logs/access_log"
|
||||
# with ServerRoot set to "/usr/local/apache2" will be interpreted by the
|
||||
# server as "/usr/local/apache2/logs/access_log", whereas "/logs/access_log"
|
||||
# will be interpreted as '/logs/access_log'.
|
||||
|
||||
#
|
||||
# ServerRoot: The top of the directory tree under which the server's
|
||||
# configuration, error, and log files are kept.
|
||||
#
|
||||
# Do not add a slash at the end of the directory path. If you point
|
||||
# ServerRoot at a non-local disk, be sure to specify a local disk on the
|
||||
# Mutex directive, if file-based mutexes are used. If you wish to share the
|
||||
# same ServerRoot for multiple httpd daemons, you will need to change at
|
||||
# least PidFile.
|
||||
#
|
||||
ServerRoot "/usr/local/apache2"
|
||||
|
||||
#
|
||||
# Mutex: Allows you to set the mutex mechanism and mutex file directory
|
||||
# for individual mutexes, or change the global defaults
|
||||
#
|
||||
# Uncomment and change the directory if mutexes are file-based and the default
|
||||
# mutex file directory is not on a local disk or is not appropriate for some
|
||||
# other reason.
|
||||
#
|
||||
# Mutex default:logs
|
||||
|
||||
#
|
||||
# Listen: Allows you to bind Apache to specific IP addresses and/or
|
||||
# ports, instead of the default. See also the <VirtualHost>
|
||||
# directive.
|
||||
#
|
||||
# Change this to Listen on specific IP addresses as shown below to
|
||||
# prevent Apache from glomming onto all bound IP addresses.
|
||||
#
|
||||
#Listen 12.34.56.78:80
|
||||
Listen 80
|
||||
Listen 443
|
||||
|
||||
#
|
||||
# Dynamic Shared Object (DSO) Support
|
||||
#
|
||||
# To be able to use the functionality of a module which was built as a DSO you
|
||||
# have to place corresponding `LoadModule' lines at this location so the
|
||||
# directives contained in it are actually available _before_ they are used.
|
||||
# Statically compiled modules (those listed by `httpd -l') do not need
|
||||
# to be loaded here.
|
||||
#
|
||||
# Example:
|
||||
# LoadModule foo_module modules/mod_foo.so
|
||||
#
|
||||
LoadModule authn_file_module modules/mod_authn_file.so
|
||||
#LoadModule authn_dbm_module modules/mod_authn_dbm.so
|
||||
#LoadModule authn_anon_module modules/mod_authn_anon.so
|
||||
#LoadModule authn_dbd_module modules/mod_authn_dbd.so
|
||||
#LoadModule authn_socache_module modules/mod_authn_socache.so
|
||||
LoadModule authn_core_module modules/mod_authn_core.so
|
||||
LoadModule authz_host_module modules/mod_authz_host.so
|
||||
LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
|
||||
LoadModule authz_user_module modules/mod_authz_user.so
|
||||
#LoadModule authz_dbm_module modules/mod_authz_dbm.so
|
||||
#LoadModule authz_owner_module modules/mod_authz_owner.so
|
||||
#LoadModule authz_dbd_module modules/mod_authz_dbd.so
|
||||
LoadModule authz_core_module modules/mod_authz_core.so
|
||||
#LoadModule authnz_ldap_module modules/mod_authnz_ldap.so
|
||||
#LoadModule authnz_fcgi_module modules/mod_authnz_fcgi.so
|
||||
LoadModule access_compat_module modules/mod_access_compat.so
|
||||
LoadModule auth_basic_module modules/mod_auth_basic.so
|
||||
#LoadModule auth_form_module modules/mod_auth_form.so
|
||||
#LoadModule auth_digest_module modules/mod_auth_digest.so
|
||||
#LoadModule allowmethods_module modules/mod_allowmethods.so
|
||||
#LoadModule isapi_module modules/mod_isapi.so
|
||||
#LoadModule file_cache_module modules/mod_file_cache.so
|
||||
#LoadModule cache_module modules/mod_cache.so
|
||||
#LoadModule cache_disk_module modules/mod_cache_disk.so
|
||||
#LoadModule cache_socache_module modules/mod_cache_socache.so
|
||||
#LoadModule socache_shmcb_module modules/mod_socache_shmcb.so
|
||||
#LoadModule socache_dbm_module modules/mod_socache_dbm.so
|
||||
#LoadModule socache_memcache_module modules/mod_socache_memcache.so
|
||||
#LoadModule watchdog_module modules/mod_watchdog.so
|
||||
#LoadModule macro_module modules/mod_macro.so
|
||||
#LoadModule dbd_module modules/mod_dbd.so
|
||||
#LoadModule bucketeer_module modules/mod_bucketeer.so
|
||||
#LoadModule dumpio_module modules/mod_dumpio.so
|
||||
#LoadModule echo_module modules/mod_echo.so
|
||||
#LoadModule example_hooks_module modules/mod_example_hooks.so
|
||||
#LoadModule case_filter_module modules/mod_case_filter.so
|
||||
#LoadModule case_filter_in_module modules/mod_case_filter_in.so
|
||||
#LoadModule example_ipc_module modules/mod_example_ipc.so
|
||||
#LoadModule buffer_module modules/mod_buffer.so
|
||||
#LoadModule data_module modules/mod_data.so
|
||||
#LoadModule ratelimit_module modules/mod_ratelimit.so
|
||||
LoadModule reqtimeout_module modules/mod_reqtimeout.so
|
||||
#LoadModule ext_filter_module modules/mod_ext_filter.so
|
||||
#LoadModule request_module modules/mod_request.so
|
||||
#LoadModule include_module modules/mod_include.so
|
||||
LoadModule filter_module modules/mod_filter.so
|
||||
#LoadModule reflector_module modules/mod_reflector.so
|
||||
#LoadModule substitute_module modules/mod_substitute.so
|
||||
#LoadModule sed_module modules/mod_sed.so
|
||||
#LoadModule charset_lite_module modules/mod_charset_lite.so
|
||||
#LoadModule deflate_module modules/mod_deflate.so
|
||||
#LoadModule xml2enc_module modules/mod_xml2enc.so
|
||||
#LoadModule proxy_html_module modules/mod_proxy_html.so
|
||||
LoadModule mime_module modules/mod_mime.so
|
||||
#LoadModule ldap_module modules/mod_ldap.so
|
||||
LoadModule log_config_module modules/mod_log_config.so
|
||||
#LoadModule log_debug_module modules/mod_log_debug.so
|
||||
#LoadModule log_forensic_module modules/mod_log_forensic.so
|
||||
#LoadModule logio_module modules/mod_logio.so
|
||||
#LoadModule lua_module modules/mod_lua.so
|
||||
LoadModule env_module modules/mod_env.so
|
||||
#LoadModule mime_magic_module modules/mod_mime_magic.so
|
||||
#LoadModule cern_meta_module modules/mod_cern_meta.so
|
||||
#LoadModule expires_module modules/mod_expires.so
|
||||
LoadModule headers_module modules/mod_headers.so
|
||||
#LoadModule ident_module modules/mod_ident.so
|
||||
#LoadModule usertrack_module modules/mod_usertrack.so
|
||||
#LoadModule unique_id_module modules/mod_unique_id.so
|
||||
LoadModule setenvif_module modules/mod_setenvif.so
|
||||
LoadModule version_module modules/mod_version.so
|
||||
#LoadModule remoteip_module modules/mod_remoteip.so
|
||||
LoadModule proxy_module modules/mod_proxy.so
|
||||
#LoadModule proxy_connect_module modules/mod_proxy_connect.so
|
||||
#LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
|
||||
LoadModule proxy_http_module modules/mod_proxy_http.so
|
||||
#LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
|
||||
#LoadModule proxy_scgi_module modules/mod_proxy_scgi.so
|
||||
#LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so
|
||||
#LoadModule proxy_wstunnel_module modules/mod_proxy_wstunnel.so
|
||||
#LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
|
||||
#LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
|
||||
#LoadModule proxy_express_module modules/mod_proxy_express.so
|
||||
#LoadModule proxy_hcheck_module modules/mod_proxy_hcheck.so
|
||||
#LoadModule session_module modules/mod_session.so
|
||||
#LoadModule session_cookie_module modules/mod_session_cookie.so
|
||||
#LoadModule session_crypto_module modules/mod_session_crypto.so
|
||||
#LoadModule session_dbd_module modules/mod_session_dbd.so
|
||||
#LoadModule slotmem_shm_module modules/mod_slotmem_shm.so
|
||||
#LoadModule slotmem_plain_module modules/mod_slotmem_plain.so
|
||||
LoadModule ssl_module modules/mod_ssl.so
|
||||
#LoadModule optional_hook_export_module modules/mod_optional_hook_export.so
|
||||
#LoadModule optional_hook_import_module modules/mod_optional_hook_import.so
|
||||
#LoadModule optional_fn_import_module modules/mod_optional_fn_import.so
|
||||
#LoadModule optional_fn_export_module modules/mod_optional_fn_export.so
|
||||
#LoadModule dialup_module modules/mod_dialup.so
|
||||
#LoadModule http2_module modules/mod_http2.so
|
||||
#LoadModule proxy_http2_module modules/mod_proxy_http2.so
|
||||
#LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
|
||||
#LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
|
||||
#LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so
|
||||
#LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so
|
||||
LoadModule unixd_module modules/mod_unixd.so
|
||||
#LoadModule heartbeat_module modules/mod_heartbeat.so
|
||||
#LoadModule heartmonitor_module modules/mod_heartmonitor.so
|
||||
#LoadModule dav_module modules/mod_dav.so
|
||||
LoadModule status_module modules/mod_status.so
|
||||
LoadModule autoindex_module modules/mod_autoindex.so
|
||||
#LoadModule asis_module modules/mod_asis.so
|
||||
#LoadModule info_module modules/mod_info.so
|
||||
#LoadModule suexec_module modules/mod_suexec.so
|
||||
<IfModule !mpm_prefork_module>
|
||||
#LoadModule cgid_module modules/mod_cgid.so
|
||||
</IfModule>
|
||||
<IfModule mpm_prefork_module>
|
||||
#LoadModule cgi_module modules/mod_cgi.so
|
||||
</IfModule>
|
||||
#LoadModule dav_fs_module modules/mod_dav_fs.so
|
||||
#LoadModule dav_lock_module modules/mod_dav_lock.so
|
||||
#LoadModule vhost_alias_module modules/mod_vhost_alias.so
|
||||
#LoadModule negotiation_module modules/mod_negotiation.so
|
||||
LoadModule dir_module modules/mod_dir.so
|
||||
#LoadModule imagemap_module modules/mod_imagemap.so
|
||||
#LoadModule actions_module modules/mod_actions.so
|
||||
#LoadModule speling_module modules/mod_speling.so
|
||||
#LoadModule userdir_module modules/mod_userdir.so
|
||||
LoadModule alias_module modules/mod_alias.so
|
||||
#LoadModule rewrite_module modules/mod_rewrite.so
|
||||
LoadModule wsgi_module modules/mod_wsgi.so
|
||||
|
||||
<IfModule unixd_module>
|
||||
#
|
||||
# If you wish httpd to run as a different user or group, you must run
|
||||
# httpd as root initially and it will switch.
|
||||
#
|
||||
# User/Group: The name (or #number) of the user/group to run httpd as.
|
||||
# It is usually good practice to create a dedicated user and group for
|
||||
# running httpd, as with most system services.
|
||||
#
|
||||
User daemon
|
||||
Group daemon
|
||||
|
||||
</IfModule>
|
||||
|
||||
# 'Main' server configuration
|
||||
#
|
||||
# The directives in this section set up the values used by the 'main'
|
||||
# server, which responds to any requests that aren't handled by a
|
||||
# <VirtualHost> definition. These values also provide defaults for
|
||||
# any <VirtualHost> containers you may define later in the file.
|
||||
#
|
||||
# All of these directives may appear inside <VirtualHost> containers,
|
||||
# in which case these default settings will be overridden for the
|
||||
# virtual host being defined.
|
||||
#
|
||||
|
||||
#
|
||||
# ServerAdmin: Your address, where problems with the server should be
|
||||
# e-mailed. This address appears on some server-generated pages, such
|
||||
# as error documents. e.g. admin@your-domain.com
|
||||
#
|
||||
ServerAdmin you@example.com
|
||||
|
||||
#
|
||||
# ServerName gives the name and port that the server uses to identify itself.
|
||||
# This can often be determined automatically, but we recommend you specify
|
||||
# it explicitly to prevent problems during startup.
|
||||
#
|
||||
# If your host doesn't have a registered DNS name, enter its IP address here.
|
||||
#
|
||||
#ServerName www.example.com:80
|
||||
|
||||
#
|
||||
# Deny access to the entirety of your server's filesystem. You must
|
||||
# explicitly permit access to web content directories in other
|
||||
# <Directory> blocks below.
|
||||
#
|
||||
<Directory />
|
||||
AllowOverride none
|
||||
Require all denied
|
||||
</Directory>
|
||||
|
||||
#
|
||||
# Note that from this point forward you must specifically allow
|
||||
# particular features to be enabled - so if something's not working as
|
||||
# you might expect, make sure that you have specifically enabled it
|
||||
# below.
|
||||
#
|
||||
|
||||
#
|
||||
# DocumentRoot: The directory out of which you will serve your
|
||||
# documents. By default, all requests are taken from this directory, but
|
||||
# symbolic links and aliases may be used to point to other locations.
|
||||
#
|
||||
DocumentRoot "/usr/local/apache2/htdocs"
|
||||
<Directory "/usr/local/apache2/htdocs">
|
||||
#
|
||||
# Possible values for the Options directive are "None", "All",
|
||||
# or any combination of:
|
||||
# Indexes Includes FollowSymLinks SymLinksifOwnerMatch ExecCGI MultiViews
|
||||
#
|
||||
# Note that "MultiViews" must be named *explicitly* --- "Options All"
|
||||
# doesn't give it to you.
|
||||
#
|
||||
# The Options directive is both complicated and important. Please see
|
||||
# http://httpd.apache.org/docs/2.4/mod/core.html#options
|
||||
# for more information.
|
||||
#
|
||||
Options Indexes FollowSymLinks
|
||||
|
||||
#
|
||||
# AllowOverride controls what directives may be placed in .htaccess files.
|
||||
# It can be "All", "None", or any combination of the keywords:
|
||||
# AllowOverride FileInfo AuthConfig Limit
|
||||
#
|
||||
AllowOverride None
|
||||
|
||||
#
|
||||
# Controls who can get stuff from this server.
|
||||
#
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
#
|
||||
# DirectoryIndex: sets the file that Apache will serve if a directory
|
||||
# is requested.
|
||||
#
|
||||
<IfModule dir_module>
|
||||
DirectoryIndex index.html
|
||||
</IfModule>
|
||||
|
||||
#
|
||||
# The following lines prevent .htaccess and .htpasswd files from being
|
||||
# viewed by Web clients.
|
||||
#
|
||||
<Files ".ht*">
|
||||
Require all denied
|
||||
</Files>
|
||||
|
||||
#
|
||||
# ErrorLog: The location of the error log file.
|
||||
# If you do not specify an ErrorLog directive within a <VirtualHost>
|
||||
# container, error messages relating to that virtual host will be
|
||||
# logged here. If you *do* define an error logfile for a <VirtualHost>
|
||||
# container, that host's errors will be logged there and not here.
|
||||
#
|
||||
ErrorLog /proc/self/fd/2
|
||||
|
||||
#
|
||||
# LogLevel: Control the number of messages logged to the error_log.
|
||||
# Possible values include: debug, info, notice, warn, error, crit,
|
||||
# alert, emerg.
|
||||
#
|
||||
LogLevel warn
|
||||
|
||||
<IfModule log_config_module>
|
||||
#
|
||||
# The following directives define some format nicknames for use with
|
||||
# a CustomLog directive (see below).
|
||||
#
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b" common
|
||||
|
||||
<IfModule logio_module>
|
||||
# You need to enable mod_logio.c to use %I and %O
|
||||
LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio
|
||||
</IfModule>
|
||||
|
||||
#
|
||||
# The location and format of the access logfile (Common Logfile Format).
|
||||
# If you do not define any access logfiles within a <VirtualHost>
|
||||
# container, they will be logged here. Contrariwise, if you *do*
|
||||
# define per-<VirtualHost> access logfiles, transactions will be
|
||||
# logged therein and *not* in this file.
|
||||
#
|
||||
CustomLog /proc/self/fd/1 common
|
||||
|
||||
#
|
||||
# If you prefer a logfile with access, agent, and referer information
|
||||
# (Combined Logfile Format) you can use the following directive.
|
||||
#
|
||||
#CustomLog "logs/access_log" combined
|
||||
</IfModule>
|
||||
|
||||
<IfModule alias_module>
|
||||
#
|
||||
# Redirect: Allows you to tell clients about documents that used to
|
||||
# exist in your server's namespace, but do not anymore. The client
|
||||
# will make a new request for the document at its new location.
|
||||
# Example:
|
||||
# Redirect permanent /foo http://www.example.com/bar
|
||||
|
||||
#
|
||||
# Alias: Maps web paths into filesystem paths and is used to
|
||||
# access content that does not live under the DocumentRoot.
|
||||
# Example:
|
||||
# Alias /webpath /full/filesystem/path
|
||||
#
|
||||
# If you include a trailing / on /webpath then the server will
|
||||
# require it to be present in the URL. You will also likely
|
||||
# need to provide a <Directory> section to allow access to
|
||||
# the filesystem path.
|
||||
|
||||
#
|
||||
# ScriptAlias: This controls which directories contain server scripts.
|
||||
# ScriptAliases are essentially the same as Aliases, except that
|
||||
# documents in the target directory are treated as applications and
|
||||
# run by the server when requested rather than as documents sent to the
|
||||
# client. The same rules about trailing "/" apply to ScriptAlias
|
||||
# directives as to Alias.
|
||||
#
|
||||
ScriptAlias /cgi-bin/ "/usr/local/apache2/cgi-bin/"
|
||||
|
||||
</IfModule>
|
||||
|
||||
<IfModule cgid_module>
|
||||
#
|
||||
# ScriptSock: On threaded servers, designate the path to the UNIX
|
||||
# socket used to communicate with the CGI daemon of mod_cgid.
|
||||
#
|
||||
#Scriptsock cgisock
|
||||
</IfModule>
|
||||
|
||||
#
|
||||
# "/usr/local/apache2/cgi-bin" should be changed to whatever your ScriptAliased
|
||||
# CGI directory exists, if you have that configured.
|
||||
#
|
||||
<Directory "/usr/local/apache2/cgi-bin">
|
||||
AllowOverride None
|
||||
Options None
|
||||
Require all granted
|
||||
</Directory>
|
||||
|
||||
<IfModule headers_module>
|
||||
#
|
||||
# Avoid passing HTTP_PROXY environment to CGI's on this or any proxied
|
||||
# backend servers which have lingering "httpoxy" defects.
|
||||
# 'Proxy' request header is undefined by the IETF, not listed by IANA
|
||||
#
|
||||
RequestHeader unset Proxy early
|
||||
</IfModule>
|
||||
|
||||
<IfModule mime_module>
|
||||
#
|
||||
# TypesConfig points to the file containing the list of mappings from
|
||||
# filename extension to MIME-type.
|
||||
#
|
||||
TypesConfig conf/mime.types
|
||||
|
||||
#
|
||||
# AddType allows you to add to or override the MIME configuration
|
||||
# file specified in TypesConfig for specific file types.
|
||||
#
|
||||
#AddType application/x-gzip .tgz
|
||||
#
|
||||
# AddEncoding allows you to have certain browsers uncompress
|
||||
# information on the fly. Note: Not all browsers support this.
|
||||
#
|
||||
#AddEncoding x-compress .Z
|
||||
#AddEncoding x-gzip .gz .tgz
|
||||
#
|
||||
# If the AddEncoding directives above are commented-out, then you
|
||||
# probably should define those extensions to indicate media types:
|
||||
#
|
||||
AddType application/x-compress .Z
|
||||
AddType application/x-gzip .gz .tgz
|
||||
|
||||
#
|
||||
# AddHandler allows you to map certain file extensions to "handlers":
|
||||
# actions unrelated to filetype. These can be either built into the server
|
||||
# or added with the Action directive (see below)
|
||||
#
|
||||
# To use CGI scripts outside of ScriptAliased directories:
|
||||
# (You will also need to add "ExecCGI" to the "Options" directive.)
|
||||
#
|
||||
#AddHandler cgi-script .cgi
|
||||
|
||||
# For type maps (negotiated resources):
|
||||
#AddHandler type-map var
|
||||
|
||||
#
|
||||
# Filters allow you to process content before it is sent to the client.
|
||||
#
|
||||
# To parse .shtml files for server-side includes (SSI):
|
||||
# (You will also need to add "Includes" to the "Options" directive.)
|
||||
#
|
||||
#AddType text/html .shtml
|
||||
#AddOutputFilter INCLUDES .shtml
|
||||
</IfModule>
|
||||
|
||||
#
|
||||
# The mod_mime_magic module allows the server to use various hints from the
|
||||
# contents of the file itself to determine its type. The MIMEMagicFile
|
||||
# directive tells the module where the hint definitions are located.
|
||||
#
|
||||
#MIMEMagicFile conf/magic
|
||||
|
||||
#
|
||||
# Customizable error responses come in three flavors:
|
||||
# 1) plain text 2) local redirects 3) external redirects
|
||||
#
|
||||
# Some examples:
|
||||
#ErrorDocument 500 "The server made a boo boo."
|
||||
#ErrorDocument 404 /missing.html
|
||||
#ErrorDocument 404 "/cgi-bin/missing_handler.pl"
|
||||
#ErrorDocument 402 http://www.example.com/subscription_info.html
|
||||
#
|
||||
|
||||
#
|
||||
# MaxRanges: Maximum number of Ranges in a request before
|
||||
# returning the entire resource, or one of the special
|
||||
# values 'default', 'none' or 'unlimited'.
|
||||
# Default setting is to accept 200 Ranges.
|
||||
#MaxRanges unlimited
|
||||
|
||||
#
|
||||
# EnableMMAP and EnableSendfile: On systems that support it,
|
||||
# memory-mapping or the sendfile syscall may be used to deliver
|
||||
# files. This usually improves server performance, but must
|
||||
# be turned off when serving from networked-mounted
|
||||
# filesystems or if support for these functions is otherwise
|
||||
# broken on your system.
|
||||
# Defaults: EnableMMAP On, EnableSendfile Off
|
||||
#
|
||||
#EnableMMAP off
|
||||
#EnableSendfile on
|
||||
|
||||
# Supplemental configuration
|
||||
#
|
||||
# The configuration files in the conf/extra/ directory can be
|
||||
# included to add extra features or to modify the default configuration of
|
||||
# the server, or you may simply copy their contents here and change as
|
||||
# necessary.
|
||||
|
||||
# Server-pool management (MPM specific)
|
||||
#Include conf/extra/httpd-mpm.conf
|
||||
|
||||
# Multi-language error messages
|
||||
#Include conf/extra/httpd-multilang-errordoc.conf
|
||||
|
||||
# Fancy directory listings
|
||||
#Include conf/extra/httpd-autoindex.conf
|
||||
|
||||
# Language settings
|
||||
#Include conf/extra/httpd-languages.conf
|
||||
|
||||
# User home directories
|
||||
#Include conf/extra/httpd-userdir.conf
|
||||
|
||||
# Real-time info on requests and configuration
|
||||
#Include conf/extra/httpd-info.conf
|
||||
|
||||
# Virtual hosts
|
||||
#Include conf/extra/httpd-vhosts.conf
|
||||
|
||||
# Local access to the Apache HTTP Server Manual
|
||||
#Include conf/extra/httpd-manual.conf
|
||||
|
||||
# Distributed authoring and versioning (WebDAV)
|
||||
#Include conf/extra/httpd-dav.conf
|
||||
|
||||
# Various default settings
|
||||
#Include conf/extra/httpd-default.conf
|
||||
|
||||
# Configure mod_proxy_html to understand HTML4/XHTML1
|
||||
<IfModule proxy_html_module>
|
||||
Include conf/extra/proxy-html.conf
|
||||
</IfModule>
|
||||
|
||||
# Secure (SSL/TLS) connections
|
||||
#Include conf/extra/httpd-ssl.conf
|
||||
#
|
||||
# Note: The following must must be present to support
|
||||
# starting without SSL on platforms with no /dev/random equivalent
|
||||
# but a statically compiled-in mod_ssl.
|
||||
#
|
||||
<IfModule ssl_module>
|
||||
SSLRandomSeed startup builtin
|
||||
SSLRandomSeed connect builtin
|
||||
</IfModule>
|
||||
|
||||
<VirtualHost *:80>
|
||||
DocumentRoot /usr/local/apache2/htdocs/zwift-offline/cdn
|
||||
ServerName cdn.zwift.com
|
||||
|
||||
<Directory /usr/local/apache2/htdocs/zwift-offline/cdn/>
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
allow from all
|
||||
</Directory>
|
||||
|
||||
#ProxyPass "/gameassets/MapSchedule.xml" "http://cdn.zwift.com/gameassets/MapSchedule.xml"
|
||||
|
||||
ProxyPassMatch "/gameassets/Zwift_Updates_Root/(Zwift_[0-9]+.[0-9]+.[0-9]+)/(.*)" "http://cdn.zwift.com/gameassets/Zwift_Updates_Root/$1/$2"
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
DocumentRoot /usr/local/apache2/htdocs/zwift-offline
|
||||
ServerName secure.zwift.com
|
||||
|
||||
WSGIScriptAlias / /usr/local/apache2/htdocs/zwift-offline/secure.zwift.com.wsgi
|
||||
|
||||
<Directory /usr/local/apache2/htdocs/zwift-offline/>
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
allow from all
|
||||
</Directory>
|
||||
|
||||
Alias /auth/resources /usr/local/apache2/htdocs/zwift-offline/auth/resources
|
||||
<Directory /usr/local/apache2/htdocs/zwift-offline/auth/resources/>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
SSLCertificateFile /usr/local/apache2/htdocs/zwift-offline/ssl/cert-secure-zwift.pem
|
||||
SSLCertificateKeyFile /usr/local/apache2/htdocs/zwift-offline/ssl/key-secure-zwift.pem
|
||||
</VirtualHost>
|
||||
|
||||
<VirtualHost *:443>
|
||||
DocumentRoot /usr/local/apache2/htdocs/zwift-offline
|
||||
ServerName us-or-rly101.zwift.com
|
||||
|
||||
WSGIScriptAlias / /usr/local/apache2/htdocs/zwift-offline/us-or-rly101.zwift.com.wsgi
|
||||
|
||||
<Directory /usr/local/apache2/htdocs/zwift-offline/>
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
allow from all
|
||||
</Directory>
|
||||
|
||||
SSLCertificateFile /usr/local/apache2/htdocs/zwift-offline/ssl/cert-us-or.pem
|
||||
SSLCertificateKeyFile /usr/local/apache2/htdocs/zwift-offline/ssl/key-us-or.pem
|
||||
</VirtualHost>
|
||||
26
apache/secure.zwift.com.conf
Normal file
26
apache/secure.zwift.com.conf
Normal file
@@ -0,0 +1,26 @@
|
||||
<VirtualHost *:443>
|
||||
DocumentRoot /var/www/zwift-offline
|
||||
ServerName secure.zwift.com
|
||||
|
||||
WSGIScriptAlias / /var/www/zwift-offline/secure.zwift.com.wsgi
|
||||
|
||||
<Directory /var/www/zwift-offline/>
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
allow from all
|
||||
</Directory>
|
||||
|
||||
Alias /auth/resources /var/www/zwift-offline/auth/resources
|
||||
<Directory /var/www/zwift-offline/auth/resources/>
|
||||
Order allow,deny
|
||||
Allow from all
|
||||
</Directory>
|
||||
|
||||
LogLevel warn
|
||||
CustomLog /var/log/apache2/secure-zwift_access.log combined
|
||||
ErrorLog /var/log/apache2/secure-zwift_error.log
|
||||
|
||||
SSLCertificateFile /var/www/zwift-offline/ssl/cert-secure-zwift.pem
|
||||
SSLCertificateKeyFile /var/www/zwift-offline/ssl/key-secure-zwift.pem
|
||||
</VirtualHost>
|
||||
20
apache/us-or-rly101.zwift.com.conf
Normal file
20
apache/us-or-rly101.zwift.com.conf
Normal file
@@ -0,0 +1,20 @@
|
||||
<VirtualHost *:443>
|
||||
DocumentRoot /var/www/zwift-offline
|
||||
ServerName us-or-rly101.zwift.com
|
||||
|
||||
WSGIScriptAlias / /var/www/zwift-offline/us-or-rly101.zwift.com.wsgi
|
||||
|
||||
<Directory /var/www/zwift-offline/>
|
||||
Options -Indexes
|
||||
AllowOverride None
|
||||
Order allow,deny
|
||||
allow from all
|
||||
</Directory>
|
||||
|
||||
LogLevel warn
|
||||
CustomLog /var/log/apache2/us-or-rly101_access.log combined
|
||||
ErrorLog /var/log/apache2/us-or-rly101_error.log
|
||||
|
||||
SSLCertificateFile /var/www/zwift-offline/ssl/cert-us-or.pem
|
||||
SSLCertificateKeyFile /var/www/zwift-offline/ssl/key-us-or.pem
|
||||
</VirtualHost>
|
||||
52
auth_server.py
Executable file
52
auth_server.py
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import os
|
||||
import time
|
||||
from flask import Flask, request, jsonify, redirect
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
|
||||
|
||||
@app.route('/auth/rb_bf03269xbi', methods=['POST'])
|
||||
def api_auth():
|
||||
return 'OK(Java)'
|
||||
|
||||
|
||||
@app.route('/auth/realms/zwift/protocol/openid-connect/auth', methods=['GET'])
|
||||
def auth_realms_zwift_protocol_openid_connect_auth():
|
||||
return redirect("http://zwift/?code=abc", 302)
|
||||
|
||||
|
||||
@app.route('/auth/realms/zwift/login-actions/request/login', methods=['GET', 'POST'])
|
||||
def auth_realms_zwift_login_actions_request_login():
|
||||
return redirect("http://zwift/?code=abc", 302)
|
||||
|
||||
|
||||
@app.route('/auth/realms/zwift/protocol/openid-connect/registrations', methods=['GET'])
|
||||
def auth_realms_zwift_protocol_openid_connect_registrations():
|
||||
return redirect("http://zwift/?code=abc", 302)
|
||||
|
||||
|
||||
#@app.route('/auth/realms/zwift/protocol/openid-connect/logout', methods=['GET'])
|
||||
#def auth_realms_zwift_protocol_openid_connect_logout():
|
||||
# return redirect("https://secure.zwift.com/auth/realms/zwift/protocol/openid-connect/auth?client_id=Game_Launcher&response_type=code&redirect_uri=http://zwift/", code=302)
|
||||
|
||||
|
||||
# Unused as it's a direct redirect now from auth/login
|
||||
@app.route('/auth/realms/zwift/login-actions/startriding', methods=['GET'])
|
||||
def auth_realms_zwift_login_actions_startriding():
|
||||
return redirect("http://zwift/?code=abc", code=302)
|
||||
|
||||
|
||||
@app.route('/auth/realms/zwift/protocol/openid-connect/token', methods=['POST'])
|
||||
def auth_realms_zwift_protocol_openid_connect_token():
|
||||
return '{"access_token":"abc","expires_in":10800,"refresh_expires_in":2592000,"refresh_token":"abc","token_type":"bearer","id_token":"abc","not-before-policy":1408458483,"session-state":"a-b-c"}', 200
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(ssl_context=('ssl/cert-secure-zwift.pem', 'ssl/key-secure-zwift.pem'),
|
||||
port=9000,
|
||||
host='0.0.0.0',
|
||||
debug=True)
|
||||
179
cdn/gameassets/MapSchedule.xml
Normal file
179
cdn/gameassets/MapSchedule.xml
Normal file
@@ -0,0 +1,179 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<MapSchedule>
|
||||
<appointments>
|
||||
<appointment map="WATOPIA" start="2017-10-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-10-02T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-10-05T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-10-08T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-10-09T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-10-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-10-16T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-10-24T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2017-10-25T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-10-26T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-10-29T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2017-11-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-11-05T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-11-08T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-11-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-11-15T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-11-16T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2017-11-17T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-11-18T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-11-26T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-11-28T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2017-12-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-12-02T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-12-05T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-12-08T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-12-09T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-12-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-12-16T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-12-24T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2017-12-25T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2017-12-26T00:01-04"/>
|
||||
<appointment map="LONDON" start="2017-12-29T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-01-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-01-05T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-01-08T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-01-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-01-15T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-01-16T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-01-17T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-01-18T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-01-26T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-01-28T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-02-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-02-02T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-02-05T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-02-08T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-02-09T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-02-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-02-16T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-02-24T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-02-25T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-02-26T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-02-28T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-03-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-03-05T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-03-08T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-03-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-03-15T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-03-16T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-03-17T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-03-18T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-03-26T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-03-28T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-04-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-04-02T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-04-05T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-04-08T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-04-09T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-04-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-04-16T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-04-24T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-04-25T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-04-26T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-04-29T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-05-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-05-05T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-05-08T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-05-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-05-15T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-05-16T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-05-17T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-05-18T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-05-26T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-05-28T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-06-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-06-02T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-06-05T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-06-08T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-06-09T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-06-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-06-16T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-06-24T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-06-25T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-06-26T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-06-29T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-07-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-07-05T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-07-08T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-07-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-07-15T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-07-16T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-07-17T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-07-18T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-07-26T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-07-28T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-08-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-08-02T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-08-05T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-08-08T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-08-09T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-08-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-08-16T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-08-24T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-08-25T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-08-26T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-08-29T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-09-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-09-05T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-09-08T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-09-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-09-15T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-09-16T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-09-17T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-09-18T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-09-26T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-09-28T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-10-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-10-02T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-10-05T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-10-08T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-10-09T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-10-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-10-16T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-10-24T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-10-25T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-10-26T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-10-29T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-11-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-11-05T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-11-08T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-11-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-11-15T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-11-16T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-11-17T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-11-18T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-11-26T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-11-28T00:01-04"/>
|
||||
|
||||
<appointment map="WATOPIA" start="2018-12-01T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-12-02T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-12-05T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-12-08T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-12-09T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-12-14T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-12-16T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-12-24T00:01-04"/>
|
||||
<appointment map="RICHMOND" start="2018-12-25T00:01-04"/>
|
||||
<appointment map="WATOPIA" start="2018-12-26T00:01-04"/>
|
||||
<appointment map="LONDON" start="2018-12-29T00:01-04"/>
|
||||
</appointments>
|
||||
<VERSION>1</VERSION>
|
||||
</MapSchedule>
|
||||
2
cdn/gameassets/Zwift_Updates_Root/Launcher_ver_cur.xml
Normal file
2
cdn/gameassets/Zwift_Updates_Root/Launcher_ver_cur.xml
Normal file
@@ -0,0 +1,2 @@
|
||||
<Launcher version="0.0.0"
|
||||
URL="www.zwift.com"/>
|
||||
6
cdn/gameassets/Zwift_Updates_Root/Zwift_ver_cur.xml
Normal file
6
cdn/gameassets/Zwift_Updates_Root/Zwift_ver_cur.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<Zwift version="1.0.21832"
|
||||
manifest="Zwift_1.0.21832_manifest.xml"
|
||||
manifest_checksum="-940388083"
|
||||
mandatory_version="0.0.0"
|
||||
GAME_URL="https://us-or-rly101.zwift.com"
|
||||
ver_cur_checksum="1866824900"/>
|
||||
8
cdn/static/web/launcher/embed.html
Normal file
8
cdn/static/web/launcher/embed.html
Normal file
@@ -0,0 +1,8 @@
|
||||
<html>
|
||||
<head>
|
||||
<title>Loading Zwift</title>
|
||||
</head>
|
||||
<body>
|
||||
Loading Zwift...
|
||||
</body>
|
||||
</html>
|
||||
73
initialize_db.sql
Normal file
73
initialize_db.sql
Normal file
@@ -0,0 +1,73 @@
|
||||
CREATE TABLE version ( version INTEGER );
|
||||
|
||||
INSERT INTO version VALUES (0);
|
||||
|
||||
/* column names must match protobuf field names */
|
||||
|
||||
CREATE TABLE activity (
|
||||
id TEXT PRIMARY KEY, /* uint64 */
|
||||
player_id TEXT, /* uint64 */
|
||||
f3 INTEGER,
|
||||
name TEXT,
|
||||
f5 INTEGER,
|
||||
f6 INTEGER,
|
||||
start_date TEXT,
|
||||
end_date TEXT,
|
||||
distance REAL,
|
||||
avg_heart_rate REAL,
|
||||
max_heart_rate REAL,
|
||||
avg_watts REAL,
|
||||
max_watts REAL,
|
||||
avg_cadence REAL,
|
||||
max_cadence REAL,
|
||||
avg_speed REAL,
|
||||
max_speed REAL,
|
||||
calories REAL,
|
||||
total_elevation REAL,
|
||||
strava_upload_id TEXT, /* uint64 */
|
||||
strava_activity_id TEXT, /* uint64 */
|
||||
f23 INTEGER,
|
||||
fit BLOB,
|
||||
fit_filename TEXT,
|
||||
f29 INTEGER,
|
||||
date TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE goal (
|
||||
id TEXT PRIMARY KEY, /* uint64 */
|
||||
player_id TEXT, /* uint64 */
|
||||
f3 INTEGER,
|
||||
name TEXT,
|
||||
type INTEGER,
|
||||
periodicity INTEGER,
|
||||
target_distance REAL,
|
||||
target_duration REAL,
|
||||
actual_distance REAL,
|
||||
actual_duration REAL,
|
||||
created_on TEXT, /* uint64 */
|
||||
period_end_date TEXT, /* uint64 */
|
||||
f13 TEXT /* uint64 */
|
||||
);
|
||||
|
||||
CREATE TABLE segment_result (
|
||||
id TEXT PRIMARY KEY, /* uint64 */
|
||||
player_id TEXT, /* uint64 */
|
||||
f3 INTEGER,
|
||||
f4 INTEGER,
|
||||
segment_id TEXT, /* uint64 */
|
||||
event_subgroup_id TEXT, /* uint64 */
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
world_time TEXT, /* uint64 */
|
||||
finish_time_str TEXT,
|
||||
elapsed_ms TEXT, /* uint64 */
|
||||
f12 NUMERIC, /* bool */
|
||||
f13 INTEGER,
|
||||
f14 INTEGER,
|
||||
f15 INTEGER,
|
||||
f16 NUMERIC, /* bool */
|
||||
f17 TEXT,
|
||||
f18 TEXT, /* uint64 */
|
||||
f19 INTEGER,
|
||||
f20 INTEGER
|
||||
);
|
||||
12
protobuf/Makefile
Normal file
12
protobuf/Makefile
Normal file
@@ -0,0 +1,12 @@
|
||||
all:
|
||||
protoc --python_out=. activity.proto
|
||||
protoc --python_out=. segment-result.proto
|
||||
protoc --python_out=. profile.proto
|
||||
protoc --python_out=. per-session-info.proto
|
||||
protoc --python_out=. login-response.proto
|
||||
protoc --python_out=. periodic-info.proto
|
||||
protoc --python_out=. world.proto
|
||||
protoc --python_out=. goal.proto
|
||||
|
||||
clean:
|
||||
rm -f *_pb2.py *_pb2.pyc
|
||||
0
protobuf/__init__.py
Normal file
0
protobuf/__init__.py
Normal file
33
protobuf/activity.proto
Normal file
33
protobuf/activity.proto
Normal file
@@ -0,0 +1,33 @@
|
||||
message Activity {
|
||||
optional uint64 id = 1;
|
||||
required uint64 player_id = 2;
|
||||
required uint32 f3 = 3; /* world_id or player_type_id */
|
||||
required string name = 4;
|
||||
optional uint32 f5 = 5;
|
||||
|
||||
optional uint32 f6 = 6;
|
||||
required string start_date = 7;
|
||||
optional string end_date = 8;
|
||||
optional float distance = 9; /* in meters */
|
||||
optional float avg_heart_rate = 10;
|
||||
optional float max_heart_rate = 11;
|
||||
optional float avg_watts = 12;
|
||||
optional float max_watts = 13;
|
||||
optional float avg_cadence = 14;
|
||||
optional float max_cadence = 15;
|
||||
optional float avg_speed = 16; /* in m/s */
|
||||
optional float max_speed = 17; /* in m/s */
|
||||
optional float calories = 18;
|
||||
optional float total_elevation = 19;
|
||||
optional uint64 strava_upload_id = 20;
|
||||
optional uint64 strava_activity_id = 21;
|
||||
optional uint32 f23 = 23;
|
||||
optional bytes fit = 24;
|
||||
optional string fit_filename = 25;
|
||||
optional uint32 f29 = 29;
|
||||
optional string date = 31;
|
||||
}
|
||||
|
||||
message Activities {
|
||||
repeated Activity activities = 1;
|
||||
}
|
||||
19
protobuf/goal.proto
Normal file
19
protobuf/goal.proto
Normal file
@@ -0,0 +1,19 @@
|
||||
message Goal {
|
||||
optional uint64 id = 1;
|
||||
optional uint64 player_id = 2;
|
||||
optional uint32 f3 = 3; /* status or sport? */
|
||||
optional string name = 4;
|
||||
optional uint32 type = 5; /* 0=distance, 1=time */
|
||||
optional uint32 periodicity = 6; /* 0=weekly, 1=monthly */
|
||||
optional float target_distance = 7; /* in meters. set to dur for dur goals */
|
||||
optional float target_duration = 8; /* in minutes. set to dist for dist goals */
|
||||
optional float actual_distance = 9; /* in minutes. is also set for dur goals? */
|
||||
optional float actual_duration = 10; /* in meters. is also set for dist goals? */
|
||||
optional uint64 created_on = 11; /* in ms since epoch */
|
||||
optional uint64 period_end_date = 12; /* "" */
|
||||
optional uint64 f13 = 13; /* status or sport? */
|
||||
}
|
||||
|
||||
message Goals {
|
||||
repeated Goal goals = 1;
|
||||
}
|
||||
3
protobuf/login-response.proto
Normal file
3
protobuf/login-response.proto
Normal file
@@ -0,0 +1,3 @@
|
||||
message LoginResponse {
|
||||
required string session_id = 1;
|
||||
}
|
||||
3
protobuf/per-session-info.proto
Normal file
3
protobuf/per-session-info.proto
Normal file
@@ -0,0 +1,3 @@
|
||||
message PerSessionInfo {
|
||||
required string relay_url = 1;
|
||||
}
|
||||
13
protobuf/periodic-info.proto
Normal file
13
protobuf/periodic-info.proto
Normal file
@@ -0,0 +1,13 @@
|
||||
message PeriodicInfo {
|
||||
required string game_server_ip = 1;
|
||||
optional uint32 f2 = 2;
|
||||
optional uint32 f3 = 3;
|
||||
optional uint32 f4 = 4;
|
||||
optional uint32 f5 = 5;
|
||||
optional uint32 f6 = 6;
|
||||
}
|
||||
|
||||
|
||||
message PeriodicInfos {
|
||||
repeated PeriodicInfo infos = 1;
|
||||
}
|
||||
221
protobuf/profile.proto
Normal file
221
protobuf/profile.proto
Normal file
@@ -0,0 +1,221 @@
|
||||
message Profile {
|
||||
optional int64 id = 1;
|
||||
optional int32 is_connected_to_strava = 2;
|
||||
optional bytes f3 = 3;
|
||||
optional bytes f4 = 4;
|
||||
optional bytes f5 = 5;
|
||||
optional bool is_male = 6;
|
||||
optional bytes f7 = 7;
|
||||
optional uint32 weight_in_grams = 9;
|
||||
optional uint32 ftp = 10;
|
||||
optional uint32 f11 = 11;
|
||||
optional uint32 f12 = 12;
|
||||
optional uint32 f13 = 13;
|
||||
optional uint32 f14 = 14;
|
||||
optional uint32 f15 = 15;
|
||||
optional uint32 f16 = 16;
|
||||
optional uint32 f17 = 17;
|
||||
optional uint32 f18 = 18;
|
||||
optional uint32 f19 = 19;
|
||||
optional fixed32 f20 = 20;
|
||||
optional fixed32 f21 = 21;
|
||||
optional fixed32 f22 = 22;
|
||||
optional fixed32 f23 = 23;
|
||||
optional fixed32 f24 = 24;
|
||||
optional fixed32 f25 = 25;
|
||||
optional fixed32 f26 = 26;
|
||||
optional fixed64 f27 = 27;
|
||||
optional fixed64 f28 = 28;
|
||||
optional fixed64 f29 = 29;
|
||||
optional fixed64 f30 = 30;
|
||||
optional fixed64 f31 = 31;
|
||||
optional fixed64 f32 = 32;
|
||||
optional bytes f33 = 33;
|
||||
optional uint32 country_code = 34;
|
||||
optional uint32 total_distance_in_meters = 35;
|
||||
optional uint32 elevation_gain_in_meters = 36;
|
||||
optional uint32 time_ridden_in_minutes = 37;
|
||||
optional uint32 f38 = 38; /* time in jersey X */
|
||||
optional uint32 f39 = 39; /* time in jersey Y */
|
||||
optional uint32 f40 = 40; /* time in jersey Z */
|
||||
optional uint32 total_watt_hours = 41;
|
||||
optional uint32 height_in_millimeters = 42;
|
||||
optional string dob = 43;
|
||||
optional uint32 f44 = 44;
|
||||
optional bool f45 = 45;
|
||||
optional uint32 total_xp = 46;
|
||||
optional uint32 f47 = 47;
|
||||
|
||||
optional PlayerType player_type = 48;
|
||||
enum PlayerType {
|
||||
PLAYERTYPE0 = 0;
|
||||
NORMAL = 1;
|
||||
PLAYERTYPE2 = 2;
|
||||
PLAYERTYPE3 = 3;
|
||||
PLAYERTYPE4 = 4;
|
||||
}
|
||||
|
||||
optional uint32 achievement_level = 49;
|
||||
optional bool f50 = 50;
|
||||
optional bool f51 = 51;
|
||||
optional uint32 f52 = 52;
|
||||
optional uint32 f53 = 53;
|
||||
optional uint32 f54 = 54;
|
||||
optional uint32 age = 55;
|
||||
optional fixed32 f56 = 56;
|
||||
optional uint32 f57 = 57;
|
||||
optional bytes f58 = 58;
|
||||
optional fixed64 f59 = 59;
|
||||
repeated bytes f60 = 60; /* related to subscription/billing */
|
||||
|
||||
optional ProfileSocialFacts social_facts = 61;
|
||||
message ProfileSocialFacts {
|
||||
optional int64 profile_id = 1;
|
||||
optional int64 f2 = 2;
|
||||
optional int64 f3 = 3;
|
||||
optional int64 f4 = 4;
|
||||
optional ProfileFollowStatus f5 = 5;
|
||||
optional ProfileFollowStatus f6 = 6;
|
||||
optional bool f7 = 7;
|
||||
}
|
||||
|
||||
optional ProfileFollowStatus f62 = 62;
|
||||
optional bool f63 = 63;
|
||||
optional bool f64 = 64;
|
||||
|
||||
optional ProfileEnrolledProgram f65 = 65;
|
||||
enum ProfileEnrolledProgram {
|
||||
ENROLLEDPROGRAM0 = 0;
|
||||
ENROLLEDPROGRAM1 = 1;
|
||||
ENROLLEDPROGRAM2 = 2;
|
||||
ENROLLEDPROGRAM3 = 3;
|
||||
ENROLLEDPROGRAM4 = 4;
|
||||
}
|
||||
|
||||
optional bytes f66 = 66;
|
||||
optional uint32 f67 = 67;
|
||||
optional fixed32 f68 = 68;
|
||||
optional fixed32 f69 = 69;
|
||||
optional fixed32 f70 = 70;
|
||||
optional fixed32 f71 = 71;
|
||||
optional fixed32 f72 = 72;
|
||||
optional fixed32 f73 = 73;
|
||||
optional uint32 f74 = 74;
|
||||
optional uint32 f75 = 75;
|
||||
optional fixed32 f76 = 76;
|
||||
optional fixed32 f77 = 77;
|
||||
optional fixed32 f78 = 78;
|
||||
optional fixed32 f79 = 79;
|
||||
optional uint32 f80 = 80;
|
||||
optional uint32 f81 = 81;
|
||||
optional Subscription f82 = 82;
|
||||
enum Sport {
|
||||
SPORT0 = 0;
|
||||
SPORT1 = 1;
|
||||
SPORT2 = 2;
|
||||
SPORT3 = 3;
|
||||
SPORT4 = 4;
|
||||
}
|
||||
optional string mix_panel_distinct_id = 83;
|
||||
optional uint32 f84 = 84;
|
||||
optional uint32 f85 = 85;
|
||||
optional Sport sport = 86;
|
||||
optional uint32 f87 = 87;
|
||||
optional bool f88 = 88;
|
||||
optional string preferred_language = 89;
|
||||
optional uint32 f90 = 90;
|
||||
optional uint32 f91 = 91;
|
||||
optional uint32 f92 = 92;
|
||||
optional uint32 f93 = 93;
|
||||
optional uint32 f94 = 94;
|
||||
optional uint32 f95 = 95;
|
||||
optional uint32 f96 = 96;
|
||||
optional uint32 f97 = 97;
|
||||
optional uint32 f98 = 98;
|
||||
optional uint32 f99 = 99;
|
||||
optional uint32 f100 = 100;
|
||||
optional uint32 f101 = 101;
|
||||
optional uint32 f102 = 102;
|
||||
optional uint32 f103 = 103;
|
||||
optional uint32 f104 = 104;
|
||||
optional bool f105 = 105;
|
||||
optional bool f106 = 106;
|
||||
repeated bytes f107 = 107;
|
||||
optional string launched_game_client = 108;
|
||||
optional int64 f109 = 109;
|
||||
optional bool f110 = 110;
|
||||
}
|
||||
|
||||
message ProfileEntitlement {
|
||||
optional EntitlementType f1 = 1;
|
||||
enum EntitlementType {
|
||||
ENTITLEMENTTYPE0 = 0;
|
||||
ENTITLEMENTTYPE1 = 1;
|
||||
ENTITLEMENTTYPE2 = 2;
|
||||
ENTITLEMENTTYPE3 = 3;
|
||||
ENTITLEMENTTYPE4 = 4;
|
||||
}
|
||||
|
||||
optional int64 f2 = 2;
|
||||
|
||||
optional ProfileEntitlementStatus c = 3;
|
||||
enum ProfileEntitlementStatus {
|
||||
ENTITLEMENTSTATUS0 = 0;
|
||||
ENTITLEMENTSTATUS1 = 1;
|
||||
ENTITLEMENTSTATUS2 = 2;
|
||||
ENTITLEMENTSTATUS3 = 3;
|
||||
ENTITLEMENTSTATUS4 = 4;
|
||||
}
|
||||
|
||||
optional bytes f4 = 4;
|
||||
optional uint32 f5 = 5;
|
||||
optional uint32 f6 = 6;
|
||||
optional uint32 f7 = 7;
|
||||
optional uint32 f8 = 8;
|
||||
optional uint32 f9 = 9;
|
||||
optional bytes f10 = 10;
|
||||
|
||||
optional Platform f11 = 11;
|
||||
enum Platform {
|
||||
PLATFORM0 = 0;
|
||||
PLATFORM1 = 1;
|
||||
PLATFORM2 = 2;
|
||||
PLATFORM3 = 3;
|
||||
PLATFORM4 = 4;
|
||||
PLATFORM5 = 5;
|
||||
PLATFORM6 = 6;
|
||||
}
|
||||
|
||||
optional uint32 f12 = 12;
|
||||
optional bool f13 = 13;
|
||||
}
|
||||
|
||||
enum ProfileFollowStatus {
|
||||
FOLLOWSTATUS0 = 0;
|
||||
SELF = 1;
|
||||
FOLLOWSTATUS2 = 2;
|
||||
FOLLOWSTATUS3 = 3;
|
||||
FOLLOWSTATUS4 = 4;
|
||||
}
|
||||
|
||||
message Subscription {
|
||||
optional Gateway f1 = 1;
|
||||
enum Gateway {
|
||||
GATEWAY0 = 0;
|
||||
GATEWAY1 = 1;
|
||||
GATEWAY2 = 2;
|
||||
GATEWAY3 = 3;
|
||||
GATEWAY4 = 4;
|
||||
}
|
||||
|
||||
optional SubscriptionStatus f2 = 2;
|
||||
enum SubscriptionStatus {
|
||||
STATUS0 = 0;
|
||||
STATUS1 = 1;
|
||||
ACTIVE = 2;
|
||||
ACTIVE_CANCELLED = 3;
|
||||
STATUS4 = 4;
|
||||
STATUS5 = 5;
|
||||
STATUS6 = 6;
|
||||
}
|
||||
}
|
||||
29
protobuf/segment-result.proto
Normal file
29
protobuf/segment-result.proto
Normal file
@@ -0,0 +1,29 @@
|
||||
message SegmentResult {
|
||||
optional uint64 id = 1;
|
||||
required uint64 player_id = 2;
|
||||
optional uint32 f3 = 3;
|
||||
optional uint32 f4 = 4;
|
||||
optional uint64 segment_id = 5;
|
||||
optional uint64 event_subgroup_id = 6;
|
||||
required string first_name = 7;
|
||||
required string last_name = 8;
|
||||
optional uint64 world_time = 9;
|
||||
optional string finish_time_str = 10;
|
||||
required uint64 elapsed_ms = 11;
|
||||
optional bool f12 = 12;
|
||||
optional uint32 f13 = 13;
|
||||
optional uint32 f14 = 14;
|
||||
optional uint32 f15 = 15;
|
||||
optional bool f16 = 16;
|
||||
optional string f17 = 17;
|
||||
optional uint64 f18 = 18;
|
||||
optional uint32 f19 = 19;
|
||||
optional uint32 f20 = 20;
|
||||
}
|
||||
|
||||
message SegmentResults {
|
||||
required uint32 world_id = 1;
|
||||
required uint64 segment_id = 2;
|
||||
optional uint64 event_subgroup_id = 3;
|
||||
repeated SegmentResult segment_results = 4;
|
||||
}
|
||||
13
protobuf/world.proto
Normal file
13
protobuf/world.proto
Normal file
@@ -0,0 +1,13 @@
|
||||
message World {
|
||||
required uint32 id = 1;
|
||||
required string name = 2;
|
||||
required uint32 f3 = 3;
|
||||
/* missing 4 */
|
||||
required uint64 f5 = 5;
|
||||
required uint64 world_time = 6;
|
||||
required uint64 real_time = 7;
|
||||
}
|
||||
|
||||
message Worlds {
|
||||
repeated World worlds = 1;
|
||||
}
|
||||
180
scripts/get_profile.py
Executable file
180
scripts/get_profile.py
Executable file
@@ -0,0 +1,180 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
#
|
||||
# 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 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.
|
||||
|
||||
import sys, argparse, getpass
|
||||
import requests
|
||||
import json
|
||||
import os, time, stat
|
||||
from collections import namedtuple
|
||||
|
||||
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
|
||||
|
||||
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,
|
||||
verify = args.verifyCert,
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
print('Response HTTP Status Code: {status_code}'.format(
|
||||
status_code=response.status_code))
|
||||
print('Response HTTP Response Body: {content}'.format(
|
||||
content=response.content))
|
||||
|
||||
json_dict = json.loads(response.content)
|
||||
|
||||
return (json_dict["access_token"], json_dict["refresh_token"], json_dict["expires_in"])
|
||||
|
||||
except requests.exceptions.RequestException, e:
|
||||
print('HTTP Request failed: %s' % e)
|
||||
|
||||
def query_player_profile(session, access_token):
|
||||
# Query Player Profile
|
||||
# GET https://us-or-rly101.zwift.com/api/profiles/<player_id>
|
||||
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",
|
||||
},
|
||||
verify = args.verifyCert,
|
||||
)
|
||||
|
||||
if args.verbose:
|
||||
print('Response HTTP Status Code: {status_code}'.format(
|
||||
status_code=response.status_code))
|
||||
|
||||
return response.content
|
||||
|
||||
except requests.exceptions.RequestException, 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,
|
||||
},
|
||||
verify = args.verifyCert,
|
||||
)
|
||||
if args.verbose:
|
||||
print('Response HTTP Status Code: {status_code}'.format(
|
||||
status_code=response.status_code))
|
||||
print('Response HTTP Response Body: {content}'.format(
|
||||
content=response.content))
|
||||
except requests.exceptions.RequestException, 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
|
||||
|
||||
access_token = None
|
||||
cookies = None
|
||||
|
||||
parser = argparse.ArgumentParser(description = 'Zwift Profile Fetcher')
|
||||
parser.add_argument('-v', '--verbose', action='store_true',
|
||||
help='Verbose output')
|
||||
parser.add_argument('--dont-check-certificates', action='store_false',
|
||||
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']
|
||||
|
||||
session = requests.session()
|
||||
|
||||
access_token, refresh_token = login(session, args.user, password)
|
||||
profile = query_player_profile(session, access_token)
|
||||
with open('profile.bin', 'wb') as f:
|
||||
f.write(profile)
|
||||
|
||||
logout(session, refresh_token)
|
||||
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
main(sys.argv)
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
except SystemExit, se:
|
||||
print "ERROR:",se
|
||||
10
secure.zwift.com.wsgi
Normal file
10
secure.zwift.com.wsgi
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/python
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
logging.basicConfig(stream=sys.stderr)
|
||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
|
||||
from auth_server import app as application
|
||||
application.debug = True
|
||||
BIN
ssl/cert-secure-zwift.p12
Normal file
BIN
ssl/cert-secure-zwift.p12
Normal file
Binary file not shown.
32
ssl/cert-secure-zwift.pem
Normal file
32
ssl/cert-secure-zwift.pem
Normal file
@@ -0,0 +1,32 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFkzCCA3ugAwIBAgIJAI1VyxapSWRwMA0GCSqGSIb3DQEBCwUAMGAxCzAJBgNV
|
||||
BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQxGTAXBgNVBAMMEHNlY3VyZS56d2lmdC5jb20wHhcNMTcx
|
||||
MTE3MDAyNDQ5WhcNMzcxMTEyMDAyNDQ5WjBgMQswCQYDVQQGEwJBVTETMBEGA1UE
|
||||
CAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRk
|
||||
MRkwFwYDVQQDDBBzZWN1cmUuendpZnQuY29tMIICIjANBgkqhkiG9w0BAQEFAAOC
|
||||
Ag8AMIICCgKCAgEAwTWex70TcPusmhbfyfg96oL3d+O0/FRjmQBezgO9IP9xdY1f
|
||||
B9wT46jan7UDiJrpB0IhVi3O4wHpR/VvYyO9H1piRllb8YG02e5d6wXA3iSVi25f
|
||||
Up36aIggjLCKMZLjbHk7LB09/mKFc8pmjfWTcbmqAG5OZVLp81fYzKKmWQ+0gZT6
|
||||
T88RPlUsnxDeDzKsd8Nnw3oiF/cpnS7eko8ohhB6s5xyA62wCP5S8qIHZgD2e0CY
|
||||
vq/u6SXqYkBYnSrK4eTQZ92PiPQ3CriRCO87YGpuPnu18TntJ4ooEsL4iJKGSaCl
|
||||
o16++NF5tD2IzkexHwMREllUTvfHiZyBGfyXhecDKFdsMK8+4cGlxP6FDzSCoYBy
|
||||
eVd7O48m0L6AvCXXumGv/PviOViWYMKV1jg3wR8uoRc95hJiMeXayScbSxjvDTjv
|
||||
B1/FxUda0msTPUOaEXW1Zr3lhM32p9ESuNXX3DxIXOSw8yokRastw53AfdPZ01ql
|
||||
RD8PmvY4LXAVMcRdkYryEpDl1lSYXPtgScvTtc4tmGNkQvOFvvJMgoSHJ4cltPMj
|
||||
VpFNBqOxBAAaAKax1hVt4PAlVZW1ACXt5VALR4HCKVljQQAlFeWA9Fsd/uvhDpVL
|
||||
yBWS7oIeMO2dIchoal77VHE+0dIvC2p5dxxiyGT4cWLUcEwQCZrEwFnxqH8CAwEA
|
||||
AaNQME4wHQYDVR0OBBYEFAc8t0qbC65PUOqvYQjhsvGT72DTMB8GA1UdIwQYMBaA
|
||||
FAc8t0qbC65PUOqvYQjhsvGT72DTMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEL
|
||||
BQADggIBADXA5BuuEm/epOKTZ7I/VkO5lldJ/xwCtv5g29PomTTO0X3mxL3zZe4U
|
||||
94zraHPu1dpfzbdheXc65rJyvtxTLbSBlK+X58WPzdpFI983sPsOx0g9NqLZdPYO
|
||||
oIMY5sn2ASM8CLRgjkDePmo2K8cXfb4uF/PbBs6WOfaYWNZAgR6nUXgGGZ594eEa
|
||||
dKBfwNuXbUrmQ/MFWzWbGaqTq8KyMUh8bVBbpnu0/hAn4kHKxeZ2O3Zr3fPEawfh
|
||||
iMdypTbUwuGY3EFhKrhaw9lSJIznxhbW0xoI78oSmExc95YcIvfBJUKEIxRzJvP4
|
||||
AyJZbPITMvRT5yl8GFvNHFIhbavAZguISvY8jQ8Q/Ygn+EVMLi0Yo2K5j5e/RcHW
|
||||
KCfrG/iV3B1fHRLO9ruv/YU2/kperB3uOxXm401JDqwJM1xl7Cy6gpiAZCbXefdz
|
||||
gkwvLtNTT/8Dw7XJja73TK3BjF/HbhgpI/Vf4NQE+86nY1mOr8FmShy9YpI+2wy+
|
||||
5lH0De2QWjtA8tjd+Wm2e7mD1VW1TpOuVKsmXeyODOqCOAzaxTnJuvX9fWzqldih
|
||||
4J5vTsSvMI/dcMRCxD+CIpS1rpymnKj2okVR2sMdWhEXW1pjDKOo5cG9p/u0R0n/
|
||||
5bKV3CniC10bfOXASWkyIFtdDrsfIhooSAxjW5fSgZYNTq/8AYYw
|
||||
-----END CERTIFICATE-----
|
||||
BIN
ssl/cert-us-or.p12
Normal file
BIN
ssl/cert-us-or.p12
Normal file
Binary file not shown.
33
ssl/cert-us-or.pem
Normal file
33
ssl/cert-us-or.pem
Normal file
@@ -0,0 +1,33 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFnzCCA4egAwIBAgIJANIl+1SWZch0MA0GCSqGSIb3DQEBCwUAMGYxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQxHzAdBgNVBAMMFnVzLW9yLXJseTEwMS56d2lmdC5jb20w
|
||||
HhcNMTcxMTE3MDAyMzE0WhcNMzcxMTEyMDAyMzE0WjBmMQswCQYDVQQGEwJVUzET
|
||||
MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
|
||||
dHkgTHRkMR8wHQYDVQQDDBZ1cy1vci1ybHkxMDEuendpZnQuY29tMIICIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA/E2X0WFE28UUUnBaflRalw/BlLv6UA1s
|
||||
A4XFxoLZ8fKm4roGL/rl6bg+1f9iViEHsy0DdWnPpo+XuyReVU7qjFb+wssEBnj+
|
||||
Y0c2HdW3J7FJk/x0w3r0DyuiefFXafCvwzGHuZlsmD6/WbIhzM40kYAl1RCAZrN9
|
||||
3odM47GdBd0lbhPiLmhivewQzwSEGjbWJiMlKQxJ2JKX4vg/UipvVvecNbyfP9mA
|
||||
O37pEUzt00kTEFYW88uAj2Vc8PVCOl2YUdTG/b9ATVBVedi4afvFgIMRr/Mgg7mg
|
||||
2xKJt1teH+3os5uK1GrhT2bv6bVNyjYwmF/hJY1UgPo4d3ETAwC+7J4nWlRibsOZ
|
||||
kicEH7aJ/TtcjI0+KX84GqPN50UWYcOIepKYEx/R1dViei+Z6YtdVkNqZKsTZ44d
|
||||
M28k2+w2FsjGb21XXxSwWCMBo31AwUzvUhXNMLnO+LTijcyj6jKfF8KYuvhKKJwR
|
||||
IZwjf6UdK4JKhHp5PqQUVEIakC4lxeRGbcGxGFKYgr6iRYpQlzXgCVwIvUlnLlpg
|
||||
FrsX8pA6W3e6WUWw9Cjj/F2aqeHl2v7l3n4/MlJww9/UJElJPam43B7NsUNpGZPw
|
||||
oHfV1v7JAwg4eBOGnHD4zNVpLF3eljJQ2v/7nd5JcS69E2rb/4r528aXAaY4s3rf
|
||||
p5/j/iwxR2ECAwEAAaNQME4wHQYDVR0OBBYEFF7iQt/UFc/ULcRhf76pfSm/8J98
|
||||
MB8GA1UdIwQYMBaAFF7iQt/UFc/ULcRhf76pfSm/8J98MAwGA1UdEwQFMAMBAf8w
|
||||
DQYJKoZIhvcNAQELBQADggIBAKaG7XXAUyoAm2WwVTY/C2G/NhHNYcUGqaL3gTeP
|
||||
DO7hvxef1QaWl/u4GP/PvPhdQMFuc/XNwzf2zOJkf/bUo0bbgmVmaoCgWrtciwSa
|
||||
HE3FKaGbyXcv9XPuX1reFlHTSA/J7Jfi8+nDjk/LlLe7RGk7+a6SkJHQo6c5VR+h
|
||||
72xh7Y+s8hsyg3pp7Qay2FWEkl9EXyzgWIpLkZltPEzvcine8cyeCRPgue7PAnKG
|
||||
RyqHkTIn2hnjFDY8p1yNrY047bY+HFukOCPrucVfQ8Datruqpw9MLeS50qO/TDLy
|
||||
+Jyl0C6S6LqA/N/OuwmkP5ggZoVz9lf+LvtbK8Gh1ewp3SBKAC+wWkQj2HrHQ/cb
|
||||
fZEafNKSIn0ll07yNfxhZrTtDLc4ZzgyvxV4XKy6xsDYGgqP6PpFgGIlpJGQo3ok
|
||||
is+qJESrzxN8OlwQfqrMEl7hs8TM5TtcdiAwOYb7ddbH96jZgoNM1f1CnjOhmT/s
|
||||
ayjVvDuk/QxkLzmhI5XnYiR95fH34hVq5RZpbT73g63MHXSh/TKgu6a/esnkN/2x
|
||||
Qho5aiNZfSzq53pOhTmJ9UZOdfGa65PgJpcFydbKhNHgeL3kwc1e0I7uIBLlm1OK
|
||||
rBf+9X3e/15HvFFVAABVvXGafumRheiGR081MVNfQywbswjQIZ5CkkNla5ucocVv
|
||||
td58
|
||||
-----END CERTIFICATE-----
|
||||
52
ssl/key-secure-zwift.pem
Normal file
52
ssl/key-secure-zwift.pem
Normal file
@@ -0,0 +1,52 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
6b51tGnoGW5fHK2d3wqsBnWwS/d8wLM=
|
||||
-----END PRIVATE KEY-----
|
||||
85
ssl/key-us-or.pem
Normal file
85
ssl/key-us-or.pem
Normal file
@@ -0,0 +1,85 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
ddveSTm8c0DJ5YAlcFgpxWtU0h3FjaAv
|
||||
-----END PRIVATE KEY-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFnzCCA4egAwIBAgIJANIl+1SWZch0MA0GCSqGSIb3DQEBCwUAMGYxCzAJBgNV
|
||||
BAYTAlVTMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX
|
||||
aWRnaXRzIFB0eSBMdGQxHzAdBgNVBAMMFnVzLW9yLXJseTEwMS56d2lmdC5jb20w
|
||||
HhcNMTcxMTE3MDAyMzE0WhcNMzcxMTEyMDAyMzE0WjBmMQswCQYDVQQGEwJVUzET
|
||||
MBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQ
|
||||
dHkgTHRkMR8wHQYDVQQDDBZ1cy1vci1ybHkxMDEuendpZnQuY29tMIICIjANBgkq
|
||||
hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA/E2X0WFE28UUUnBaflRalw/BlLv6UA1s
|
||||
A4XFxoLZ8fKm4roGL/rl6bg+1f9iViEHsy0DdWnPpo+XuyReVU7qjFb+wssEBnj+
|
||||
Y0c2HdW3J7FJk/x0w3r0DyuiefFXafCvwzGHuZlsmD6/WbIhzM40kYAl1RCAZrN9
|
||||
3odM47GdBd0lbhPiLmhivewQzwSEGjbWJiMlKQxJ2JKX4vg/UipvVvecNbyfP9mA
|
||||
O37pEUzt00kTEFYW88uAj2Vc8PVCOl2YUdTG/b9ATVBVedi4afvFgIMRr/Mgg7mg
|
||||
2xKJt1teH+3os5uK1GrhT2bv6bVNyjYwmF/hJY1UgPo4d3ETAwC+7J4nWlRibsOZ
|
||||
kicEH7aJ/TtcjI0+KX84GqPN50UWYcOIepKYEx/R1dViei+Z6YtdVkNqZKsTZ44d
|
||||
M28k2+w2FsjGb21XXxSwWCMBo31AwUzvUhXNMLnO+LTijcyj6jKfF8KYuvhKKJwR
|
||||
IZwjf6UdK4JKhHp5PqQUVEIakC4lxeRGbcGxGFKYgr6iRYpQlzXgCVwIvUlnLlpg
|
||||
FrsX8pA6W3e6WUWw9Cjj/F2aqeHl2v7l3n4/MlJww9/UJElJPam43B7NsUNpGZPw
|
||||
oHfV1v7JAwg4eBOGnHD4zNVpLF3eljJQ2v/7nd5JcS69E2rb/4r528aXAaY4s3rf
|
||||
p5/j/iwxR2ECAwEAAaNQME4wHQYDVR0OBBYEFF7iQt/UFc/ULcRhf76pfSm/8J98
|
||||
MB8GA1UdIwQYMBaAFF7iQt/UFc/ULcRhf76pfSm/8J98MAwGA1UdEwQFMAMBAf8w
|
||||
DQYJKoZIhvcNAQELBQADggIBAKaG7XXAUyoAm2WwVTY/C2G/NhHNYcUGqaL3gTeP
|
||||
DO7hvxef1QaWl/u4GP/PvPhdQMFuc/XNwzf2zOJkf/bUo0bbgmVmaoCgWrtciwSa
|
||||
HE3FKaGbyXcv9XPuX1reFlHTSA/J7Jfi8+nDjk/LlLe7RGk7+a6SkJHQo6c5VR+h
|
||||
72xh7Y+s8hsyg3pp7Qay2FWEkl9EXyzgWIpLkZltPEzvcine8cyeCRPgue7PAnKG
|
||||
RyqHkTIn2hnjFDY8p1yNrY047bY+HFukOCPrucVfQ8Datruqpw9MLeS50qO/TDLy
|
||||
+Jyl0C6S6LqA/N/OuwmkP5ggZoVz9lf+LvtbK8Gh1ewp3SBKAC+wWkQj2HrHQ/cb
|
||||
fZEafNKSIn0ll07yNfxhZrTtDLc4ZzgyvxV4XKy6xsDYGgqP6PpFgGIlpJGQo3ok
|
||||
is+qJESrzxN8OlwQfqrMEl7hs8TM5TtcdiAwOYb7ddbH96jZgoNM1f1CnjOhmT/s
|
||||
ayjVvDuk/QxkLzmhI5XnYiR95fH34hVq5RZpbT73g63MHXSh/TKgu6a/esnkN/2x
|
||||
Qho5aiNZfSzq53pOhTmJ9UZOdfGa65PgJpcFydbKhNHgeL3kwc1e0I7uIBLlm1OK
|
||||
rBf+9X3e/15HvFFVAABVvXGafumRheiGR081MVNfQywbswjQIZ5CkkNla5ucocVv
|
||||
td58
|
||||
-----END CERTIFICATE-----
|
||||
0
storage/force_dir_in_git.txt
Normal file
0
storage/force_dir_in_git.txt
Normal file
10
us-or-rly101.zwift.com.wsgi
Normal file
10
us-or-rly101.zwift.com.wsgi
Normal file
@@ -0,0 +1,10 @@
|
||||
#!/usr/bin/python
|
||||
import os
|
||||
import sys
|
||||
import logging
|
||||
logging.basicConfig(stream=sys.stderr)
|
||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
sys.path.insert(0, SCRIPT_DIR)
|
||||
|
||||
from zwift_offline import app as application
|
||||
application.debug = True
|
||||
485
zwift_offline.py
Executable file
485
zwift_offline.py
Executable file
@@ -0,0 +1,485 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
import calendar
|
||||
import datetime
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import sqlite3
|
||||
import time
|
||||
from copy import copy
|
||||
from datetime import timedelta
|
||||
from io import BytesIO
|
||||
|
||||
from flask import Flask, request, jsonify, g
|
||||
from google.protobuf.descriptor import FieldDescriptor
|
||||
from protobuf_to_dict import protobuf_to_dict, TYPE_CALLABLE_MAP
|
||||
|
||||
import protobuf.activity_pb2 as activity_pb2
|
||||
import protobuf.goal_pb2 as goal_pb2
|
||||
import protobuf.login_response_pb2 as login_response_pb2
|
||||
import protobuf.per_session_info_pb2 as per_session_info_pb2
|
||||
import protobuf.periodic_info_pb2 as periodic_info_pb2
|
||||
import protobuf.profile_pb2 as profile_pb2
|
||||
import protobuf.segment_result_pb2 as segment_result_pb2
|
||||
import protobuf.world_pb2 as world_pb2
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__))
|
||||
STORAGE_DIR = "%s/storage" % SCRIPT_DIR
|
||||
DATABASE_PATH = "%s/zwift-offline.db" % STORAGE_DIR
|
||||
DATABASE_INIT_SQL = "%s/initialize_db.sql" % SCRIPT_DIR
|
||||
DATABASE_CUR_VER = 0
|
||||
|
||||
|
||||
####
|
||||
# Set up protobuf_to_dict call map
|
||||
type_callable_map = copy(TYPE_CALLABLE_MAP)
|
||||
# Override base64 encoding of byte fields
|
||||
type_callable_map[FieldDescriptor.TYPE_BYTES] = str
|
||||
# sqlite doesn't support uint64 so make them strings
|
||||
type_callable_map[FieldDescriptor.TYPE_UINT64] = str
|
||||
|
||||
|
||||
logger = logging.getLogger('zoffline')
|
||||
logger.setLevel(logging.WARN)
|
||||
|
||||
|
||||
def insert_protobuf_into_db(table_name, msg):
|
||||
cur = g.db.cursor()
|
||||
msg_dict = protobuf_to_dict(msg, type_callable_map=type_callable_map)
|
||||
columns = ', '.join(msg_dict.keys())
|
||||
placeholders = ':'+', :'.join(msg_dict.keys())
|
||||
query = 'INSERT INTO %s (%s) VALUES (%s)' % (table_name, columns, placeholders)
|
||||
cur.execute(query, msg_dict)
|
||||
g.db.commit()
|
||||
|
||||
|
||||
# XXX: can't be used to 'nullify' a column value
|
||||
def update_protobuf_in_db(table_name, msg, id):
|
||||
try:
|
||||
# If protobuf has an id field and it's uint64, make it a string
|
||||
id_field = msg.DESCRIPTOR.fields_by_name['id']
|
||||
if id_field.type == id_field.TYPE_UINT64:
|
||||
id = str(id)
|
||||
except AttributeError:
|
||||
pass
|
||||
cur = g.db.cursor()
|
||||
msg_dict = protobuf_to_dict(msg, type_callable_map=type_callable_map)
|
||||
columns = ', '.join(msg_dict.keys())
|
||||
placeholders = ':'+', :'.join(msg_dict.keys())
|
||||
setters = ', '.join('{}=:{}'.format(key, key) for key in msg_dict)
|
||||
query = 'UPDATE %s SET %s WHERE id=%s' % (table_name, setters, id)
|
||||
cur.execute(query, msg_dict)
|
||||
g.db.commit()
|
||||
|
||||
|
||||
def row_to_protobuf(row, msg, exclude_fields=[]):
|
||||
for key in msg.DESCRIPTOR.fields_by_name.keys():
|
||||
if key in exclude_fields:
|
||||
continue
|
||||
if row[key] is None:
|
||||
continue
|
||||
field = msg.DESCRIPTOR.fields_by_name[key]
|
||||
if field.type == field.TYPE_UINT64:
|
||||
setattr(msg, key, int(row[key]))
|
||||
else:
|
||||
setattr(msg, key, row[key])
|
||||
return msg
|
||||
|
||||
|
||||
# FIXME: I should really do this properly...
|
||||
def get_id(table_name):
|
||||
cur = g.db.cursor()
|
||||
while True:
|
||||
# I think activity id is actually only uint32. On the off chance it's
|
||||
# int32, stick with 31 bits.
|
||||
ident = int(random.getrandbits(31))
|
||||
cur.execute("SELECT id FROM %s WHERE id = ?" % table_name, (str(ident),))
|
||||
if not cur.fetchall():
|
||||
break
|
||||
return ident
|
||||
|
||||
|
||||
@app.route('/api/auth', methods=['GET'])
|
||||
def api_auth():
|
||||
return '{"realm":"zwift","url":"https://secure.zwift.com/auth/"}'
|
||||
|
||||
|
||||
@app.route('/api/users/login', methods=['POST'])
|
||||
def api_users_login():
|
||||
response = login_response_pb2.LoginResponse()
|
||||
response.session_id = 'abc'
|
||||
return response.SerializeToString(), 200
|
||||
|
||||
|
||||
@app.route('/api/users/logout', methods=['POST'])
|
||||
def api_users_logout():
|
||||
return '', 204
|
||||
|
||||
|
||||
@app.route('/api/analytics/event', methods=['POST'])
|
||||
def api_analytics_event():
|
||||
return '', 200
|
||||
|
||||
|
||||
@app.route('/api/per-session-info', methods=['GET'])
|
||||
def api_per_session_info():
|
||||
info = per_session_info_pb2.PerSessionInfo()
|
||||
info.relay_url = "https://us-or-rly101.zwift.com/relay"
|
||||
return info.SerializeToString(), 200
|
||||
|
||||
|
||||
@app.route('/api/events/search', methods=['POST'])
|
||||
def api_events_search():
|
||||
return '', 200
|
||||
|
||||
|
||||
@app.route('/api/profiles/me', methods=['GET'])
|
||||
def api_profiles_me():
|
||||
profile_file = '%s/profile.bin' % STORAGE_DIR
|
||||
if not os.path.isfile(profile_file):
|
||||
profile = profile_pb2.Profile()
|
||||
profile.id = 1000
|
||||
profile.is_connected_to_strava = True
|
||||
return profile.SerializeToString(), 200
|
||||
with open(profile_file, 'rb') as fd:
|
||||
return fd.read()
|
||||
|
||||
|
||||
# FIXME (not going to fix unless really bored): only supports 1 profile
|
||||
@app.route('/api/profiles/<int:player_id>', methods=['PUT'])
|
||||
def api_profiles_id(player_id):
|
||||
if not request.stream:
|
||||
return '', 400
|
||||
if not os.path.exists(STORAGE_DIR):
|
||||
os.makedirs(STORAGE_DIR)
|
||||
with open('%s/profile.bin' % STORAGE_DIR, 'wb') as f:
|
||||
f.write(request.stream.read())
|
||||
return '', 204
|
||||
|
||||
|
||||
@app.route('/api/profiles/<int:player_id>/activities/', methods=['GET', 'POST'], strict_slashes=False)
|
||||
def api_profiles_activities(player_id):
|
||||
if request.method == 'POST':
|
||||
if not request.stream:
|
||||
return '', 400
|
||||
activity = activity_pb2.Activity()
|
||||
activity.ParseFromString(request.stream.read())
|
||||
activity.id = get_id('activity')
|
||||
insert_protobuf_into_db('activity', activity)
|
||||
return '{"id": %ld}' % activity.id, 200
|
||||
|
||||
# request.method == 'GET'
|
||||
activities = activity_pb2.Activities()
|
||||
cur = g.db.cursor()
|
||||
cur.execute("SELECT * FROM activity WHERE player_id = ?", (str(player_id),))
|
||||
for row in cur.fetchall():
|
||||
activity = activities.activities.add()
|
||||
row_to_protobuf(row, activity, exclude_fields=['fit'])
|
||||
|
||||
return activities.SerializeToString(), 200
|
||||
|
||||
|
||||
# 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/<int:player_id>/activities/<string:activity_id>', methods=['PUT'])
|
||||
def api_profiles_activities_id(player_id, activity_id):
|
||||
if not request.stream:
|
||||
return '', 400
|
||||
activity_id = int(activity_id) & 0xffffffffffffffff
|
||||
activity = activity_pb2.Activity()
|
||||
activity.ParseFromString(request.stream.read())
|
||||
update_protobuf_in_db('activity', activity, activity_id)
|
||||
|
||||
response = '{"id":%s}' % activity_id
|
||||
if request.args.get('upload-to-strava') != 'true':
|
||||
return response, 200
|
||||
try:
|
||||
from stravalib.client import Client
|
||||
except ImportError:
|
||||
logger.warn("stravalib is not installed. Skipping Strava upload attempt.")
|
||||
return response, 200
|
||||
strava = Client()
|
||||
try:
|
||||
with open('%s/strava_token.txt' % STORAGE_DIR, 'r') as f:
|
||||
strava.access_token = f.read().rstrip('\r\n')
|
||||
except:
|
||||
logger.warn("Failed to read %s/strava_token.txt. Skipping Strava upload attempt.")
|
||||
return response, 200
|
||||
try:
|
||||
# See if there's internet to upload to Strava
|
||||
strava.upload_activity(BytesIO(activity.fit), data_type='fit', name=activity.name)
|
||||
# XXX: assume the upload succeeds on strava's end. not checking on it.
|
||||
except:
|
||||
logger.warn("Strava upload failed. No internet?")
|
||||
return response, 200
|
||||
|
||||
|
||||
@app.route('/api/profiles/<int:player_id>/followees', methods=['GET'])
|
||||
def api_profiles_followees(player_id):
|
||||
return '', 200
|
||||
|
||||
|
||||
def get_week_range(dt):
|
||||
d = datetime.datetime(dt.year,1,1)
|
||||
if (d.weekday()<= 3):
|
||||
d = d - timedelta(d.weekday())
|
||||
else:
|
||||
d = d + timedelta(7-d.weekday())
|
||||
dlt = timedelta(days = (int(dt.strftime('%W'))-1)*7)
|
||||
first = d + dlt
|
||||
last = d + dlt + timedelta(days=6, hours=23, minutes=59, seconds=59)
|
||||
return first, last
|
||||
|
||||
def get_month_range(dt):
|
||||
num_days = calendar.monthrange(dt.year, dt.month)[1]
|
||||
first = datetime.datetime(dt.year, dt.month, 1)
|
||||
last = datetime.datetime(dt.year, dt.month, num_days, 23, 59, 59)
|
||||
return first, last
|
||||
|
||||
|
||||
def unix_time_millis(dt):
|
||||
return int(dt.strftime('%s')) * 1000
|
||||
|
||||
|
||||
def fill_in_goal_progress(goal, player_id):
|
||||
cur = g.db.cursor()
|
||||
now = datetime.datetime.now()
|
||||
if goal.periodicity == 0: # weekly
|
||||
first_dt, last_dt = get_week_range(now)
|
||||
else: # monthly
|
||||
first_dt, last_dt = get_month_range(now)
|
||||
if goal.type == 0: # distance
|
||||
cur.execute("""SELECT SUM(distance) FROM activity
|
||||
WHERE player_id = ?
|
||||
AND strftime('%s', start_date) >= strftime('%s', ?)
|
||||
AND strftime('%s', start_date) <= strftime('%s', ?)
|
||||
AND end_date IS NOT NULL""",
|
||||
(str(player_id), first_dt, last_dt))
|
||||
distance = cur.fetchall()[0][0]
|
||||
if distance:
|
||||
goal.actual_distance = distance
|
||||
goal.actual_duration = distance
|
||||
else:
|
||||
goal.actual_distance = 0.0
|
||||
goal.actual_duration = 0.0
|
||||
|
||||
else: # duration
|
||||
cur.execute("""SELECT SUM(julianday(end_date) - julianday(start_date))
|
||||
FROM activity
|
||||
WHERE player_id = ?
|
||||
AND strftime('%s', start_date) >= strftime('%s', ?)
|
||||
AND strftime('%s', start_date) <= strftime('%s', ?)
|
||||
AND end_date IS NOT NULL""",
|
||||
(str(player_id), first_dt, last_dt))
|
||||
duration = cur.fetchall()[0][0]
|
||||
if duration:
|
||||
goal.actual_duration = duration*1440 # convert from days to minutes
|
||||
goal.actual_distance = duration*1440
|
||||
else:
|
||||
goal.actual_duration = 0.0
|
||||
goal.actual_distance = 0.0
|
||||
|
||||
|
||||
def set_goal_end_date(goal, now):
|
||||
if goal.periodicity == 0: # weekly
|
||||
goal.period_end_date = unix_time_millis(get_week_range(now)[1])
|
||||
else: # monthly
|
||||
goal.period_end_date = unix_time_millis(get_month_range(now)[1])
|
||||
|
||||
|
||||
@app.route('/api/profiles/<int:player_id>/goals', methods=['GET', 'POST'])
|
||||
def api_profiles_goals(player_id):
|
||||
if request.method == 'POST':
|
||||
if not request.stream:
|
||||
return '', 400
|
||||
goal = goal_pb2.Goal()
|
||||
goal.ParseFromString(request.stream.read())
|
||||
goal.id = get_id('goal')
|
||||
now = datetime.datetime.now()
|
||||
goal.created_on = unix_time_millis(now)
|
||||
set_goal_end_date(goal, now)
|
||||
fill_in_goal_progress(goal, player_id)
|
||||
insert_protobuf_into_db('goal', goal)
|
||||
|
||||
return goal.SerializeToString(), 200
|
||||
|
||||
# request.method == 'GET'
|
||||
goals = goal_pb2.Goals()
|
||||
cur = g.db.cursor()
|
||||
cur.execute("SELECT * FROM goal WHERE player_id = ?", (str(player_id),))
|
||||
rows = cur.fetchall()
|
||||
for row in rows:
|
||||
goal = goals.goals.add()
|
||||
row_to_protobuf(row, goal)
|
||||
end_dt = datetime.datetime.fromtimestamp(goal.period_end_date / 1000)
|
||||
now = datetime.datetime.now()
|
||||
if end_dt < now:
|
||||
set_goal_end_date(goal, now)
|
||||
update_protobuf_in_db('goal', goal, goal.id)
|
||||
fill_in_goal_progress(goal, player_id)
|
||||
|
||||
return goals.SerializeToString(), 200
|
||||
|
||||
|
||||
@app.route('/api/profiles/<int:player_id>/goals/<string:goal_id>', methods=['DELETE'])
|
||||
def api_profiles_goals_id(player_id, goal_id):
|
||||
goal_id = int(goal_id) & 0xffffffffffffffff
|
||||
cur = g.db.cursor()
|
||||
cur.execute("DELETE FROM goal WHERE id = ?", (str(goal_id),))
|
||||
g.db.commit()
|
||||
return '', 200
|
||||
|
||||
|
||||
@app.route('/relay/worlds', methods=['GET'])
|
||||
def relay_worlds():
|
||||
worlds = world_pb2.Worlds()
|
||||
world = worlds.worlds.add()
|
||||
world.id = 1
|
||||
world.name = 'Public Watopia'
|
||||
world.f3 = 1
|
||||
world.f5 = 1
|
||||
world.world_time = int(time.time())*1000
|
||||
world.real_time = int(time.time())
|
||||
return worlds.SerializeToString(), 200
|
||||
|
||||
|
||||
@app.route('/relay/worlds/<int:world_id>', methods=['GET'])
|
||||
def relay_worlds_id(world_id):
|
||||
# XXX: Will need to keep MapSchedule.xml up to date
|
||||
return jsonify({ 'currentDateTime': int(time.time()),
|
||||
'currentWorldTime': int(time.time())*1000,
|
||||
'friendsInWorld': [ ],
|
||||
'mapId': world_id,
|
||||
'name': 'Public Watopia',
|
||||
'playerCount': 0,
|
||||
'worldId': 1 })
|
||||
|
||||
|
||||
@app.route('/relay/worlds/<int:world_id>/join', methods=['POST'])
|
||||
def relay_worlds_id_join(world_id):
|
||||
return '{"worldTime":%ld}' % (time.time()*1000)
|
||||
|
||||
|
||||
@app.route('/relay/worlds/<int:world_id>/my-hash-seeds', methods=['GET'])
|
||||
def relay_worlds_my_hash_seeds(world_id):
|
||||
return '[{"expiryDate":196859639979,"seed1":-733221030,"seed2":-2142448243},{"expiryDate":196860425476,"seed1":1528095532,"seed2":-2078218472},{"expiryDate":196862212008,"seed1":1794747796,"seed2":-1901929955},{"expiryDate":196862637148,"seed1":-1411883466,"seed2":1171710140},{"expiryDate":196863874267,"seed1":670195825,"seed2":-317830991}]'
|
||||
|
||||
|
||||
# XXX: relay/worlds/<id>/attributes not implemented. seems okay with a 404
|
||||
|
||||
|
||||
@app.route('/relay/periodic-info', methods=['GET'])
|
||||
def relay_periodic_info():
|
||||
# Use 127.0.0.1 as the game server and ignore log errors
|
||||
infos = periodic_info_pb2.PeriodicInfos()
|
||||
info = infos.infos.add()
|
||||
info.game_server_ip = '127.0.0.1'
|
||||
info.f2 = 3022
|
||||
info.f3 = 10
|
||||
info.f4 = 60
|
||||
info.f5 = 30
|
||||
info.f6 = 3
|
||||
return infos.SerializeToString(), 200
|
||||
|
||||
|
||||
def handle_segment_results(request):
|
||||
if request.method == 'POST':
|
||||
if not request.stream:
|
||||
return '', 400
|
||||
result = segment_result_pb2.SegmentResult()
|
||||
result.ParseFromString(request.stream.read())
|
||||
result.id = get_id('segment_result')
|
||||
result.world_time = int(time.time())*1000
|
||||
result.finish_time_str = datetime.datetime.now().strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
result.f20 = 0
|
||||
insert_protobuf_into_db('segment_result', result)
|
||||
return '{"id": %ld}' % result.id, 200
|
||||
|
||||
# request.method == GET
|
||||
# world_id = int(request.args.get('world_id'))
|
||||
player_id = request.args.get('player_id')
|
||||
# full = request.args.get('full') == 'true'
|
||||
segment_id = int(request.args.get('segment_id')) & 0xffffffffffffffff
|
||||
# only_best = request.args.get('only-best') == 'true'
|
||||
from_date = request.args.get('from')
|
||||
to_date = request.args.get('to')
|
||||
|
||||
results = segment_result_pb2.SegmentResults()
|
||||
results.world_id = 1
|
||||
results.segment_id = segment_id
|
||||
|
||||
cur = g.db.cursor()
|
||||
where_stmt = "WHERE segment_id = ?"
|
||||
where_args = [str(segment_id)]
|
||||
if player_id:
|
||||
where_stmt += " AND player_id = ?"
|
||||
where_args.append(player_id)
|
||||
if from_date:
|
||||
where_stmt += " AND strftime('%s', finish_time_str) > strftime('%s', ?)"
|
||||
where_args.append(from_date)
|
||||
if to_date:
|
||||
where_stmt += " AND strftime('%s', finish_time_str) < strftime('%s', ?)"
|
||||
where_args.append(to_date)
|
||||
cur.execute("SELECT * FROM segment_result %s" % where_stmt, where_args)
|
||||
for row in cur.fetchall():
|
||||
result = results.segment_results.add()
|
||||
row_to_protobuf(row, result)
|
||||
|
||||
return results.SerializeToString(), 200
|
||||
|
||||
|
||||
@app.route('/relay/segment-results', methods=['GET'])
|
||||
def relay_segment_results():
|
||||
return handle_segment_results(request)
|
||||
|
||||
|
||||
@app.route('/api/segment-results', methods=['GET', 'POST'])
|
||||
def api_segment_results():
|
||||
return handle_segment_results(request)
|
||||
|
||||
|
||||
@app.route('/relay/worlds/<int:world_id>/leave', methods=['POST'])
|
||||
def relay_worlds_leave(world_id):
|
||||
return '{"worldtime":%ld}' % (time.time()*1000)
|
||||
|
||||
|
||||
def connect_db():
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
conn.text_factory = str
|
||||
conn.row_factory = sqlite3.Row
|
||||
return conn
|
||||
|
||||
|
||||
@app.before_request
|
||||
def before_request():
|
||||
g.db = connect_db()
|
||||
|
||||
|
||||
@app.teardown_request
|
||||
def teardown_request(exception):
|
||||
if hasattr(g, 'db'):
|
||||
g.db.close()
|
||||
|
||||
|
||||
@app.before_first_request
|
||||
def init_database():
|
||||
# Nothing to do for now
|
||||
if not os.path.exists(DATABASE_PATH):
|
||||
conn = connect_db()
|
||||
cur = conn.cursor()
|
||||
# Create a new database
|
||||
with open(DATABASE_INIT_SQL, 'r') as f:
|
||||
cur.executescript(f.read())
|
||||
cur.execute('INSERT INTO version VALUES (?)', (DATABASE_CUR_VER,))
|
||||
conn.close()
|
||||
# Migrate database if necessary
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(#ssl_context=('ssl/cert-us-or.pem', 'ssl/key-us-or.pem'),
|
||||
port=8000,
|
||||
host='0.0.0.0',
|
||||
debug=True)
|
||||
Reference in New Issue
Block a user