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:
Fabian Dill 2020-06-26 19:29:33 +02:00
parent 545bb8023c
commit e0e13ac59e
10 changed files with 162 additions and 102 deletions

View File

@ -1,16 +1,14 @@
"""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."""
import json
import os
import logging
import typing
import multiprocessing
import threading
import zlib
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 flaskext.autoversion import Autoversion
@ -22,13 +20,13 @@ os.makedirs(LOGS_FOLDER, exist_ok=True)
def allowed_file(filename):
return filename.endswith('multidata')
return filename.endswith(('multidata', ".zip"))
app = Flask(__name__)
Pony(app)
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
app.config["SECRET_KEY"] = os.urandom(32)
app.config['SESSION_PERMANENT'] = True
@ -78,19 +76,21 @@ class MultiworldInstance():
self.process = None
@app.route('/seed/<int:seed>')
def view_seed(seed: int):
@app.route('/seed/<uuid:seed>')
def view_seed(seed: UUID):
seed = Seed.get(id=seed)
if seed:
return render_template("view_seed.html", seed=seed)
else:
if not seed:
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>')
def new_room(seed: int):
@app.route('/new_room/<uuid:seed>')
def new_room(seed: UUID):
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()
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."
@app.route('/log/<int:room>')
def display_log(room: int):
@app.route('/log/<uuid:room>')
def display_log(room: UUID):
# noinspection PyTypeChecker
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()
@app.route('/hosted/<int:room>', methods=['GET', 'POST'])
def host_room(room: int):
@app.route('/hosted/<uuid:room>', methods=['GET', 'POST'])
def host_room(room: UUID):
room = Room.get(id=room)
if room is None:
return abort(404)
@ -135,30 +135,4 @@ def host_room(room: int):
from WebHost.customserver import run_server_process
from . import tracker # 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)
from . import tracker, upload # to trigger app routing picking up on it

View File

@ -7,26 +7,30 @@ db = Database()
class Patch(db.Entity):
id = PrimaryKey(int, auto=True)
player = Required(int)
data = Required(buffer)
simple_seed = Required('Seed')
seed = Optional('Seed')
class Room(db.Entity):
id = PrimaryKey(int, auto=True)
id = PrimaryKey(UUID, default=uuid4)
last_activity = Required(datetime, default=lambda: datetime.utcnow(), index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow())
owner = Required(UUID, index=True)
commands = Set('Command')
seed = Required('Seed', index=True)
multisave = Optional(Json)
show_spoiler = Required(int, default=0) # 0 -> never, 1 -> after completion, -> 2 always
timeout = Required(int, default=lambda: 6)
allow_tracker = Required(bool, default=True)
tracker = Optional(UUID, index=True)
last_port = Optional(int, default=lambda: 0)
class Seed(db.Entity):
id = PrimaryKey(int, auto=True)
id = PrimaryKey(UUID, default=uuid4)
rooms = Set(Room)
multidata = Optional(Json)
owner = Required(UUID, index=True)
creation_time = Required(datetime, default=lambda: datetime.utcnow())
patches = Set(Patch)
spoiler = Optional(str)

View File

@ -3,9 +3,11 @@
<title>Multiworld {{ room.id }}</title>
{% endblock %}
{% block body %}
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id }}</a><br>
{% if room.allow_tracker %}
This room has a <a href="{{ url_for("get_tracker", room=room.id) }}">Multiworld Tracker</a> enabled.<br>
{% if room.owner == session["_id"] %}
Room created from <a href="{{ url_for("view_seed", seed=room.seed.id) }}">Seed #{{ room.seed.id }}</a><br>
{% endif %}
{% if room.tracker %}
This room has a <a href="{{ url_for("get_tracker", tracker=room.tracker) }}">Multiworld Tracker</a> enabled.<br>
{% endif %}
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>

View File

@ -16,6 +16,7 @@
{% endwith %}
{% block body %}{% endblock %}
</div>
<br> {# spacing for notice #}
<footer class="page-footer" style="position: fixed; left: 0; bottom: 0; width: 100%; text-align: center">
<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>

View File

@ -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 %}

View File

@ -3,7 +3,7 @@
<title>Upload Multidata</title>
{% endblock %}
{% block body %}
<h1>Upload Multidata</h1>
<h1>Upload Multidata or Multiworld Zip</h1>
<form method=post enctype=multipart/form-data>
<input type=file name=file>
<input type=submit value=Upload>

View File

@ -1,9 +1,11 @@
{% extends 'layout.html' %}
{% import "macros.html" as macros %}
{% block head %}
<title>Multiworld Seed {{ seed.id }}</title>
{% endblock %}
{% block body %}
Seed #{{ seed.id }}<br>
Created: {{ seed.creation_time }} UTC <br>
Players:
<ul class="list-group">
{% for team in seed.multidata["names"] %}
@ -16,12 +18,8 @@
</li>
{% endfor %}
</ul>
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 %}
{% call macros.list_rooms(rooms) %}
<li class="list-group-item list-group-item-action"><a href="{{ url_for("new_room", seed=seed.id) }}">new
room</a></li>
</ul>
{% endcall %}
{% endblock %}

View File

@ -4,6 +4,7 @@ from flask import render_template
from werkzeug.exceptions import abort
import datetime
import logging
from uuid import UUID
import Items
from WebHost import app, cache, Room
@ -191,57 +192,54 @@ def render_timedelta(delta: datetime.timedelta):
return f"{hours}:{minutes}"
@app.route('/tracker/<int:room>')
@app.route('/tracker/<uuid:tracker>')
@cache.memoize(timeout=30) # update every 30 seconds
def get_tracker(room: int):
room = Room.get(id=room)
def get_tracker(tracker: UUID):
room = Room.get(tracker=tracker)
if not room:
abort(404)
if room.allow_tracker:
multidata = room.seed.multidata
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
multidata = room.seed.multidata
locations = {tuple(k): tuple(v) for k, v in multidata['locations']}
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(multidata["names"])}
inventory = {teamnumber: {playernumber: collections.Counter() for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(multidata["names"])}
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(multidata["names"])}
precollected_items = room.seed.multidata.get("precollected_items", None)
checks_done = {teamnumber: {playernumber: {loc_name: 0 for loc_name in default_locations}
for playernumber in range(1, len(team) + 1)}
for teamnumber, team in enumerate(multidata["names"])}
precollected_items = room.seed.multidata.get("precollected_items", None)
for (team, player), locations_checked in room.multisave.get("location_checks", {}):
if precollected_items:
precollected = precollected_items[player - 1]
for item_id in precollected:
attribute_item(inventory, team, player, item_id)
for location in locations_checked:
item, recipient = locations[location, player]
attribute_item(inventory, team, recipient, item)
checks_done[team][player][location_to_area[location]] += 1
checks_done[team][player]["Total"] += 1
for (team, player), locations_checked in room.multisave.get("location_checks", {}):
if precollected_items:
precollected = precollected_items[player - 1]
for item_id in precollected:
attribute_item(inventory, team, player, item_id)
for location in locations_checked:
item, recipient = locations[location, player]
attribute_item(inventory, team, recipient, item)
checks_done[team][player][location_to_area[location]] += 1
checks_done[team][player]["Total"] += 1
for (team, player), game_state in room.multisave.get("client_game_state", []):
if game_state:
inventory[team][player][106] = 1 # Triforce
for (team, player), game_state in room.multisave.get("client_game_state", []):
if game_state:
inventory[team][player][106] = 1 # Triforce
activity_timers = {}
now = datetime.datetime.utcnow()
for (team, player), timestamp in room.multisave.get("client_activity_timers", []):
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
activity_timers = {}
now = datetime.datetime.utcnow()
for (team, player), timestamp in room.multisave.get("client_activity_timers", []):
activity_timers[team, player] = now - datetime.datetime.utcfromtimestamp(timestamp)
player_names = {}
for team, names in enumerate(multidata['names']):
for player, name in enumerate(names, 1):
player_names[(team, player)] = name
player_names = {}
for team, names in enumerate(multidata['names']):
for player, name in enumerate(names, 1):
player_names[(team, player)] = name
for (team, player), alias in room.multisave.get("name_aliases", []):
player_names[team, player] = alias
for (team, player), alias in room.multisave.get("name_aliases", []):
player_names[team, player] = alias
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
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)
else:
return "Tracker disabled for this room."
return render_template("tracker.html", inventory=inventory, get_item_name_from_id=get_item_name_from_id,
lookup_id_to_name=Items.lookup_id_to_name, player_names=player_names,
tracking_names=tracking_names, tracking_ids=tracking_ids, room=room, icons=icons,
multi_items=multi_items, checks_done=checks_done, ordered_areas=ordered_areas,
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)

73
WebHost/upload.py Normal file
View File

@ -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)

View File

@ -67,7 +67,7 @@ multi_mystery_options:
#include the spoiler log in the zip, 2 -> delete the non-zipped one
zip_spoiler: 0
#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_format: 1 # 1 -> zip, 2 -> 7z, 3->bz2
#create roms flagged as race roms