diff --git a/.gitignore b/.gitignore index f0cbb51..0bf6e0b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ config.py public/assets/song_skins .venv public/src/js/plugin +.hidden diff --git a/app.py b/app.py index d547ba5..84564b9 100644 --- a/app.py +++ b/app.py @@ -15,7 +15,7 @@ import os import time from functools import wraps -from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash, make_response +from flask import Flask, g, jsonify, render_template, request, abort, redirect, session, flash, make_response, send_from_directory from flask_caching import Cache from flask_session import Session from flask_wtf.csrf import CSRFProtect, generate_csrf, CSRFError @@ -33,6 +33,7 @@ def take_config(name, required=False): app = Flask(__name__) client = MongoClient(host=take_config('MONGO', required=True)['host']) +basedir = take_config('BASEDIR') or '/' app.secret_key = take_config('SECRET_KEY') or 'change-me' app.config['SESSION_TYPE'] = 'redis' @@ -79,8 +80,8 @@ def generate_hash(id, form): raise HashException('Invalid response from %s (status code %s)' % (resp.url, resp.status_code)) md5.update(resp.content) else: - if url.startswith("/"): - url = url[1:] + if url.startswith(basedir): + url = url[len(basedir):] path = os.path.normpath(os.path.join("public", url)) if not os.path.isfile(path): raise HashException("File not found: %s" % (os.path.abspath(path))) @@ -129,6 +130,7 @@ def before_request_func(): def get_config(credentials=False): config_out = { + 'basedir': basedir, 'songs_baseurl': take_config('SONGS_BASEURL', required=True), 'assets_baseurl': take_config('ASSETS_BASEURL', required=True), 'email': take_config('EMAIL'), @@ -138,6 +140,10 @@ def get_config(credentials=False): 'preview_type': take_config('PREVIEW_TYPE') or 'mp3', 'multiplayer_url': take_config('MULTIPLAYER_URL') } + relative_urls = ['songs_baseurl', 'assets_baseurl'] + for name in relative_urls: + if not config_out[name].startswith("/") and not config_out[name].startswith("http://") and not config_out[name].startswith("https://"): + config_out[name] = basedir + config_out[name] if credentials: google_credentials = take_config('GOOGLE_CREDENTIALS') min_level = google_credentials['min_level'] or 0 @@ -200,24 +206,24 @@ def is_hex(input): return False -@app.route('/') +@app.route(basedir) def route_index(): version = get_version() return render_template('index.html', version=version, config=get_config()) -@app.route('/api/csrftoken') +@app.route(basedir + 'api/csrftoken') def route_csrftoken(): return jsonify({'status': 'ok', 'token': generate_csrf()}) -@app.route('/admin') +@app.route(basedir + 'admin') @admin_required(level=50) def route_admin(): - return redirect('/admin/songs') + return redirect(basedir + 'admin/songs') -@app.route('/admin/songs') +@app.route(basedir + 'admin/songs') @admin_required(level=50) def route_admin_songs(): songs = sorted(list(db.songs.find({})), key=lambda x: x['id']) @@ -226,7 +232,7 @@ def route_admin_songs(): return render_template('admin_songs.html', songs=songs, admin=user, categories=list(categories), config=get_config()) -@app.route('/admin/songs/') +@app.route(basedir + 'admin/songs/') @admin_required(level=50) def route_admin_songs_id(id): song = db.songs.find_one({'id': id}) @@ -242,7 +248,7 @@ def route_admin_songs_id(id): song=song, categories=categories, song_skins=song_skins, makers=makers, admin=user, config=get_config()) -@app.route('/admin/songs/new') +@app.route(basedir + 'admin/songs/new') @admin_required(level=100) def route_admin_songs_new(): categories = list(db.categories.find({})) @@ -254,7 +260,7 @@ def route_admin_songs_new(): return render_template('admin_song_new.html', categories=categories, song_skins=song_skins, makers=makers, config=get_config(), id=seq_new) -@app.route('/admin/songs/new', methods=['POST']) +@app.route(basedir + 'admin/songs/new', methods=['POST']) @admin_required(level=100) def route_admin_songs_new_post(): output = {'title_lang': {}, 'subtitle_lang': {}, 'courses': {}} @@ -303,10 +309,10 @@ def route_admin_songs_new_post(): db.seq.update_one({'name': 'songs'}, {'$set': {'value': seq_new}}, upsert=True) - return redirect('/admin/songs/%s' % str(seq_new)) + return redirect(basedir + 'admin/songs/%s' % str(seq_new)) -@app.route('/admin/songs/', methods=['POST']) +@app.route(basedir + 'admin/songs/', methods=['POST']) @admin_required(level=50) def route_admin_songs_id_post(id): song = db.songs.find_one({'id': id}) @@ -356,10 +362,10 @@ def route_admin_songs_id_post(id): if not hash_error: flash('Changes saved.') - return redirect('/admin/songs/%s' % id) + return redirect(basedir + 'admin/songs/%s' % id) -@app.route('/admin/songs//delete', methods=['POST']) +@app.route(basedir + 'admin/songs//delete', methods=['POST']) @admin_required(level=100) def route_admin_songs_id_delete(id): song = db.songs.find_one({'id': id}) @@ -368,10 +374,10 @@ def route_admin_songs_id_delete(id): db.songs.delete_one({'id': id}) flash('Song deleted.') - return redirect('/admin/songs') + return redirect(basedir + 'admin/songs') -@app.route('/admin/users') +@app.route(basedir + 'admin/users') @admin_required(level=50) def route_admin_users(): user = db.users.find_one({'username': session.get('username')}) @@ -379,7 +385,7 @@ def route_admin_users(): return render_template('admin_users.html', config=get_config(), max_level=max_level, username='', level='') -@app.route('/admin/users', methods=['POST']) +@app.route(basedir + 'admin/users', methods=['POST']) @admin_required(level=50) def route_admin_users_post(): admin_name = session.get('username') @@ -411,7 +417,7 @@ def route_admin_users_post(): return render_template('admin_users.html', config=get_config(), max_level=max_level, username=username, level=level) -@app.route('/api/preview') +@app.route(basedir + 'api/preview') @app.cache.cached(timeout=15, query_string=True) def route_api_preview(): song_id = request.args.get('id', None) @@ -432,7 +438,7 @@ def route_api_preview(): return redirect(get_config()['songs_baseurl'] + '%s/preview.mp3' % song_id) -@app.route('/api/songs') +@app.route(basedir + 'api/songs') @app.cache.cached(timeout=15) def route_api_songs(): songs = list(db.songs.find({'enabled': True}, {'_id': False, 'enabled': False})) @@ -460,20 +466,20 @@ def route_api_songs(): return jsonify(songs) -@app.route('/api/categories') +@app.route(basedir + 'api/categories') @app.cache.cached(timeout=15) def route_api_categories(): categories = list(db.categories.find({},{'_id': False})) return jsonify(categories) -@app.route('/api/config') +@app.route(basedir + 'api/config') @app.cache.cached(timeout=15) def route_api_config(): config = get_config(credentials=True) return jsonify(config) -@app.route('/api/register', methods=['POST']) +@app.route(basedir + 'api/register', methods=['POST']) def route_api_register(): data = request.get_json() if not schema.validate(data, schema.register): @@ -514,7 +520,7 @@ def route_api_register(): return jsonify({'status': 'ok', 'username': username, 'display_name': username, 'don': don}) -@app.route('/api/login', methods=['POST']) +@app.route(basedir + 'api/login', methods=['POST']) def route_api_login(): data = request.get_json() if not schema.validate(data, schema.login): @@ -541,14 +547,14 @@ def route_api_login(): return jsonify({'status': 'ok', 'username': result['username'], 'display_name': result['display_name'], 'don': don}) -@app.route('/api/logout', methods=['POST']) +@app.route(basedir + 'api/logout', methods=['POST']) @login_required def route_api_logout(): session.clear() return jsonify({'status': 'ok'}) -@app.route('/api/account/display_name', methods=['POST']) +@app.route(basedir + 'api/account/display_name', methods=['POST']) @login_required def route_api_account_display_name(): data = request.get_json() @@ -568,7 +574,7 @@ def route_api_account_display_name(): return jsonify({'status': 'ok', 'display_name': display_name}) -@app.route('/api/account/don', methods=['POST']) +@app.route(basedir + 'api/account/don', methods=['POST']) @login_required def route_api_account_don(): data = request.get_json() @@ -593,7 +599,7 @@ def route_api_account_don(): return jsonify({'status': 'ok', 'don': {'body_fill': don_body_fill, 'face_fill': don_face_fill}}) -@app.route('/api/account/password', methods=['POST']) +@app.route(basedir + 'api/account/password', methods=['POST']) @login_required def route_api_account_password(): data = request.get_json() @@ -621,7 +627,7 @@ def route_api_account_password(): return jsonify({'status': 'ok'}) -@app.route('/api/account/remove', methods=['POST']) +@app.route(basedir + 'api/account/remove', methods=['POST']) @login_required def route_api_account_remove(): data = request.get_json() @@ -640,7 +646,7 @@ def route_api_account_remove(): return jsonify({'status': 'ok'}) -@app.route('/api/scores/save', methods=['POST']) +@app.route(basedir + 'api/scores/save', methods=['POST']) @login_required def route_api_scores_save(): data = request.get_json() @@ -663,7 +669,7 @@ def route_api_scores_save(): return jsonify({'status': 'ok'}) -@app.route('/api/scores/get') +@app.route(basedir + 'api/scores/get') @login_required def route_api_scores_get(): username = session.get('username') @@ -680,7 +686,7 @@ def route_api_scores_get(): return jsonify({'status': 'ok', 'scores': scores, 'username': user['username'], 'display_name': user['display_name'], 'don': don}) -@app.route('/privacy') +@app.route(basedir + 'privacy') def route_api_privacy(): last_modified = time.strftime('%d %B %Y', time.gmtime(os.path.getmtime('templates/privacy.txt'))) integration = take_config('GOOGLE_CREDENTIALS')['gdrive_enabled'] if take_config('GOOGLE_CREDENTIALS') else False @@ -706,10 +712,26 @@ def make_preview(song_id, song_type, song_ext, preview): return prev_path +error_pages = take_config('ERROR_PAGES') or {} + +def create_error_page(code, url): + if url.startswith("http://") or url.startswith("https://"): + resp = requests.get(url) + if resp.status_code == 200: + app.register_error_handler(code, lambda e: (resp.content, code)) + else: + if url.startswith(basedir): + url = url[len(basedir):] + path = os.path.normpath(os.path.join("public", url)) + if os.path.isfile(path): + app.register_error_handler(code, lambda e: (send_from_directory(".", path), code)) + +for code in error_pages: + if error_pages[code]: + create_error_page(code, error_pages[code]) if __name__ == '__main__': import argparse - from flask import send_from_directory parser = argparse.ArgumentParser(description='Run the taiko-web development server.') parser.add_argument('port', type=int, metavar='PORT', nargs='?', default=34801, help='Port to listen on.') @@ -717,11 +739,11 @@ if __name__ == '__main__': parser.add_argument('-d', '--debug', action='store_true', help='Enable debug mode.') args = parser.parse_args() - @app.route('/src/') + @app.route(basedir + 'src/') def send_src(path): return send_from_directory('public/src', path) - @app.route('/assets/') + @app.route(basedir + 'assets/') def send_assets(path): return send_from_directory('public/assets', path) diff --git a/config.example.py b/config.example.py index 9a03ae5..68347c5 100644 --- a/config.example.py +++ b/config.example.py @@ -1,3 +1,6 @@ +# The base URL for Taiko Web, with trailing slash. +BASEDIR = '/' + # The full URL base asset URL, with trailing slash. ASSETS_BASEURL = '/assets/' @@ -7,6 +10,11 @@ SONGS_BASEURL = '/songs/' # Multiplayer websocket URL. Defaults to /p2 if blank. MULTIPLAYER_URL = '' +# Send static files for custom error pages +ERROR_PAGES = { + 404: '' +} + # The email address to display in the "About Simulator" menu. EMAIL = None diff --git a/public/src/js/customsongs.js b/public/src/js/customsongs.js index a20146c..7ded3ce 100644 --- a/public/src/js/customsongs.js +++ b/public/src/js/customsongs.js @@ -300,7 +300,7 @@ class CustomSongs{ this.loading(true) var importSongs = new ImportSongs(true) if(!gpicker){ - var gpickerPromise = loader.loadScript("/src/js/gpicker.js").then(() => { + var gpickerPromise = loader.loadScript("src/js/gpicker.js").then(() => { gpicker = new Gpicker() }) }else{ diff --git a/public/src/js/loader.js b/public/src/js/loader.js index bc42d90..f12b33d 100644 --- a/public/src/js/loader.js +++ b/public/src/js/loader.js @@ -13,11 +13,11 @@ class Loader{ var promises = [] - promises.push(this.ajax("/src/views/loader.html").then(page => { + promises.push(this.ajax("src/views/loader.html").then(page => { this.screen.innerHTML = page })) - promises.push(this.ajax("/api/config").then(conf => { + promises.push(this.ajax("api/config").then(conf => { gameConfig = JSON.parse(conf) })) @@ -39,7 +39,7 @@ class Loader{ assets.js.push("lib/oggmented-wasm.js") } assets.js.forEach(name => { - this.addPromise(this.loadScript("/src/js/" + name), "/src/js/" + name) + this.addPromise(this.loadScript("src/js/" + name), "src/js/" + name) }) var pageVersion = versionLink.href @@ -59,7 +59,7 @@ class Loader{ assets.css.forEach(name => { var stylesheet = document.createElement("link") stylesheet.rel = "stylesheet" - stylesheet.href = "/src/css/" + name + this.queryString + stylesheet.href = "src/css/" + name + this.queryString document.head.appendChild(stylesheet) }) var checkStyles = () => { @@ -124,13 +124,13 @@ class Loader{ assets.views.forEach(name => { var id = this.getFilename(name) - var url = "/src/views/" + name + this.queryString + var url = "src/views/" + name + this.queryString this.addPromise(this.ajax(url).then(page => { assets.pages[id] = page }), url) }) - this.addPromise(this.ajax("/api/categories").then(cats => { + this.addPromise(this.ajax("api/categories").then(cats => { assets.categories = JSON.parse(cats) assets.categories.forEach(cat => { if(cat.song_skin){ @@ -150,7 +150,7 @@ class Loader{ infoFill: "#656565" } }) - }), "/api/categories") + }), "api/categories") var url = gameConfig.assets_baseurl + "img/vectors.json" + this.queryString this.addPromise(this.ajax(url).then(response => { @@ -159,7 +159,7 @@ class Loader{ this.afterJSCount = [ - "/api/songs", + "api/songs", "blurPerformance", "categories" ].length + @@ -178,7 +178,7 @@ class Loader{ style.appendChild(document.createTextNode(css.join("\n"))) document.head.appendChild(style) - this.addPromise(this.ajax("/api/songs").then(songs => { + this.addPromise(this.ajax("api/songs").then(songs => { songs = JSON.parse(songs) songs.forEach(song => { var directory = gameConfig.songs_baseurl + song.id + "/" @@ -203,7 +203,7 @@ class Loader{ }) assets.songsDefault = songs assets.songs = assets.songsDefault - }), "/api/songs") + }), "api/songs") var categoryPromises = [] assets.categories //load category backgrounds to DOM @@ -276,7 +276,7 @@ class Loader{ }), "blurPerformance") if(gameConfig.accounts){ - this.addPromise(this.ajax("/api/scores/get").then(response => { + this.addPromise(this.ajax("api/scores/get").then(response => { response = JSON.parse(response) if(response.status === "ok"){ account.loggedIn = true @@ -286,7 +286,7 @@ class Loader{ scoreStorage.load(response.scores) pageEvents.send("login", account.username) } - }), "/api/scores/get") + }), "api/scores/get") } settings = new Settings() diff --git a/public/src/js/p2.js b/public/src/js/p2.js index 99ced66..02b7ebe 100644 --- a/public/src/js/p2.js +++ b/public/src/js/p2.js @@ -32,7 +32,7 @@ class P2Connection{ if(this.closed && !this.disabled){ this.closed = false var wsProtocol = location.protocol == "https:" ? "wss:" : "ws:" - this.socket = new WebSocket(gameConfig.multiplayer_url ? gameConfig.multiplayer_url : wsProtocol + "//" + location.host + "/p2") + this.socket = new WebSocket(gameConfig.multiplayer_url ? gameConfig.multiplayer_url : wsProtocol + "//" + location.host + location.pathname + "p2") pageEvents.race(this.socket, "open", "close").then(response => { if(response.type === "open"){ return this.openEvent() diff --git a/templates/admin.html b/templates/admin.html index d5d8118..3c8fc6b 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -7,13 +7,13 @@ - +
diff --git a/templates/admin_songs.html b/templates/admin_songs.html index 2f3bca4..8a89a37 100644 --- a/templates/admin_songs.html +++ b/templates/admin_songs.html @@ -1,14 +1,14 @@ {% extends 'admin.html' %} {% block content %} {% if admin.user_level >= 100 %} -New song +New song {% endif %}

Songs

{% for message in get_flashed_messages() %}
{{ message }}
{% endfor %} {% for song in songs %} - + - - + +