move webhost over to UUID to make it nearly impossible to guess at seeds
Also introduce zip uploads and store the remaining relevant data, implemention of that still pending
This commit is contained in:
parent
545bb8023c
commit
e0e13ac59e
|
@ -1,16 +1,14 @@
|
||||||
"""Friendly reminder that if you want to host this somewhere on the internet, that it's licensed under MIT Berserker66
|
"""Friendly reminder that if you want to host this somewhere on the internet, that it's licensed under MIT Berserker66
|
||||||
So unless you're Berserker you need to include license information."""
|
So unless you're Berserker you need to include license information."""
|
||||||
|
|
||||||
import json
|
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
import typing
|
import typing
|
||||||
import multiprocessing
|
import multiprocessing
|
||||||
import threading
|
import threading
|
||||||
import zlib
|
|
||||||
|
|
||||||
from pony.flask import Pony
|
from pony.flask import Pony
|
||||||
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort, flash
|
from flask import Flask, request, redirect, url_for, render_template, Response, session, abort
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
from flaskext.autoversion import Autoversion
|
from flaskext.autoversion import Autoversion
|
||||||
|
|
||||||
|
@ -22,13 +20,13 @@ os.makedirs(LOGS_FOLDER, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
def allowed_file(filename):
|
def allowed_file(filename):
|
||||||
return filename.endswith('multidata')
|
return filename.endswith(('multidata', ".zip"))
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
Pony(app)
|
Pony(app)
|
||||||
|
|
||||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||||
app.config['MAX_CONTENT_LENGTH'] = 1 * 1024 * 1024 # 1 megabyte limit
|
app.config['MAX_CONTENT_LENGTH'] = 4 * 1024 * 1024 # 4 megabyte limit
|
||||||
# if you want persistent sessions on your server, make sure you make this a constant in your config.yaml
|
# if you want persistent sessions on your server, make sure you make this a constant in your config.yaml
|
||||||
app.config["SECRET_KEY"] = os.urandom(32)
|
app.config["SECRET_KEY"] = os.urandom(32)
|
||||||
app.config['SESSION_PERMANENT'] = True
|
app.config['SESSION_PERMANENT'] = True
|
||||||
|
@ -78,19 +76,21 @@ class MultiworldInstance():
|
||||||
self.process = None
|
self.process = None
|
||||||
|
|
||||||
|
|
||||||
@app.route('/seed/<int:seed>')
|
@app.route('/seed/<uuid:seed>')
|
||||||
def view_seed(seed: int):
|
def view_seed(seed: UUID):
|
||||||
seed = Seed.get(id=seed)
|
seed = Seed.get(id=seed)
|
||||||
if seed:
|
if not seed:
|
||||||
return render_template("view_seed.html", seed=seed)
|
|
||||||
else:
|
|
||||||
abort(404)
|
abort(404)
|
||||||
|
return render_template("view_seed.html", seed=seed,
|
||||||
|
rooms=[room for room in seed.rooms if room.owner == session["_id"]])
|
||||||
|
|
||||||
|
|
||||||
@app.route('/new_room/<int:seed>')
|
@app.route('/new_room/<uuid:seed>')
|
||||||
def new_room(seed: int):
|
def new_room(seed: UUID):
|
||||||
seed = Seed.get(id=seed)
|
seed = Seed.get(id=seed)
|
||||||
room = Room(seed=seed, owner=session["_id"])
|
if not seed:
|
||||||
|
abort(404)
|
||||||
|
room = Room(seed=seed, owner=session["_id"], tracker=uuid4())
|
||||||
commit()
|
commit()
|
||||||
return redirect(url_for("host_room", room=room.id))
|
return redirect(url_for("host_room", room=room.id))
|
||||||
|
|
||||||
|
@ -104,8 +104,8 @@ def _read_log(path: str):
|
||||||
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
f"Likely a crash during spinup of multiworld instance or it is still spinning up."
|
||||||
|
|
||||||
|
|
||||||
@app.route('/log/<int:room>')
|
@app.route('/log/<uuid:room>')
|
||||||
def display_log(room: int):
|
def display_log(room: UUID):
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
return Response(_read_log(os.path.join("logs", str(room) + ".txt")), mimetype="text/plain;charset=UTF-8")
|
||||||
|
|
||||||
|
@ -113,8 +113,8 @@ def display_log(room: int):
|
||||||
processstartlock = threading.Lock()
|
processstartlock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
@app.route('/hosted/<int:room>', methods=['GET', 'POST'])
|
@app.route('/hosted/<uuid:room>', methods=['GET', 'POST'])
|
||||||
def host_room(room: int):
|
def host_room(room: UUID):
|
||||||
room = Room.get(id=room)
|
room = Room.get(id=room)
|
||||||
if room is None:
|
if room is None:
|
||||||
return abort(404)
|
return abort(404)
|
||||||
|
@ -135,30 +135,4 @@ def host_room(room: int):
|
||||||
|
|
||||||
|
|
||||||
from WebHost.customserver import run_server_process
|
from WebHost.customserver import run_server_process
|
||||||
from . import tracker # to trigger app routing picking up on it
|
from . import tracker, upload # to trigger app routing picking up on it
|
||||||
|
|
||||||
@app.route('/', methods=['GET', 'POST'])
|
|
||||||
def upload_multidata():
|
|
||||||
if request.method == 'POST':
|
|
||||||
# check if the post request has the file part
|
|
||||||
if 'file' not in request.files:
|
|
||||||
flash('No file part')
|
|
||||||
else:
|
|
||||||
file = request.files['file']
|
|
||||||
# if user does not select file, browser also
|
|
||||||
# submit an empty part without filename
|
|
||||||
if file.filename == '':
|
|
||||||
flash('No selected file')
|
|
||||||
elif file and allowed_file(file.filename):
|
|
||||||
try:
|
|
||||||
multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig"))
|
|
||||||
except:
|
|
||||||
flash("Could not load multidata. File may be corrupted or incompatible.")
|
|
||||||
else:
|
|
||||||
seed = Seed(multidata=multidata)
|
|
||||||
commit() # place into DB and generate ids
|
|
||||||
return redirect(url_for("view_seed", seed=seed.id))
|
|
||||||
else:
|
|
||||||
flash("Not recognized file format. Awaiting a .multidata file.")
|
|
||||||
rooms = select(room for room in Room if room.owner == session["_id"])
|
|
||||||
return render_template("upload_multidata.html", rooms=rooms)
|
|
||||||
|
|
|
@ -7,26 +7,30 @@ db = Database()
|
||||||
|
|
||||||
class Patch(db.Entity):
|
class Patch(db.Entity):
|
||||||
id = PrimaryKey(int, auto=True)
|
id = PrimaryKey(int, auto=True)
|
||||||
|
player = Required(int)
|
||||||
data = Required(buffer)
|
data = Required(buffer)
|
||||||
simple_seed = Required('Seed')
|
seed = Optional('Seed')
|
||||||
|
|
||||||
|
|
||||||
class Room(db.Entity):
|
class Room(db.Entity):
|
||||||
id = PrimaryKey(int, auto=True)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
|
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
|
||||||
|
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
||||||
owner = Required(UUID, index=True)
|
owner = Required(UUID, index=True)
|
||||||
commands = Set('Command')
|
commands = Set('Command')
|
||||||
seed = Required('Seed', index=True)
|
seed = Required('Seed', index=True)
|
||||||
multisave = Optional(Json)
|
multisave = Optional(Json)
|
||||||
|
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
|
||||||
timeout = Required(int, default=lambda: 6)
|
timeout = Required(int, default=lambda: 6)
|
||||||
allow_tracker = Required(bool, default=True)
|
tracker = Optional(UUID, index=True)
|
||||||
last_port = Optional(int, default=lambda: 0)
|
last_port = Optional(int, default=lambda: 0)
|
||||||
|
|
||||||
|
|
||||||
class Seed(db.Entity):
|
class Seed(db.Entity):
|
||||||
id = PrimaryKey(int, auto=True)
|
id = PrimaryKey(UUID, default=uuid4)
|
||||||
rooms = Set(Room)
|
rooms = Set(Room)
|
||||||
multidata = Optional(Json)
|
multidata = Optional(Json)
|
||||||
|
owner = Required(UUID, index=True)
|
||||||
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
creation_time = Required(datetime, default=lambda: datetime.utcnow())
|
||||||
patches = Set(Patch)
|
patches = Set(Patch)
|
||||||
spoiler = Optional(str)
|
spoiler = Optional(str)
|
||||||
|
|
|
@ -3,9 +3,11 @@
|
||||||
<title>Multiworld {{ room.id }}</title>
|
<title>Multiworld {{ room.id }}</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
|
{% if room.owner == session["_id"] %}
|
||||||
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id }}</a><br>
|
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id }}</a><br>
|
||||||
{% if room.allow_tracker %}
|
{% endif %}
|
||||||
This room has a <a href="{{ url_for("get_tracker", room=room.id) }}">Multiworld Tracker</a> enabled.<br>
|
{% if room.tracker %}
|
||||||
|
This room has a <a href="{{ url_for("get_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.<br>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
This room will be closed after {{ room.timeout }} hours of inactivity. Should you wish to continue later,
|
This room will be closed after {{ room.timeout }} hours of inactivity. Should you wish to continue later,
|
||||||
you can simply refresh this page and the server will be started again.<br>
|
you can simply refresh this page and the server will be started again.<br>
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% block body %}{% endblock %}
|
{% block body %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
|
<br> {# spacing for notice #}
|
||||||
<footer class="page-footer" style="position: fixed; left: 0; bottom: 0; width: 100%; text-align: center">
|
<footer class="page-footer" style="position: fixed; left: 0; bottom: 0; width: 100%; text-align: center">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<span class="text-muted">This site uses a cookie to track your session in order to give you ownership over uploaded files and created instances.</span>
|
<span class="text-muted">This site uses a cookie to track your session in order to give you ownership over uploaded files and created instances.</span>
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
{% macro list_rooms(rooms) -%}
|
||||||
|
Rooms:
|
||||||
|
<ul class="list-group">
|
||||||
|
{% for room in rooms %}
|
||||||
|
<li class="list-group-item"><a href="{{ url_for("host_room", room=room.id) }}">Room #{{ room.id }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
{{ caller() }}
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
{%- endmacro %}
|
|
@ -3,7 +3,7 @@
|
||||||
<title>Upload Multidata</title>
|
<title>Upload Multidata</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
<h1>Upload Multidata</h1>
|
<h1>Upload Multidata or Multiworld Zip</h1>
|
||||||
<form method=post enctype=multipart/form-data>
|
<form method=post enctype=multipart/form-data>
|
||||||
<input type=file name=file>
|
<input type=file name=file>
|
||||||
<input type=submit value=Upload>
|
<input type=submit value=Upload>
|
|
@ -1,9 +1,11 @@
|
||||||
{% extends 'layout.html' %}
|
{% extends 'layout.html' %}
|
||||||
|
{% import "macros.html" as macros %}
|
||||||
{% block head %}
|
{% block head %}
|
||||||
<title>Multiworld Seed {{ seed.id }}</title>
|
<title>Multiworld Seed {{ seed.id }}</title>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block body %}
|
{% block body %}
|
||||||
Seed #{{ seed.id }}<br>
|
Seed #{{ seed.id }}<br>
|
||||||
|
Created: {{ seed.creation_time }} UTC <br>
|
||||||
Players:
|
Players:
|
||||||
<ul class="list-group">
|
<ul class="list-group">
|
||||||
{% for team in seed.multidata["names"] %}
|
{% for team in seed.multidata["names"] %}
|
||||||
|
@ -16,12 +18,8 @@
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
Rooms:
|
{% call macros.list_rooms(rooms) %}
|
||||||
<ul class="list-group">
|
|
||||||
{% for room in seed.rooms if room.owner == session["_id"] %}
|
|
||||||
<li class="list-group-item"><a href="{{ url_for("host_room", room=room.id) }}">Room #{{ room.id }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
<li class="list-group-item list-group-item-action"><a href="{{ url_for("new_room", seed=seed.id) }}">new
|
<li class="list-group-item list-group-item-action"><a href="{{ url_for("new_room", seed=seed.id) }}">new
|
||||||
room</a></li>
|
room</a></li>
|
||||||
</ul>
|
{% endcall %}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -4,6 +4,7 @@ from flask import render_template
|
||||||
from werkzeug.exceptions import abort
|
from werkzeug.exceptions import abort
|
||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
import Items
|
import Items
|
||||||
from WebHost import app, cache, Room
|
from WebHost import app, cache, Room
|
||||||
|
@ -191,13 +192,12 @@ def render_timedelta(delta: datetime.timedelta):
|
||||||
return f"{hours}:{minutes}"
|
return f"{hours}:{minutes}"
|
||||||
|
|
||||||
|
|
||||||
@app.route('/tracker/<int:room>')
|
@app.route('/tracker/<uuid:tracker>')
|
||||||
@cache.memoize(timeout=30) # update every 30 seconds
|
@cache.memoize(timeout=30) # update every 30 seconds
|
||||||
def get_tracker(room: int):
|
def get_tracker(tracker: UUID):
|
||||||
room = Room.get(id=room)
|
room = Room.get(tracker=tracker)
|
||||||
if not room:
|
if not room:
|
||||||
abort(404)
|
abort(404)
|
||||||
if room.allow_tracker:
|
|
||||||
multidata = room.seed.multidata
|
multidata = room.seed.multidata
|
||||||
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
|
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
|
||||||
|
|
||||||
|
@ -243,5 +243,3 @@ def get_tracker(room: int):
|
||||||
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
|
||||||
checks_in_area=checks_in_area, activity_timers=activity_timers,
|
checks_in_area=checks_in_area, activity_timers=activity_timers,
|
||||||
key_locations=key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids)
|
key_locations=key_locations, small_key_ids=small_key_ids, big_key_ids=big_key_ids)
|
||||||
else:
|
|
||||||
return "Tracker disabled for this room."
|
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import json
|
||||||
|
import zlib
|
||||||
|
import zipfile
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from flask import request, flash, redirect, url_for, session, render_template
|
||||||
|
from pony.orm import commit, select
|
||||||
|
|
||||||
|
from WebHost import app, allowed_file, Seed, Room, Patch
|
||||||
|
|
||||||
|
accepted_zip_contents = {"patches": ".bmbp",
|
||||||
|
"spoiler": ".txt",
|
||||||
|
"multidata": "multidata"}
|
||||||
|
|
||||||
|
banned_zip_contents = (".sfc",)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/', methods=['GET', 'POST'])
|
||||||
|
def upload_game():
|
||||||
|
if request.method == 'POST':
|
||||||
|
# check if the post request has the file part
|
||||||
|
if 'file' not in request.files:
|
||||||
|
flash('No file part')
|
||||||
|
else:
|
||||||
|
file = request.files['file']
|
||||||
|
# if user does not select file, browser also
|
||||||
|
# submit an empty part without filename
|
||||||
|
if file.filename == '':
|
||||||
|
flash('No selected file')
|
||||||
|
elif file and allowed_file(file.filename):
|
||||||
|
if file.filename.endswith(".zip"):
|
||||||
|
patches = set()
|
||||||
|
spoiler = ""
|
||||||
|
multidata = None
|
||||||
|
with zipfile.ZipFile(file, 'r') as zfile:
|
||||||
|
infolist = zfile.infolist()
|
||||||
|
|
||||||
|
for file in infolist:
|
||||||
|
if file.filename.endswith(banned_zip_contents):
|
||||||
|
return "Uploaded data contained a rom file, which is likely to contain copyrighted material. Your file was deleted."
|
||||||
|
elif file.filename.endswith(".bmbp"):
|
||||||
|
player = int(file.filename.split("P")[-1].split(".")[0].split("_")[0])
|
||||||
|
patches.add(Patch(data=zfile.open(file, "r").read(), player=player))
|
||||||
|
elif file.filename.endswith(".txt"):
|
||||||
|
spoiler = zfile.open(file, "rt").read().decode("utf-8-sig")
|
||||||
|
elif file.filename.endswith("multidata"):
|
||||||
|
try:
|
||||||
|
multidata = json.loads(zlib.decompress(zfile.open(file).read()).decode("utf-8-sig"))
|
||||||
|
except:
|
||||||
|
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||||
|
if multidata:
|
||||||
|
commit() # commit patches
|
||||||
|
seed = Seed(multidata=multidata, spoiler=spoiler, patches=patches, owner=session["_id"])
|
||||||
|
commit() # create seed
|
||||||
|
for patch in patches:
|
||||||
|
patch.seed = seed
|
||||||
|
|
||||||
|
return redirect(url_for("view_seed", seed=seed.id))
|
||||||
|
else:
|
||||||
|
flash("No multidata was found in the zip file, which is required.")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
multidata = json.loads(zlib.decompress(file.read()).decode("utf-8-sig"))
|
||||||
|
except:
|
||||||
|
flash("Could not load multidata. File may be corrupted or incompatible.")
|
||||||
|
else:
|
||||||
|
seed = Seed(multidata=multidata, owner=session["_id"], private=False)
|
||||||
|
commit() # place into DB and generate ids
|
||||||
|
return redirect(url_for("view_seed", seed=seed.id))
|
||||||
|
else:
|
||||||
|
flash("Not recognized file format. Awaiting a .multidata file.")
|
||||||
|
rooms = select(room for room in Room if room.owner == session["_id"])
|
||||||
|
return render_template("upload_game.html", rooms=rooms)
|
|
@ -67,7 +67,7 @@ multi_mystery_options:
|
||||||
#include the spoiler log in the zip, 2 -> delete the non-zipped one
|
#include the spoiler log in the zip, 2 -> delete the non-zipped one
|
||||||
zip_spoiler: 0
|
zip_spoiler: 0
|
||||||
#include the multidata file in the zip, 2 -> delete the non-zipped one, which also means the server won't autostart
|
#include the multidata file in the zip, 2 -> delete the non-zipped one, which also means the server won't autostart
|
||||||
zip_multidata: 0
|
zip_multidata: 1
|
||||||
#zip algorithm to use. zip is recommended for patch files, 7z is recommended for roms. All of them get the job done.
|
#zip algorithm to use. zip is recommended for patch files, 7z is recommended for roms. All of them get the job done.
|
||||||
zip_format: 1 # 1 -> zip, 2 -> 7z, 3->bz2
|
zip_format: 1 # 1 -> zip, 2 -> 7z, 3->bz2
|
||||||
#create roms flagged as race roms
|
#create roms flagged as race roms
|
||||||
|
|
Loading…
Reference in New Issue