#!/usr/bin/env bash # OpenFUT Setup Script # Builds, configures, and starts the OpenFUT offline FUT emulator for FIFA 23. # Run as a regular user; the script will sudo only for hosts/cert/iptables steps. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" CORE_DIR="$SCRIPT_DIR/openfut-core" BRIDGE_DIR="$SCRIPT_DIR/openfut-bridge" CORE_PORT="${CORE_PORT:-8080}" BRIDGE_PORT="${BRIDGE_PORT:-8443}" # internal port; iptables forwards 443 → this TLS_ENABLED="${TLS_ENABLED:-true}" CORE_PID_FILE="/tmp/openfut-core.pid" BRIDGE_PID_FILE="/tmp/openfut-bridge.pid" CORE_LOG="/tmp/openfut-core.log" BRIDGE_LOG="/tmp/openfut-bridge.log" # EA FUT domains to redirect. # Run './setup.sh watch-domains' while starting FIFA 23 to discover additional domains. EA_HOSTS=( # Core FUT API "fut.ea.com" "utas.mob.v4.fut.ea.com" # EA account / auth "accounts.ea.com" "signin.ea.com" "gateway.ea.com" # Origin / EA Desktop API "api1.origin.com" "api2.origin.com" "api3.origin.com" # Telemetry / assets (can safely be silenced) "eas.ea.com" "eaassets-a.akamaihd.net" "eaassets-b.akamaihd.net" ) HOSTS_MARKER="# OpenFUT — managed block (do not edit manually)" # ── Colours ──────────────────────────────────────────────────────────────────── RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m' info() { echo -e "${CYAN}[INFO]${RESET} $*"; } ok() { echo -e "${GREEN}[OK]${RESET} $*"; } warn() { echo -e "${YELLOW}[WARN]${RESET} $*"; } error() { echo -e "${RED}[ERROR]${RESET} $*" >&2; } heading() { echo -e "\n${BOLD}$*${RESET}"; } # ── Helpers ──────────────────────────────────────────────────────────────────── require_cmd() { command -v "$1" &>/dev/null || { error "Required command not found: $1"; exit 1; } } is_running() { local pid_file="$1" [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" 2>/dev/null } stop_service() { local pid_file="$1" name="$2" if is_running "$pid_file"; then kill "$(cat "$pid_file")" 2>/dev/null && ok "Stopped $name" rm -f "$pid_file" else warn "$name is not running" fi } # ── Commands ─────────────────────────────────────────────────────────────────── cmd_build() { heading "Building OpenFUT Core and Bridge…" require_cmd cargo info "Building Core (release)…" cargo build --release --manifest-path "$CORE_DIR/Cargo.toml" \ 2>&1 | grep -E "^(error|warning\[)" || true ok "Core built" info "Building Bridge (release)…" cargo build --release --manifest-path "$BRIDGE_DIR/Cargo.toml" \ 2>&1 | grep -E "^(error|warning\[)" || true ok "Bridge built" } cmd_start() { heading "Starting OpenFUT services…" # ── Core ── if is_running "$CORE_PID_FILE"; then warn "Core is already running (PID $(cat "$CORE_PID_FILE"))" else # Run from CORE_DIR so the default sqlite://openfut.db relative path works ( cd "$CORE_DIR" LISTEN_ADDR="127.0.0.1:$CORE_PORT" \ DATA_DIR="$CORE_DIR/data" \ "$CORE_DIR/target/release/openfut-core" \ >"$CORE_LOG" 2>&1 ) & echo $! > "$CORE_PID_FILE" ok "Core started (PID $!) → http://127.0.0.1:$CORE_PORT log: $CORE_LOG" fi # Give Core a moment to bind before Bridge starts sleep 1 # ── Bridge ── if is_running "$BRIDGE_PID_FILE"; then warn "Bridge is already running (PID $(cat "$BRIDGE_PID_FILE"))" else BRIDGE_LISTEN_ADDR="0.0.0.0:$BRIDGE_PORT" \ CORE_URL="http://127.0.0.1:$CORE_PORT" \ TLS_ENABLED="$TLS_ENABLED" \ PLACEHOLDER_MODE="true" \ CAPTURES_DIR="$BRIDGE_DIR/captures" \ "$BRIDGE_DIR/target/release/openfut-bridge" \ >"$BRIDGE_LOG" 2>&1 & echo $! > "$BRIDGE_PID_FILE" local scheme="http"; [[ "$TLS_ENABLED" == "true" ]] && scheme="https" ok "Bridge started (PID $!) → ${scheme}://0.0.0.0:$BRIDGE_PORT log: $BRIDGE_LOG" fi echo info "Dashboard: http://127.0.0.1:$CORE_PORT/_bridge/dashboard (via bridge: https://127.0.0.1:$BRIDGE_PORT/_bridge/dashboard)" info "Admin: https://127.0.0.1:$BRIDGE_PORT/_bridge/admin" info "Setup guide: https://127.0.0.1:$BRIDGE_PORT/_bridge/guide" } cmd_stop() { heading "Stopping OpenFUT services…" stop_service "$BRIDGE_PID_FILE" "Bridge" stop_service "$CORE_PID_FILE" "Core" cmd_unhosts cmd_unnat } cmd_status() { heading "OpenFUT service status" if is_running "$CORE_PID_FILE"; then ok "Core is RUNNING (PID $(cat "$CORE_PID_FILE"))" else warn "Core is STOPPED" fi if is_running "$BRIDGE_PID_FILE"; then ok "Bridge is RUNNING (PID $(cat "$BRIDGE_PID_FILE"))" else warn "Bridge is STOPPED" fi echo if grep -q "$HOSTS_MARKER" /etc/hosts 2>/dev/null; then ok "Hosts entries are IN PLACE" else warn "Hosts entries are NOT configured (run: ./setup.sh hosts)" fi if sudo iptables -t nat -L OUTPUT -n 2>/dev/null | grep -q "redir ports $BRIDGE_PORT"; then ok "NAT redirect 443→$BRIDGE_PORT is ACTIVE" else warn "NAT redirect is NOT active (run: ./setup.sh nat)" fi } cmd_logs() { heading "Tailing logs (Ctrl-C to stop)…" tail -f "$CORE_LOG" "$BRIDGE_LOG" 2>/dev/null || warn "No log files yet — start the services first." } cmd_hosts() { heading "Adding EA FUT hosts entries…" if grep -q "$HOSTS_MARKER" /etc/hosts 2>/dev/null; then ok "Hosts entries already present — skipping" return fi local block block="\n$HOSTS_MARKER\n" for host in "${EA_HOSTS[@]}"; do block+="127.0.0.1 $host\n" done block+="# end OpenFUT\n" echo -e "$block" | sudo tee -a /etc/hosts > /dev/null ok "Added to /etc/hosts:" for host in "${EA_HOSTS[@]}"; do echo " 127.0.0.1 $host" done info "Flush DNS if needed: sudo resolvectl flush-caches (or: sudo systemd-resolve --flush-caches)" } cmd_unhosts() { if grep -q "$HOSTS_MARKER" /etc/hosts 2>/dev/null; then sudo sed -i "/^$HOSTS_MARKER/,/^# end OpenFUT/d" /etc/hosts ok "Removed OpenFUT hosts entries" fi } cmd_nat() { heading "Setting up iptables redirect 443 → $BRIDGE_PORT…" if sudo iptables -t nat -L OUTPUT -n 2>/dev/null | grep -q "redir ports $BRIDGE_PORT"; then ok "NAT rule already active" return fi sudo iptables -t nat -A OUTPUT -p tcp --dport 443 -j REDIRECT --to-port "$BRIDGE_PORT" ok "iptables: OUTPUT 443 → $BRIDGE_PORT" warn "This rule is NOT persistent across reboots. Re-run './setup.sh nat' after reboot or use iptables-save." } cmd_unnat() { if sudo iptables -t nat -L OUTPUT -n 2>/dev/null | grep -q "redir ports $BRIDGE_PORT"; then sudo iptables -t nat -D OUTPUT -p tcp --dport 443 -j REDIRECT --to-port "$BRIDGE_PORT" ok "Removed iptables NAT rule" fi } cmd_cert() { heading "Installing TLS certificate…" local scheme="https" local cert_url="${scheme}://127.0.0.1:$BRIDGE_PORT/_bridge/cert.pem" local cert_dest="/tmp/openfut-ca.pem" if ! is_running "$BRIDGE_PID_FILE"; then error "Bridge is not running. Start it first with './setup.sh start'" exit 1 fi info "Downloading certificate from $cert_url …" curl -sk "$cert_url" -o "$cert_dest" || { error "Failed to download cert. Is the Bridge running with TLS_ENABLED=true?"; exit 1; } if [[ ! -s "$cert_dest" ]]; then error "Downloaded cert is empty." exit 1 fi ok "Certificate saved to $cert_dest" # Detect distro cert store if command -v update-ca-trust &>/dev/null; then # Arch / Fedora / RHEL sudo cp "$cert_dest" /etc/ca-certificates/trust-source/anchors/openfut-ca.pem sudo update-ca-trust ok "Certificate installed (update-ca-trust)" elif command -v update-ca-certificates &>/dev/null; then # Debian / Ubuntu sudo cp "$cert_dest" /usr/local/share/ca-certificates/openfut-ca.crt sudo update-ca-certificates ok "Certificate installed (update-ca-certificates)" else warn "Cannot auto-install cert — manual steps:" echo " Copy $cert_dest to your system's trusted CA store." fi info "If using Steam/Proton, you may also need to add it to the game's certificate store." info "Wine cert path (Proton): WINEDEBUG=-all wine certutil -addstore Root $cert_dest" } cmd_proton_cert() { heading "Installing certificate into Proton / Wine (for Steam FIFA 23)…" local cert_dest="/tmp/openfut-ca.pem" if [[ ! -f "$cert_dest" ]]; then error "No cert found at $cert_dest — run './setup.sh cert' first" exit 1 fi # Find Proton wine binary local proton_wine proton_wine=$(find "$HOME/.steam/steam/steamapps/common" -name "wine" -path "*/Proton*/wine" 2>/dev/null | head -1) if [[ -z "$proton_wine" ]]; then warn "Could not auto-detect Proton wine binary." echo " Manually run: wine certutil -addstore Root $cert_dest" echo " with the correct WINEPREFIX for FIFA 23." return fi # Find FIFA 23 Proton prefix local steam_id_fifa23="1811260" local prefix="$HOME/.steam/steam/steamapps/compatdata/$steam_id_fifa23/pfx" if [[ ! -d "$prefix" ]]; then warn "FIFA 23 Proton prefix not found at $prefix" echo " Launch FIFA 23 at least once to create the prefix, then re-run this step." return fi WINEPREFIX="$prefix" WINEDEBUG=-all "$proton_wine" certutil -addstore Root "$cert_dest" \ && ok "Certificate added to FIFA 23 Proton prefix" \ || warn "certutil failed — you may need to add it manually." } cmd_quickstart() { heading "OpenFUT Quick Start" echo "This will: build → start services → add hosts entries → set up NAT → install cert" echo read -rp "Continue? [y/N] " confirm [[ "${confirm,,}" == "y" ]] || { info "Aborted."; exit 0; } cmd_build cmd_start info "Waiting for services to initialise…" sleep 3 # Verify Core is actually up before proceeding if ! is_running "$CORE_PID_FILE"; then error "Core failed to start. Check the log:" tail -20 "$CORE_LOG" exit 1 fi if ! is_running "$BRIDGE_PID_FILE"; then error "Bridge failed to start. Check the log:" tail -20 "$BRIDGE_LOG" exit 1 fi # sudo-gated steps — try them, print manual instructions if sudo not available echo heading "System configuration (requires sudo)…" if sudo -n true 2>/dev/null; then cmd_hosts cmd_nat sleep 1 cmd_cert cmd_proton_cert else warn "Cannot acquire sudo non-interactively. Run these steps yourself:" echo echo " 1. sudo ./setup.sh hosts" echo " 2. sudo ./setup.sh nat" echo " 3. ./setup.sh cert (after the above two)" echo " 4. ./setup.sh proton-cert" fi echo heading "Setup complete!" ok "FIFA 23 FUT traffic will now route to OpenFUT." echo echo -e " Dashboard: ${CYAN}http://127.0.0.1:$CORE_PORT/_bridge/dashboard${RESET}" echo -e " Admin panel: ${CYAN}https://127.0.0.1:$BRIDGE_PORT/_bridge/admin${RESET}" echo -e " Logs: ${CYAN}./setup.sh logs${RESET}" echo -e " Stop all: ${CYAN}./setup.sh stop${RESET}" } cmd_watch_domains() { heading "Watching for unknown EA domains (Ctrl-C to stop)…" if ! is_running "$BRIDGE_PID_FILE"; then warn "Bridge is not running. Start it first with './setup.sh start'" exit 1 fi info "Launch FIFA 23 now. Any domain that reaches the real EA servers will appear here." echo # Watch the bridge log for lines that contain host/domain info not already in our list tail -f "$BRIDGE_LOG" 2>/dev/null | grep --line-buffered -i "unknown\|placeholder\|host\|domain\|CONNECT" \ | grep -v --line-buffered "$(IFS='|'; echo "${EA_HOSTS[*]}")" \ | while IFS= read -r line; do echo -e "${YELLOW}$(date +'%H:%M:%S')${RESET} $line" done } cmd_help() { cat < ${BOLD}Commands:${RESET} quickstart Full setup in one step (build + start + hosts + nat + cert) build Compile Core and Bridge in release mode start Start Core and Bridge in the background stop Stop both services and clean up hosts/NAT restart Stop then start status Show running state, hosts, and NAT status logs Tail live logs from both services watch-domains Show unknown EA domains hitting the bridge (add them to hosts) hosts Add EA FUT domains to /etc/hosts (requires sudo) unhosts Remove OpenFUT entries from /etc/hosts nat Add iptables redirect 443 → $BRIDGE_PORT (requires sudo) unnat Remove iptables NAT rule cert Download TLS cert from Bridge and install system-wide proton-cert Install TLS cert into the FIFA 23 Proton/Wine prefix ${BOLD}Environment overrides:${RESET} CORE_PORT Core listen port (default: 8080) BRIDGE_PORT Bridge internal port (default: 8443) TLS_ENABLED Enable HTTPS on Bridge (default: true) ${BOLD}Example:${RESET} ./setup.sh quickstart # first-time full setup ./setup.sh start # start after a reboot ./setup.sh stop # clean shutdown EOF } # ── Dispatch ─────────────────────────────────────────────────────────────────── case "${1:-help}" in quickstart) cmd_quickstart ;; build) cmd_build ;; start) cmd_start ;; stop) cmd_stop ;; restart) cmd_stop; cmd_start ;; status) cmd_status ;; logs) cmd_logs ;; hosts) cmd_hosts ;; unhosts) cmd_unhosts ;; nat) cmd_nat ;; unnat) cmd_unnat ;; cert) cmd_cert ;; proton-cert) cmd_proton_cert ;; watch-domains) cmd_watch_domains ;; help|--help|-h) cmd_help ;; *) error "Unknown command: $1"; cmd_help; exit 1 ;; esac