Files
moonwell-core/issue-launcher-ticket.sh
2026-08-17 20:24:17 +04:00

168 lines
4.9 KiB
Bash

#!/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 <<EOF
Usage: $(basename "$0") <game-account> [--build <client-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