wip
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user