WebHost: automatically fill PATCH_TARGET -> HOST_ADDRESS and re-use it for rooms (#1518)

This commit is contained in:
Fabian Dill 2023-03-09 21:31:00 +01:00 committed by GitHub
parent 2e76085cf1
commit 7fdf38b2ad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 23 additions and 15 deletions

View File

@ -195,11 +195,11 @@ def get_public_ipv4() -> str:
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context() ctx = get_cert_none_ssl_context()
try: try:
ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx).read().decode("utf8").strip() ip = urllib.request.urlopen("https://checkip.amazonaws.com/", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception as e: except Exception as e:
# noinspection PyBroadException # noinspection PyBroadException
try: try:
ip = urllib.request.urlopen("https://v4.ident.me", context=ctx).read().decode("utf8").strip() ip = urllib.request.urlopen("https://v4.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception: except Exception:
logging.exception(e) logging.exception(e)
pass # we could be offline, in a local game, so no point in erroring out pass # we could be offline, in a local game, so no point in erroring out
@ -213,7 +213,7 @@ def get_public_ipv6() -> str:
ip = socket.gethostbyname(socket.gethostname()) ip = socket.gethostbyname(socket.gethostname())
ctx = get_cert_none_ssl_context() ctx = get_cert_none_ssl_context()
try: try:
ip = urllib.request.urlopen("https://v6.ident.me", context=ctx).read().decode("utf8").strip() ip = urllib.request.urlopen("https://v6.ident.me", context=ctx, timeout=10).read().decode("utf8").strip()
except Exception as e: except Exception as e:
logging.exception(e) logging.exception(e)
pass # we could be offline, in a local game, or ipv6 may not be available pass # we could be offline, in a local game, or ipv6 may not be available

View File

@ -33,6 +33,11 @@ def get_app():
import yaml import yaml
app.config.from_file(configpath, yaml.safe_load) app.config.from_file(configpath, yaml.safe_load)
logging.info(f"Updated config from {configpath}") logging.info(f"Updated config from {configpath}")
if not app.config["HOST_ADDRESS"]:
logging.info("Getting public IP, as HOST_ADDRESS is empty.")
app.config["HOST_ADDRESS"] = Utils.get_public_ipv4()
logging.info(f"HOST_ADDRESS was set to {app.config['HOST_ADDRESS']}")
db.bind(**app.config["PONY"]) db.bind(**app.config["PONY"])
db.generate_mapping(create_tables=True) db.generate_mapping(create_tables=True)
return app return app

View File

@ -51,7 +51,7 @@ app.config["PONY"] = {
app.config["MAX_ROLL"] = 20 app.config["MAX_ROLL"] = 20
app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache" app.config["CACHE_TYPE"] = "flask_caching.backends.SimpleCache"
app.config["JSON_AS_ASCII"] = False app.config["JSON_AS_ASCII"] = False
app.config["PATCH_TARGET"] = "archipelago.gg" app.config["HOST_ADDRESS"] = ""
cache = Cache(app) cache = Cache(app)
Compress(app) Compress(app)

View File

@ -179,6 +179,7 @@ class MultiworldInstance():
self.ponyconfig = config["PONY"] self.ponyconfig = config["PONY"]
self.cert = config["SELFLAUNCHCERT"] self.cert = config["SELFLAUNCHCERT"]
self.key = config["SELFLAUNCHKEY"] self.key = config["SELFLAUNCHKEY"]
self.host = config["HOST_ADDRESS"]
def start(self): def start(self):
if self.process and self.process.is_alive(): if self.process and self.process.is_alive():
@ -187,7 +188,7 @@ class MultiworldInstance():
logging.info(f"Spinning up {self.room_id}") logging.info(f"Spinning up {self.room_id}")
process = multiprocessing.Process(group=None, target=run_server_process, process = multiprocessing.Process(group=None, target=run_server_process,
args=(self.room_id, self.ponyconfig, get_static_server_data(), args=(self.room_id, self.ponyconfig, get_static_server_data(),
self.cert, self.key), self.cert, self.key, self.host),
name="MultiHost") name="MultiHost")
process.start() process.start()
# bind after start to prevent thread sync issues with guardian. # bind after start to prevent thread sync issues with guardian.

View File

@ -142,7 +142,8 @@ def get_static_server_data() -> dict:
def run_server_process(room_id, ponyconfig: dict, static_server_data: dict, def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str]): cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
host: str):
# establish DB connection for multidata and multisave # establish DB connection for multidata and multisave
db.bind(**ponyconfig) db.bind(**ponyconfig)
db.generate_mapping(check_tables=False) db.generate_mapping(check_tables=False)
@ -167,17 +168,18 @@ def run_server_process(room_id, ponyconfig: dict, static_server_data: dict,
for wssocket in ctx.server.ws_server.sockets: for wssocket in ctx.server.ws_server.sockets:
socketname = wssocket.getsockname() socketname = wssocket.getsockname()
if wssocket.family == socket.AF_INET6: if wssocket.family == socket.AF_INET6:
logging.info(f'Hosting game at [{get_public_ipv6()}]:{socketname[1]}')
# Prefer IPv4, as most users seem to not have working ipv6 support # Prefer IPv4, as most users seem to not have working ipv6 support
if not port: if not port:
port = socketname[1] port = socketname[1]
elif wssocket.family == socket.AF_INET: elif wssocket.family == socket.AF_INET:
logging.info(f'Hosting game at {get_public_ipv4()}:{socketname[1]}')
port = socketname[1] port = socketname[1]
if port: if port:
logging.info(f'Hosting game at {host}:{port}')
with db_session: with db_session:
room = Room.get(id=ctx.room_id) room = Room.get(id=ctx.room_id)
room.last_port = port room.last_port = port
else:
logging.exception("Could not determine port. Likely hosting failure.")
with db_session: with db_session:
ctx.auto_shutdown = Room.get(id=room_id).timeout ctx.auto_shutdown = Room.get(id=room_id).timeout
ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, [])) ctx.shutdown_task = asyncio.create_task(auto_shutdown(ctx, []))

View File

@ -26,7 +26,7 @@ def download_patch(room_id, patch_id):
with zipfile.ZipFile(filelike, "a") as zf: with zipfile.ZipFile(filelike, "a") as zf:
with zf.open("archipelago.json", "r") as f: with zf.open("archipelago.json", "r") as f:
manifest = json.load(f) manifest = json.load(f)
manifest["server"] = f"{app.config['PATCH_TARGET']}:{last_port}" if last_port else None manifest["server"] = f"{app.config['HOST_ADDRESS']}:{last_port}" if last_port else None
with zipfile.ZipFile(new_file, "w") as new_zip: with zipfile.ZipFile(new_file, "w") as new_zip:
for file in zf.infolist(): for file in zf.infolist():
if file.filename == "archipelago.json": if file.filename == "archipelago.json":
@ -64,7 +64,7 @@ def download_slot_file(room_id, player_id: int):
if slot_data.game == "Minecraft": if slot_data.game == "Minecraft":
from worlds.minecraft import mc_update_output from worlds.minecraft import mc_update_output
fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc" fname = f"AP_{app.jinja_env.filters['suuid'](room_id)}_P{slot_data.player_id}_{slot_data.player_name}.apmc"
data = mc_update_output(slot_data.data, server=app.config['PATCH_TARGET'], port=room.last_port) data = mc_update_output(slot_data.data, server=app.config['HOST_ADDRESS'], port=room.last_port)
return send_file(io.BytesIO(data), as_attachment=True, download_name=fname) return send_file(io.BytesIO(data), as_attachment=True, download_name=fname)
elif slot_data.game == "Factorio": elif slot_data.game == "Factorio":
with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf: with zipfile.ZipFile(io.BytesIO(slot_data.data)) as zf:

View File

@ -25,8 +25,8 @@
The most likely failure reason is that the multiworld is too old to be loaded now. The most likely failure reason is that the multiworld is too old to be loaded now.
{% elif room.last_port %} {% elif room.last_port %}
You can connect to this room by using <span class="interactive" You can connect to this room by using <span class="interactive"
data-tooltip="This means address/ip is {{ config['PATCH_TARGET'] }} and port is {{ room.last_port }}."> data-tooltip="This means address/ip is {{ config['HOST_ADDRESS'] }} and port is {{ room.last_port }}.">
'/connect {{ config['PATCH_TARGET'] }}:{{ room.last_port }}' '/connect {{ config['HOST_ADDRESS'] }}:{{ room.last_port }}'
</span> </span>
in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br> in the <a href="{{ url_for("tutorial_landing")}}">client</a>.<br>
{% endif %} {% endif %}

View File

@ -22,7 +22,7 @@
{% for patch in room.seed.slots|list|sort(attribute="player_id") %} {% for patch in room.seed.slots|list|sort(attribute="player_id") %}
<tr> <tr>
<td>{{ patch.player_id }}</td> <td>{{ patch.player_id }}</td>
<td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['PATCH_TARGET'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td> <td data-tooltip="Connect via TextClient"><a href="archipelago://{{ patch.player_name | e}}:@{{ config['HOST_ADDRESS'] }}:{{ room.last_port }}">{{ patch.player_name }}</a></td>
<td>{{ patch.game }}</td> <td>{{ patch.game }}</td>
<td> <td>
{% if patch.game == "Minecraft" %} {% if patch.game == "Minecraft" %}

View File

@ -48,5 +48,5 @@
# TODO # TODO
#JSON_AS_ASCII: false #JSON_AS_ASCII: false
# Patch target. This is the address encoded into the patch that will be used for client auto-connect. # Host Address. This is the address encoded into the patch that will be used for client auto-connect.
#PATCH_TARGET: archipelago.gg #HOST_ADDRESS: archipelago.gg