#!/usr/bin/env bash set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${ENV_FILE:-$ROOT_DIR/.env}" CLIENT_BUILD="" TTL_SECONDS=60 if [[ -f "$ENV_FILE" ]]; then set -a # shellcheck disable=SC1090 source "$ENV_FILE" set +a fi CLIENT_BUILD="${ACORE_REALMLIST_GAMEBUILD:-12340}" usage() { cat < [--build ] [--ttl <1-60>] Issues one single-use MoonWell launcher ticket and revokes the account's previous active ticket. The JSON response containing the ticket is written only to stdout; diagnostics go to stderr. EOF } if (($# == 0)) || [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then usage exit 0 fi ACCOUNT="$1" shift while (($#)); do case "$1" in --build) [[ $# -ge 2 ]] || { printf '%s\n' '--build requires a value' >&2; exit 1; } CLIENT_BUILD="$2" shift 2 ;; --ttl) [[ $# -ge 2 ]] || { printf '%s\n' '--ttl requires a value' >&2; exit 1; } TTL_SECONDS="$2" shift 2 ;; -h|--help) usage exit 0 ;; *) printf 'Unknown option: %s\n' "$1" >&2 exit 1 ;; esac done if ! [[ "$CLIENT_BUILD" =~ ^[0-9]+$ ]] || ((CLIENT_BUILD < 1 || CLIENT_BUILD > 65535)); then printf '%s\n' 'client build must be an integer from 1 to 65535' >&2 exit 1 fi if ! [[ "$TTL_SECONDS" =~ ^[0-9]+$ ]] || ((TTL_SECONDS < 1 || TTL_SECONDS > 60)); then printf '%s\n' 'ttl must be an integer from 1 to 60 seconds' >&2 exit 1 fi export MOONWELL_ISSUER_ACCOUNT="$ACCOUNT" mapfile -t ACCOUNT_DATA < <(python3 <<'PY' import json import os account = os.environ["MOONWELL_ISSUER_ACCOUNT"] account = "".join(chr(ord(ch) - 32) if "a" <= ch <= "z" else ch for ch in account) encoded = account.encode("ascii", "strict") if not encoded or len(encoded) > 17: raise SystemExit("game account must contain 1 to 17 ASCII bytes") if any(byte < 0x21 or byte > 0x7E for byte in encoded) or b"=" in encoded: raise SystemExit("game account must be printable ASCII without whitespace or '='") print(account) print(encoded.hex().upper()) print(json.dumps(account, ensure_ascii=True)) PY ) unset MOONWELL_ISSUER_ACCOUNT ACCOUNT CANONICAL_ACCOUNT="${ACCOUNT_DATA[0]}" ACCOUNT_HEX="${ACCOUNT_DATA[1]}" ACCOUNT_JSON="${ACCOUNT_DATA[2]}" unset ACCOUNT_DATA cd "$ROOT_DIR" if ! docker compose ps --status running --services | grep -qx 'ac-database'; then printf '%s\n' 'ac-database is not running; start it with ./start-server.sh' >&2 exit 1 fi ACCOUNT_ID="$(docker compose exec -T ac-database bash -lc \ 'mysql -N -B -uroot -p"$MYSQL_ROOT_PASSWORD" acore_auth' \ <<<"SELECT id FROM account WHERE username = CONVERT(0x$ACCOUNT_HEX USING ascii) LIMIT 1;")" if ! [[ "$ACCOUNT_ID" =~ ^[0-9]+$ ]]; then printf 'game account does not exist: %s\n' "$CANONICAL_ACCOUNT" >&2 exit 1 fi export MOONWELL_ISSUER_CANONICAL_ACCOUNT="$CANONICAL_ACCOUNT" mapfile -t TICKET_DATA < <(python3 <<'PY' import hashlib import os import secrets alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" account = os.environ["MOONWELL_ISSUER_CANONICAL_ACCOUNT"] ticket = "".join(secrets.choice(alphabet) for _ in range(16)) generation = secrets.token_bytes(16) salt = secrets.token_bytes(32) n = int("894B645E89E1535BBDAD5B8B290650530801B18EBFBF5E8FAB3C82872A3E9BB7", 16) inner = hashlib.sha1(f"{account}:{ticket}".encode("ascii")).digest() x = hashlib.sha1(salt + inner).digest() verifier = pow(7, int.from_bytes(x, "little"), n).to_bytes(32, "little") print(generation.hex().upper()) print(salt.hex().upper()) print(verifier.hex().upper()) print(ticket) PY ) unset MOONWELL_ISSUER_CANONICAL_ACCOUNT GENERATION_HEX="${TICKET_DATA[0]}" SALT_HEX="${TICKET_DATA[1]}" VERIFIER_HEX="${TICKET_DATA[2]}" TICKET="${TICKET_DATA[3]}" unset TICKET_DATA SQL="INSERT INTO launcher_ticket (account_id, generation_id, srp_salt, srp_verifier, client_build, issued_at, expires_at) VALUES ($ACCOUNT_ID, UNHEX('$GENERATION_HEX'), UNHEX('$SALT_HEX'), UNHEX('$VERIFIER_HEX'), $CLIENT_BUILD, UTC_TIMESTAMP(6), UTC_TIMESTAMP(6) + INTERVAL $TTL_SECONDS SECOND) ON DUPLICATE KEY UPDATE generation_id = VALUES(generation_id), srp_salt = VALUES(srp_salt), srp_verifier = VALUES(srp_verifier), client_build = VALUES(client_build), launcher_session_hash = NULL, issued_at = VALUES(issued_at), expires_at = VALUES(expires_at); SELECT DATE_FORMAT(expires_at, '%Y-%m-%dT%H:%i:%sZ') FROM launcher_ticket WHERE account_id = $ACCOUNT_ID;" EXPIRES_AT="$(docker compose exec -T ac-database bash -lc \ 'mysql -N -B -uroot -p"$MYSQL_ROOT_PASSWORD" acore_auth' <<<"$SQL")" printf '{"account":%s,"ticket":"%s","expires_at":"%s","client_build":%s}\n' \ "$ACCOUNT_JSON" "$TICKET" "$EXPIRES_AT" "$CLIENT_BUILD" TICKET="$(printf '%*s' 16 '')" unset TICKET SQL GENERATION_HEX SALT_HEX VERIFIER_HEX ACCOUNT_HEX CANONICAL_ACCOUNT ACCOUNT_JSON