Make FIFA17 roster hostname configurable
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
# Copy to .env in this directory. Required for remote deployment.
|
||||
#
|
||||
# OPENFUT_ADVERTISE — the address of THIS host as seen from the game machine
|
||||
# (105). The responders advertise it to the client for every next hop (Blaze,
|
||||
# roster, UTAS, POW). Compose refuses to start without it.
|
||||
# OPENFUT_ADVERTISE — the IP address of THIS host as seen from the game machine
|
||||
# (105). Responders advertise it for Blaze, UTAS, telemetry, and QoS.
|
||||
OPENFUT_ADVERTISE=203.0.113.10 # <- REPLACE with this host's LAN IP
|
||||
|
||||
# OPENFUT_BIND — address the listeners bind inside the container.
|
||||
@@ -10,6 +9,11 @@ OPENFUT_ADVERTISE=203.0.113.10 # <- REPLACE with this host's LAN IP
|
||||
# uses the loopback default baked into the responders when unset.
|
||||
OPENFUT_BIND=0.0.0.0
|
||||
|
||||
# FIFA17's roster verifier accepts dNSName SANs but ignores iPAddress SANs.
|
||||
# Advertise the certificate's DNS identity, then resolve that one hostname to
|
||||
# OPENFUT_ADVERTISE on the client without changing the URL or certificate.
|
||||
OPENFUT_ROSTER_HOST=winter15.gosredirector.ea.com:8081
|
||||
|
||||
# OPENFUT_SERVERS — which Python responders Docker runs (space/comma separated).
|
||||
# Default (unset) = the server-side set: "blaze roster utas pow".
|
||||
#
|
||||
|
||||
@@ -37,18 +37,14 @@ RUN set -eu; \
|
||||
|
||||
COPY data/ /app/data/
|
||||
|
||||
# Redirector/roster TLS cert (CN/SAN = winter15.gosredirector.ea.com). ProtoSSL
|
||||
# cert-verify is patched client-side, so a self-signed cert is fine — but the
|
||||
# client dials the roster and redirector BY IP, and that path still checks the
|
||||
# SAN against the dialed address (it is NOT covered by the two patched gates), so
|
||||
# a cert without a matching IP SAN is rejected with fatal certificate_unknown
|
||||
# (docs/FIFA17_FUT_SQUAD_UPDATE_TLS.md). The advertised LAN IP is a RUNTIME value,
|
||||
# unknown here, so this bakes only a loopback-IP baseline and the entrypoint
|
||||
# reissues with IP:$OPENFUT_ADVERTISE at start.
|
||||
# Redirector/roster TLS certificate. FIFA17's roster verifier compares only
|
||||
# dNSName SAN entries, so deployment advertises winter15.gosredirector.ea.com
|
||||
# through OPENFUT_ROSTER_HOST and resolves that hostname on the client. The
|
||||
# entrypoint validates this stable certificate; it never reissues it for an IP
|
||||
# SAN that the verifier ignores.
|
||||
#
|
||||
# openssl therefore has to remain in the image for the entrypoint, not be dropped
|
||||
# with the apt lists. The pair is git-ignored (*.pem/*.key); regenerate if absent
|
||||
# so a fresh checkout builds without extra steps.
|
||||
# OpenSSL remains in the image both to create the git-ignored keypair on a fresh
|
||||
# checkout and to validate the configured DNS identity at startup.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends openssl && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
RUN if [ ! -s tools/redir_cert.pem ] || [ ! -s tools/redir_key.pem ]; then \
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# cp .env.example .env # set OPENFUT_ADVERTISE to THIS host's LAN IP
|
||||
# docker compose up -d --build
|
||||
#
|
||||
# Brings up the 5 responders the game dials. OPENFUT_ADVERTISE is the address
|
||||
# the servers hand the client (105) for every next hop (Blaze, roster, UTAS,
|
||||
# POW) and is required — there is no silent loopback fallback in remote mode.
|
||||
# Brings up the 5 responders the game dials. OPENFUT_ADVERTISE is the server IP
|
||||
# handed out for Blaze, UTAS, telemetry, and QoS; OPENFUT_ROSTER_HOST is the
|
||||
# certificate DNS identity handed out for roster HTTPS.
|
||||
#
|
||||
# The client (105) still needs its first-hop redirect (hook or DNAT) plus
|
||||
# autopatch.py running locally; see client_arm.sh and the FIFARUNBOOK.
|
||||
@@ -25,6 +25,9 @@ services:
|
||||
# Address advertised to the client for the next hop. MUST be this host's
|
||||
# LAN IP as seen from the game machine (105). Required (see .env.example).
|
||||
OPENFUT_ADVERTISE: "${OPENFUT_ADVERTISE:?set OPENFUT_ADVERTISE in .env to this host's LAN IP, e.g. 203.0.113.10}"
|
||||
# FIFA17 roster TLS matches only certificate dNSName SANs. The client must
|
||||
# resolve this hostname to OPENFUT_ADVERTISE.
|
||||
OPENFUT_ROSTER_HOST: "${OPENFUT_ROSTER_HOST:-winter15.gosredirector.ea.com:8081}"
|
||||
# POW content advertises port 8080 by default, which collides with the
|
||||
# openfut-core publish on this host. Remap it to 8085 on the host and
|
||||
# advertise the remapped endpoint.
|
||||
|
||||
@@ -8,45 +8,39 @@
|
||||
# autopatch.py is NOT run here: it patches the FIFA17.exe process memory and must
|
||||
# run on the box the game runs on.
|
||||
#
|
||||
# Address behaviour is driven by two env vars (see each responder):
|
||||
# OPENFUT_BIND bind address for every listener (container: 0.0.0.0)
|
||||
# OPENFUT_ADVERTISE address handed to the client for the next hop
|
||||
# (the server's LAN IP, e.g. 203.0.113.10)
|
||||
# Address behaviour is driven by three env vars (see each responder):
|
||||
# OPENFUT_BIND bind address for every listener (container: 0.0.0.0)
|
||||
# OPENFUT_ADVERTISE IP address handed out for Blaze, UTAS, telemetry, and QoS
|
||||
# OPENFUT_ROSTER_HOST certificate DNS host:port handed out for roster HTTPS
|
||||
# ============================================================================
|
||||
set -uo pipefail
|
||||
cd "$(dirname "$(readlink -f "$0")")/tools"
|
||||
|
||||
BIND="${OPENFUT_BIND:-0.0.0.0}"
|
||||
ADV="${OPENFUT_ADVERTISE:?OPENFUT_ADVERTISE must be set to the server LAN IP (e.g. 203.0.113.10)}"
|
||||
ROSTER_HOST="${OPENFUT_ROSTER_HOST:-winter15.gosredirector.ea.com:8081}"
|
||||
export OPENFUT_BIND="$BIND"
|
||||
export OPENFUT_ADVERTISE="$ADV"
|
||||
export OPENFUT_ROSTER_HOST="$ROSTER_HOST"
|
||||
# POW keys advertised by blaze must also point at the server, not loopback.
|
||||
export POW_HOST="${POW_HOST:-$ADV:8094}"
|
||||
export POW_CONTENT_HOST="${POW_CONTENT_HOST:-$ADV:8080}"
|
||||
export POW_ADDR="${POW_ADDR:-$BIND:8094}"
|
||||
export POW_CONTENT_ADDR="${POW_CONTENT_ADDR:-$BIND:8080}"
|
||||
|
||||
echo "[openfut] bind=$BIND advertise=$ADV"
|
||||
echo "[openfut] bind=$BIND advertise=$ADV roster=$ROSTER_HOST"
|
||||
|
||||
# The TLS cert every responder serves must carry the ADVERTISED IP in its SAN.
|
||||
# The client dials the roster (:8081) and redirector by that IP, and that path
|
||||
# validates the cert's SAN against the dialed address — it is NOT covered by the
|
||||
# two client-side ProtoSSL gates autopatch patches, so a cert lacking IP:$ADV is
|
||||
# rejected with fatal certificate_unknown and the FUT hub fails with "An error
|
||||
# occurred downloading the FUT Squad Update" (docs/FIFA17_FUT_SQUAD_UPDATE_TLS.md).
|
||||
# The advertised IP is unknown at image-build time, so reconcile it here: reissue
|
||||
# only when the current cert does not already carry it, so a restart reuses the
|
||||
# same cert (no per-start fingerprint churn) and this self-heals if $ADV changes.
|
||||
CERT=redir_cert.pem KEY=redir_key.pem
|
||||
if ! openssl x509 -in "$CERT" -noout -ext subjectAltName 2>/dev/null | grep -qF "IP Address:$ADV"; then
|
||||
echo "[openfut] reissuing TLS cert with SAN IP:$ADV (was missing it)"
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -keyout "$KEY" -out "$CERT" -days 3650 \
|
||||
-subj "/CN=winter15.gosredirector.ea.com" \
|
||||
-addext "subjectAltName=DNS:winter15.gosredirector.ea.com,DNS:*.gosredirector.ea.com,DNS:*.ea.com,IP:$ADV,IP:127.0.0.1" \
|
||||
>/dev/null 2>&1 \
|
||||
&& echo "[openfut] cert SAN now: $(openssl x509 -in "$CERT" -noout -ext subjectAltName 2>/dev/null | tail -1 | tr -s ' ')" \
|
||||
|| { echo "[openfut] FATAL: could not reissue TLS cert" >&2; exit 1; }
|
||||
# FIFA17's roster verifier compares only dNSName SAN entries. It ignores a valid
|
||||
# iPAddress SAN when the advertised URL contains an IP literal, so certificate
|
||||
# regeneration cannot fix that URL. Keep the certificate stable and fail startup
|
||||
# if the configured roster hostname is not already one of its DNS identities.
|
||||
CERT=redir_cert.pem
|
||||
ROSTER_NAME="${ROSTER_HOST%%:*}"
|
||||
if ! openssl x509 -in "$CERT" -noout -checkhost "$ROSTER_NAME" >/dev/null 2>&1; then
|
||||
echo "[openfut] FATAL: TLS cert does not cover roster hostname $ROSTER_NAME" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[openfut] roster certificate matches $ROSTER_NAME; fingerprint: $(openssl x509 -in "$CERT" -noout -fingerprint -sha256)"
|
||||
|
||||
# name script extra-env
|
||||
declare -a SERVERS=(
|
||||
|
||||
@@ -147,14 +147,13 @@ def refresh_account_identity():
|
||||
|
||||
# ================================================================== config
|
||||
#
|
||||
# Client/server split support (OpenFUT dev-container): two env vars, both
|
||||
# defaulting to loopback so the original all-on-localhost flow is byte-identical.
|
||||
# OPENFUT_BIND — the address the listeners bind (0.0.0.0 in a container).
|
||||
# OPENFUT_ADVERTISE — the address this server hands back to the client for the
|
||||
# NEXT hop (Blaze host, roster/UTAS/telemetry/QoS URLs). On
|
||||
# 105-local this is 127.0.0.1; on the 120 server it is the
|
||||
# server's LAN IP so the game dials 120 directly after the
|
||||
# first (hook/DNAT-redirected) contact.
|
||||
# Client/server split support (OpenFUT dev-container): bind and advertise default
|
||||
# to loopback so the original all-on-localhost flow is byte-identical.
|
||||
# OPENFUT_BIND — address the listeners bind (0.0.0.0 in a container).
|
||||
# OPENFUT_ADVERTISE — address handed back for Blaze, UTAS, telemetry, QoS,
|
||||
# and (unless overridden) the roster service.
|
||||
# OPENFUT_ROSTER_HOST — optional roster host:port advertised in HTTPS URLs.
|
||||
# Use a certificate dNSName and resolve it on the client.
|
||||
import os as _os_cfg
|
||||
_ADVERTISE = _os_cfg.environ.get("OPENFUT_ADVERTISE", "127.0.0.1")
|
||||
_BIND = _os_cfg.environ.get("OPENFUT_BIND", "127.0.0.1")
|
||||
@@ -563,9 +562,11 @@ OSDK_TICKER = []
|
||||
# never gets advance/back -> the silent FUT loading-screen hang. The store is the
|
||||
# MERGED '_all' section (getSection @0x14719e050), so any fetched CFID works; this
|
||||
# branch does NOT wrap the value ("https://%s" is only the ini path) -> ABSOLUTE url.
|
||||
# Serve HTTPS (EA's production value is https; the DirtySDK download mgr may reject
|
||||
# http). Our ProtoSSL cert-verify is patched (autopatch), so a self-signed cert is OK.
|
||||
ROSTER_HOST = "%s:8081" % _ADVERTISE
|
||||
# Serve HTTPS (EA's production value is https; the DirtySDK download manager may
|
||||
# reject http). FIFA17's roster verifier accepts dNSName SANs but ignores
|
||||
# iPAddress SANs, so an IP-literal URL fails with certificate_unknown. A remote
|
||||
# deployment can advertise a certificate DNS name without changing other hosts.
|
||||
ROSTER_HOST = os.environ.get("OPENFUT_ROSTER_HOST") or "%s:8081" % _ADVERTISE
|
||||
POW_CONTENT_HOST = os.environ.get("POW_CONTENT_HOST", "127.0.0.1:8080")
|
||||
OSDK_ROSTER = [
|
||||
("ROSTERUPDATE_URL", "https://%s/fifa17/fut/rosterupdate.xml" % ROSTER_HOST),
|
||||
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Standalone contract test for Blaze roster-host advertisement."""
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
|
||||
TOOLS = os.path.dirname(os.path.abspath(__file__))
|
||||
if TOOLS not in sys.path:
|
||||
sys.path.insert(0, TOOLS)
|
||||
|
||||
ADVERTISE = "192.0.2.10"
|
||||
DNS_HOST = "winter15.gosredirector.ea.com:8081"
|
||||
|
||||
|
||||
def assert_roster_config(blaze, host):
|
||||
config = dict(blaze.OSDK_ROSTER)
|
||||
assert blaze.ROSTER_HOST == host
|
||||
assert config["ROSTERUPDATE_URL"] == (
|
||||
f"https://{host}/fifa17/fut/rosterupdate.xml"
|
||||
)
|
||||
assert config["ROSTER_URL"] == f"https://{host}/fifa17/roster/"
|
||||
assert config["ROSTER_VER"] == "0"
|
||||
assert config["ROSTER_CSUM"] == ""
|
||||
|
||||
|
||||
def main():
|
||||
old_advertise = os.environ.get("OPENFUT_ADVERTISE")
|
||||
old_roster_host = os.environ.get("OPENFUT_ROSTER_HOST")
|
||||
try:
|
||||
os.environ["OPENFUT_ADVERTISE"] = ADVERTISE
|
||||
os.environ.pop("OPENFUT_ROSTER_HOST", None)
|
||||
|
||||
import blaze_responder_v3b as blaze
|
||||
|
||||
blaze = importlib.reload(blaze)
|
||||
assert_roster_config(blaze, f"{ADVERTISE}:8081")
|
||||
|
||||
os.environ["OPENFUT_ROSTER_HOST"] = DNS_HOST
|
||||
blaze = importlib.reload(blaze)
|
||||
assert_roster_config(blaze, DNS_HOST)
|
||||
|
||||
os.environ["OPENFUT_ROSTER_HOST"] = ""
|
||||
blaze = importlib.reload(blaze)
|
||||
assert_roster_config(blaze, f"{ADVERTISE}:8081")
|
||||
finally:
|
||||
if old_advertise is None:
|
||||
os.environ.pop("OPENFUT_ADVERTISE", None)
|
||||
else:
|
||||
os.environ["OPENFUT_ADVERTISE"] = old_advertise
|
||||
if old_roster_host is None:
|
||||
os.environ.pop("OPENFUT_ROSTER_HOST", None)
|
||||
else:
|
||||
os.environ["OPENFUT_ROSTER_HOST"] = old_roster_host
|
||||
|
||||
print("PASS: roster host defaults, override, and URLs")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+25
-13
@@ -12,10 +12,10 @@ ONE entry point. It starts, in order:
|
||||
and then prints the three `openfut.cfg` lines the operator must put on the client.
|
||||
Tear the whole thing down with `scripts/sold-staging-down.py`.
|
||||
|
||||
WHY the client only needs Blaze ports: FIFA 17 learns the UTAS base URL from Blaze
|
||||
(`blaze_responder_v3b.py`'s `UTAS_BASE`, where the `8099` is HARDCODED). This script
|
||||
copies the responder into the staging dir and rewrites that literal to the staging
|
||||
UTAS port, so pointing the client at staging Blaze is sufficient to move UTAS too.
|
||||
WHY the client only needs Blaze ports: FIFA 17 learns both the UTAS base URL and
|
||||
the roster URL from Blaze. This script copies the responder into the staging dir,
|
||||
rewrites the hardcoded UTAS port, and passes the independently configurable roster
|
||||
host through `OPENFUT_ROSTER_HOST`.
|
||||
|
||||
ISOLATION, enforced not assumed:
|
||||
* production ports 8099 8199 18080 8443 42127 42130 42131 4216 8080 8081 8094 are
|
||||
@@ -33,10 +33,11 @@ ISOLATION, enforced not assumed:
|
||||
edits `openfut.cfg` by hand, using the block this script prints.
|
||||
|
||||
Read-only reuse of production: staging Blaze advertises the production roster
|
||||
(10.10.0.120:8081) and POW content/API hosts (10.10.0.120:8085 / :8094) verbatim, the
|
||||
same values production Blaze advertises. Those services hold NO economy state (roster
|
||||
XML and POW content are static), staging never connects to them itself -- it only
|
||||
hands the client the same strings -- and they are therefore shared deliberately.
|
||||
service through a certificate dNSName (`winter15.gosredirector.ea.com:8081` by
|
||||
default) and advertises the production POW content/API hosts
|
||||
(`10.10.0.120:8085` / `:8094`) verbatim. These services hold NO economy state.
|
||||
The client must resolve the roster hostname to this server without changing the
|
||||
advertised URL.
|
||||
|
||||
python3 scripts/sold-staging-up.py --variant highest
|
||||
python3 scripts/sold-staging-up.py --variant buyNow --coins-processed 1 \
|
||||
@@ -98,6 +99,7 @@ ADVERTISE = "10.10.0.120"
|
||||
BIND = "0.0.0.0"
|
||||
POW_CONTENT_HOST = "10.10.0.120:8085"
|
||||
POW_HOST = "10.10.0.120:8094"
|
||||
DEFAULT_ROSTER_HOST = "winter15.gosredirector.ea.com:8081"
|
||||
|
||||
BLAZE_SRC = os.path.join(REPO, "fifa17-recon", "tools", "blaze_responder_v3b.py")
|
||||
BLAZE_ASSETS = ("redir_cert.pem", "redir_key.pem")
|
||||
@@ -729,11 +731,12 @@ def start_host(lay: Layout, variant: str, coins_processed: str, count_mode: str)
|
||||
return Launched("staging-utas-host", proc, lay.host_log, f"{BIND}:{HOST_PORT}")
|
||||
|
||||
|
||||
def start_blaze(lay: Layout) -> Launched:
|
||||
def start_blaze(lay: Layout, roster_host: str) -> Launched:
|
||||
env = dict(
|
||||
os.environ,
|
||||
OPENFUT_ADVERTISE=ADVERTISE,
|
||||
OPENFUT_BIND=BIND,
|
||||
OPENFUT_ROSTER_HOST=roster_host,
|
||||
# Read-only reuse of the production auxiliary services: static content, no
|
||||
# economy state, and staging never connects to them -- it only advertises
|
||||
# the same strings production Blaze advertises.
|
||||
@@ -766,7 +769,8 @@ def start_blaze(lay: Layout) -> Launched:
|
||||
)
|
||||
ok(
|
||||
f"staging blaze ready: redirector {BLAZE_REDIR_PORT}, main {BLAZE_MAIN_PORT}, "
|
||||
f"nucleus {BLAZE_NUCLEUS_PORT} (pid {proc.pid}); logged {want!r}"
|
||||
f"nucleus {BLAZE_NUCLEUS_PORT} (pid {proc.pid}); roster {roster_host}; "
|
||||
f"logged {want!r}"
|
||||
)
|
||||
return Launched(
|
||||
"staging-blaze", proc, lay.blaze_log,
|
||||
@@ -850,7 +854,7 @@ def cfg_block() -> list[str]:
|
||||
|
||||
|
||||
def print_summary(lay: Layout, variant: str, coins_processed: str, count_mode: str,
|
||||
records: list[dict]) -> None:
|
||||
roster_host: str, records: list[dict]) -> None:
|
||||
banner("STAGING STACK IS UP")
|
||||
rows = [
|
||||
("staging Core", f"127.0.0.1:{CORE_PORT}", "loopback only; client never talks to it"),
|
||||
@@ -888,6 +892,7 @@ def print_summary(lay: Layout, variant: str, coins_processed: str, count_mode: s
|
||||
print(f" experiment variant : {variant}")
|
||||
print(f" coinsProcessed : {coins_processed}")
|
||||
print(f" count mode : {count_mode}")
|
||||
print(f" roster host : {roster_host}")
|
||||
print(f" banner : {host_banner_line(lay)}")
|
||||
print(f" seeded state : {db_summary(lay)}")
|
||||
print(f" disposable item to sell : {DISPOSABLE_ITEM} ({DISPOSABLE_CARD})")
|
||||
@@ -931,6 +936,11 @@ def main() -> int:
|
||||
ap.add_argument("--count-mode", choices=["active", "active_plus_sold"],
|
||||
default="active",
|
||||
help="what /tradePile/counts.count reports (default: active)")
|
||||
ap.add_argument(
|
||||
"--roster-host",
|
||||
default=os.environ.get("OPENFUT_ROSTER_HOST", DEFAULT_ROSTER_HOST),
|
||||
help=f"host:port advertised for roster HTTPS (default: {DEFAULT_ROSTER_HOST})",
|
||||
)
|
||||
ap.add_argument("--dir", default=os.environ.get("OPENFUT_SOLD_STAGING_DIR",
|
||||
DEFAULT_STAGING_DIR),
|
||||
help=f"staging directory (default: {DEFAULT_STAGING_DIR})")
|
||||
@@ -968,7 +978,7 @@ def main() -> int:
|
||||
args.count_mode))
|
||||
|
||||
banner("STAGING BLAZE")
|
||||
started.append(start_blaze(lay))
|
||||
started.append(start_blaze(lay, args.roster_host))
|
||||
|
||||
records = [item.record() for item in started]
|
||||
for rec in records:
|
||||
@@ -986,6 +996,7 @@ def main() -> int:
|
||||
"variant": args.variant,
|
||||
"coins_processed": args.coins_processed,
|
||||
"count_mode": args.count_mode,
|
||||
"roster_host": args.roster_host,
|
||||
"ports": {
|
||||
"core": CORE_PORT,
|
||||
"utas_host": HOST_PORT,
|
||||
@@ -1012,7 +1023,8 @@ def main() -> int:
|
||||
banner("VERIFY ISOLATION")
|
||||
verify(lay, args.variant)
|
||||
|
||||
print_summary(lay, args.variant, args.coins_processed, args.count_mode, records)
|
||||
print_summary(lay, args.variant, args.coins_processed, args.count_mode,
|
||||
args.roster_host, records)
|
||||
return 0
|
||||
except Fatal as exc:
|
||||
print(f"\nFATAL: {exc}", file=sys.stderr)
|
||||
|
||||
Reference in New Issue
Block a user