Last active
March 21, 2024 20:14
-
-
Save James-E-A/b97c1f6f3072460f519b71f89421afc7 to your computer and use it in GitHub Desktop.
Mostly self-contained bash+curl+sed+nc script to convert Xbox Live Gamertags into XUIDs (includes pseudo-UUID support for Floodgate-enabled Minecraft servers)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
set -e | |
set -o pipefail | |
# USAGE: | |
# bash gamertag_to_xuid.sh GAMERTAG [...] > out.csv | |
# DEPENDENCIES: | |
# - GNU Bash version 4 or newer (or any shell supporting pipefail, heredocs, read, readarray, and POSIX printf) | |
# - GNU sed version ?? or newer (or any sed implementation supporting, -n, -e, the "p" flag to "s", and capturing-group references in substitution) | |
# - cURL version 7.43 or newer (or any wrapper that implements -f, --data-raw, -H, and -s) | |
# - netcat (any vendor supporting -N; tested with BSD/Debian netcat) | |
# - TCP port 8080 on localhost | |
# - head (any vendor supporting -c; tested with GNU coreutils) | |
# - /dev/urandom | |
# - base64 (any vendor; tested with GNU coreutils) | |
# TODO: | |
# - cache the oauth bearer token to reduce number of logins | |
# - cache (OR AT LEAST PRINT TO STDERR) the oauth refresh token to further reduce number of logins | |
# - cache the XBL user token to reduce number of requests | |
# - cache the XBL XSTS token to reduce number of requests | |
# - disable pipefail during _catch_code incase port 8080 (or netcat) isn't available | |
main() { | |
if [ -z "${client_id+cid}" ]; then | |
client_id="388ea51c-0b25-4029-aae2-17df49d23905" | |
fi | |
if [ -z "${utok}" ]; then | |
if [ -z "${btok}" ]; then | |
if [ -z "${code+authorization_code}" ]; then | |
# Step 1a: Get an oauth authorization code | |
# (can be got from *any* XBL user) | |
code="$(_get_code)" | |
printf >"/dev/stderr" '%s=%s' "code" "${code:?code}" | |
fi | |
# Step 1b: Get an oauth bearer token | |
btok="$(_code2bearer "${code}")" | |
printf >"/dev/stderr" '%s="%s"\n' "btok" "${btok}" | |
fi | |
# Step 2a: Get an XSTS user token | |
utok="$(_bearer2user "${btok}")" | |
fi | |
# Step 2b: Get an XSTS XToken | |
readarray -t xsts <<<"$(_user2xsts "${utok}")" | |
printf '%s,%s,%s,%s\n' "gamertag" "xuid" "xuid (decimal)" "mcuuid" | |
for gt in "${@}"; do | |
# Step 3: fetch the user's profile and extract the decimal "id" field | |
uid="$(_gt2xuid "${gt}" "${xsts[@]}")" | |
# Step 4: convert the ID to hexadecimal or other desired display format(s) | |
xuid="$(hex_upper=1 _hex16 "${uid}")" | |
mcuuid="$(_xuid2mcuuid "${uid}")" | |
printf '%s,%s,%s,%s\n' "${gt}" "${xuid}" "${uid}" "${mcuuid}" | |
done | |
} | |
_gt2xuid() { | |
curl -s\ | |
-f "https://profile.xboxlive.com/users/gt(${1:?gamertag})/profile/settings"\ | |
-H "x-xbl-contract-version: 3"\ | |
-H "Authorization: XBL3.0 x=${3:?xsts_uhs};${2:?xtoken}"\ | |
| _json_key_str "id" | |
} | |
_get_code() { | |
nonce="$(_make_nonce)" | |
_prompt_login "${nonce}" | |
_catch_code "${nonce}" | |
} | |
_make_nonce() { | |
head -c 18 "/dev/urandom" | base64 | sed -e 's.+.-.g;s./._.g' | |
} | |
_prompt_login() { | |
_mkprompt_login >"/dev/stderr" "$@" | |
} | |
_mkprompt_login() { | |
printf 'Click this:\n\t%s\n' "$(_mklink_login "$@")" | |
} | |
_mklink_login() { | |
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/authentication/manager.py#L43-L58 | |
printf 'https://login.live.com/oauth20_authorize.srf?response_type=code&client_id=%s&approval_prompt=auto&redirect_uri=http%%3A%%2F%%2Flocalhost%%3A8080%%2Fauth%%2Fcallback&scope=Xboxlive.signin+Xboxlive.offline_access'"${1+&state=%s}" "${client_id?:client_id}" ${1+"${1}"} | |
} | |
_catch_code() { | |
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/scripts/authenticate.py#L94 | |
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/scripts/authenticate.py#L25 | |
body='<body><h1>Code:<br /><code>…</code></h1><script>document.querySelector("code").textContent=(new URLSearchParams(window.location.search).get("code"));</script></body>' | |
code="$( | |
printf '%s\r\n' "HTTP/1.1 200 OK" "Content-Type: text/html" ""\ | |
"${body}"\ | |
| nc -l "${2-8080}" -N | _grep "${1}" | _url_param "code" | |
#TODO: get a proper circular pipe into netcat | |
)" | |
if [ -z "${code}" ]; then | |
read -t 500 -p "There was an error getting the authorization code! Paste it if you can: " code | |
fi | |
} | |
_code2bearer() { | |
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/authentication/manager.py#L83-L114 | |
curl -s\ | |
-f "https://login.live.com/oauth20_token.srf"\ | |
--data-raw "grant_type=authorization_code"\ | |
--data-raw "code=${1:?authorization_code}"\ | |
--data-raw "scope=Xboxlive.signin+Xboxlive.offline_access"\ | |
--data-raw "redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fauth%2Fcallback"\ | |
--data-raw "client_id=${client_id?:client_id}"\ | |
${client_secret+--data-raw "client_secret=${client_secret}"}\ | |
| _json_key_str "access_token" | |
} | |
_bearer2user() { | |
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/authentication/manager.py#L116-L138 | |
curl -s\ | |
-f "https://user.auth.xboxlive.com/user/authenticate"\ | |
-H "x-xbl-contract-version: 1"\ | |
-H "Content-Type: application/json"\ | |
--data-raw '{"RelyingParty":"http://auth.xboxlive.com","TokenType":"JWT","Properties":{"AuthMethod":"RPS","SiteName":"user.auth.xboxlive.com","RpsTicket":"d='"${1:?bearer_token}"'"}}'\ | |
| _json_key_str "Token" | |
} | |
_user2xsts() { | |
# https://docs.microsoft.com/en-us/gaming/xbox-live/api-ref/xbox-live-rest/additional/edsauthorization | |
# https://github.com/OpenXbox/xbox-webapi-python/blob/v2.0.10/xbox/webapi/authentication/manager.py#L140-L157 | |
xsts_r="$(_user2xsts_raw "$@")" | |
printf '%s\n%s' "$(_json_key_str <<<"${xsts_r}" "Token")" "$(_json_key_str <<<"${xsts_r}" "uhs")" | |
} | |
_user2xsts_raw() { | |
curl -s\ | |
-f "https://xsts.auth.xboxlive.com/xsts/authorize"\ | |
-H "x-xbl-contract-version: 1"\ | |
-H "Content-Type: application/json"\ | |
--data-raw '{"RelyingParty":"http://xboxlive.com","TokenType":"JWT","Properties":{"UserTokens":["'"${1:?user_token}"'"],"SandboxId":"RETAIL"}}' | |
} | |
_json_key_str() { | |
sed -n -e "${2+${2} }"'s/^.*"'"${1?:key}"'"\s*:\s*"\([^"]\+\)".*$/\1/p' | |
#TODO escaping | |
} | |
_url_param() { | |
sed -n -e 's/^GET .\+[?&]\+'"${1}"'=\([^& ]*\).*$/\1/p' | |
#TODO escaping | |
} | |
_grep() { | |
sed -n -e '\`'"${1:?str}"'`p' | |
} | |
_hex32() { | |
printf '%032'"${hex_upper+X}${hex_upper-x}" "${1:?i}" | |
} | |
_hex16() { | |
printf '%016'"${hex_upper+X}${hex_upper-x}" "${1:?i}" | |
} | |
_xuid2mcuuid() { | |
_hex32 "${1?:uid}" | sed -e 's/\(.\{12\}\)\(.\{4\}\)\(.\{4\}\)\(.\{12\}\)/\1-\2-\3-\4/' | |
} | |
main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment