#!/bin/sh # Litsl log grab (Mac and Linux) # # What this script does, in order: # 1. Looks in the usual Minecraft launcher folders in your home folder for log files. # 2. Picks the newest log that shows your Litsl ban. If no log shows it (normal when you were # banned while offline), picks the log of your most recent Litsl session instead. # 3. Shows you which file it picked and asks "y/N". Nothing is sent unless you answer y. # 4. Makes a temporary copy with access tokens, your computer username, emails and # IP addresses (except Litsl's) removed. # 5. Sends that one copy to Litsl for your ban appeal ticket, then deletes its temporary files. # # It needs no admin rights, installs nothing and changes none of your files. # The same source is shown at __LG_BASE__/ with the three values below left as placeholders. # shellcheck shell=sh set -u LG_BASE='__LG_BASE__' # where the log is sent LG_ID='__LG_ID__' # your one-time upload link LG_BAN='__LG_BAN__' # your Ban ID (digits), or empty when the ticket has none LG_TICKET='__LG_TICKET__' # your ticket number, only shown to you below # Litsl's own server addresses (current, and the one used until September 2026). These stay in the log. LG_LITSL_IPS='170.205.24.78 135.148.136.134' LG_MAX_TEXT=26214400 # 25 MB of text LG_MAX_FILE_KB=51200 # log files over 50 MB on disk are not looked at LG_MAX_GZIP=5242880 # 5 MB compressed LG_MAX_FILES=400 # newest log files looked at # Where the y/N answer is read from. Piped from curl, the script itself is on stdin, # so the answer has to come from the terminal. LG_TTY="${LITSL_LOGGRAB_TTY:-/dev/tty}" say() { printf '%s\n' "$*" >&2; } manual_help() { say '' say 'You can still send a log yourself: open your Minecraft folder, then "logs",' say 'and drag "latest.log" (or the dated .log.gz from the day of the ban) into your ticket.' say 'The log grab is optional. Your appeal is reviewed either way.' } LG_TMP='' cleanup() { if [ -n "$LG_TMP" ] && [ -d "$LG_TMP" ]; then rm -rf "$LG_TMP"; fi } trap cleanup EXIT trap 'cleanup; exit 130' INT TERM HUP # ---------------------------------------------------------------- where logs live lg_roots() { _h="${HOME:-}" [ -n "$_h" ] || return 0 case "$(uname -s 2>/dev/null || echo unknown)" in Darwin) _a="$_h/Library/Application Support" printf '%s\n' \ "$_a/minecraft" \ "$_a/ModrinthApp" \ "$_a/com.modrinth.theseus" \ "$_a/PrismLauncher" \ "$_a/PolyMC" \ "$_a/PollyMC" \ "$_a/FjordLauncher" \ "$_a/ElyPrismLauncher" \ "$_a/MultiMC" \ "/Applications/MultiMC.app/Data" \ "$_a/ATLauncher" \ "$_a/gdlauncher_next" \ "$_a/gdlauncher_carbon" \ "$_a/gdlauncher" \ "$_a/Badlion Client" \ "$_a/.feather" \ "$_a/feather" \ "$_a/LabyMod" \ "$_a/tlauncher" \ "$_a/.tlauncher" \ "$_a/technic" \ "$_a/.ftba" \ "$_a/ftblauncher" \ "$_h/.lunarclient" \ "$_h/.feather" \ "$_h/.tlauncher" \ "$_h/Documents/curseforge/minecraft" \ "$_h/curseforge/minecraft" ;; *) _d="${XDG_DATA_HOME:-$_h/.local/share}" _c="${XDG_CONFIG_HOME:-$_h/.config}" _f="$_h/.var/app" printf '%s\n' \ "$_h/.minecraft" \ "$_d/ModrinthApp" \ "$_d/com.modrinth.theseus" \ "$_d/PrismLauncher" \ "$_d/PolyMC" \ "$_d/PollyMC" \ "$_d/FjordLauncher" \ "$_d/ElyPrismLauncher" \ "$_d/multimc" \ "$_d/MultiMC" \ "$_d/ATLauncher" \ "$_d/atlauncher" \ "$_d/gdlauncher_next" \ "$_d/gdlauncher_carbon" \ "$_d/labymod" \ "$_c/gdlauncher_next" \ "$_c/gdlauncher_carbon" \ "$_c/Badlion Client" \ "$_h/.lunarclient" \ "$_h/.feather" \ "$_h/.tlauncher" \ "$_h/.technic" \ "$_h/.ftba" \ "$_h/.atlauncher" \ "$_h/Documents/curseforge/minecraft" \ "$_h/curseforge/minecraft" \ "$_f/com.mojang.Minecraft/.minecraft" \ "$_f/com.mojang.Minecraft/data/minecraft" \ "$_f/org.prismlauncher.PrismLauncher/data/PrismLauncher" \ "$_f/org.polymc.PolyMC/data/PolyMC" \ "$_f/org.multimc.MultiMC/data/multimc" \ "$_f/com.modrinth.ModrinthApp/data/ModrinthApp" \ "$_f/com.atlauncher.ATLauncher/data" \ "$_f/io.gdevs.GDLauncher/config/gdlauncher_next" \ "$_f/com.lunarclient.LunarClient/.lunarclient" \ "$_f/net.badlion.Client/.minecraft" \ "$_h/snap/mc-installer/current/.minecraft" \ "$_h/snap/minecraft-launcher/common/.minecraft" ;; esac } # Modification time in seconds, then the path. BSD (Mac) and GNU stat differ. lg_mtime_line() { if stat -c '%Y' / >/dev/null 2>&1; then stat -c '%Y %n' "$1" 2>/dev/null else stat -f '%m %N' "$1" 2>/dev/null fi } # Every *.log and *.log.gz inside a "logs" folder under the launcher folders. Big folders that # never hold logs (assets, libraries, saves, mods ...) are skipped. debug logs and files over # 50 MB are skipped. lg_candidates() { lg_roots | while IFS= read -r _root; do [ -d "$_root" ] || continue # -L: a launcher folder that is a link to another disk is followed. -maxdepth stops link loops; # a file reached twice through a loop sorts shortest path first below. find -L "$_root" -maxdepth 9 \ \( -type d \( -name assets -o -name libraries -o -name versions -o -name natives \ -o -name cache -o -name caches -o -name saves -o -name mods -o -name config \ -o -name resourcepacks -o -name shaderpacks -o -name screenshots -o -name backups \ -o -name crash-reports -o -name jre -o -name runtime -o -name java -o -name meta \ -o -name webcache -o -name node_modules -o -name .git \) -prune \) -o \ \( -type f \( -name '*.log' -o -name '*.log.gz' \) -path '*/logs/*' ! -name 'debug*' \ -size "-${LG_MAX_FILE_KB}k" -print \) \ 2>/dev/null done | while IFS= read -r _f; do [ -r "$_f" ] && [ -s "$_f" ] && lg_mtime_line "$_f" done | sort -t' ' -k1,1nr -k2 | awk '!seen[$0]++' | head -n "$LG_MAX_FILES" | cut -d' ' -f2- } lg_cat() { case "$1" in *.gz) gzip -dc -- "$1" 2>/dev/null ;; *) cat -- "$1" 2>/dev/null ;; esac } # Does this log show the ban with this Ban ID? "#1234" not followed by another digit. lg_has_ban_id() { lg_cat "$1" | LC_ALL=C grep -Eq "#${LG_BAN}([^0-9]|\$)" } # Does this log show a ban screen from Litsl? It has to mention Litsl, a disconnect, and a ban. lg_has_litsl_ban() { lg_cat "$1" | LC_ALL=C awk ' { l = tolower($0) } l ~ /litsl/ { a = 1 } l ~ /disconnect|connection lost|lost connection|kicked/ { b = 1 } l ~ /you are (ip[- ]?)?banned|you have been (ip[- ]?)?banned|banned from|ban id|ban appeal|has been banned/ { c = 1 } END { exit !(a && b && c) }' } # Did the player connect to Litsl in this log? The client writes "Connecting to , ". lg_has_litsl_session() { lg_cat "$1" | LC_ALL=C awk -v ips="$LG_LITSL_IPS" ' BEGIN { n = split(ips, ip, " ") } { l = tolower($0) at = index(l, "connecting to ") if (!at) next host = substr(l, at + 14) sub(/[, ].*$/, "", host) if (host ~ /(^|\.)litsl\.net\.?$/) { found = 1; exit } for (i = 1; i <= n; i++) if (host == ip[i]) { found = 1; exit } } END { exit !found }' } lg_launcher() { case "$(printf '%s' "$1" | tr '[:upper:]' '[:lower:]')" in *modrinth*|*theseus*) echo 'Modrinth App' ;; *elyprism*|*fjordlauncher*|*prismlauncher*) echo 'Prism Launcher' ;; *pollymc*|*polymc*) echo 'PolyMC' ;; *multimc*) echo 'MultiMC' ;; *atlauncher*) echo 'ATLauncher' ;; *gdlauncher*) echo 'GDLauncher' ;; *curseforge*) echo 'CurseForge' ;; *lunarclient*) echo 'Lunar Client' ;; *blclient*|*badlion*) echo 'Badlion Client' ;; *feather*) echo 'Feather Client' ;; *labymod*) echo 'LabyMod' ;; *tlauncher*) echo 'TLauncher' ;; *technic*) echo 'Technic Launcher' ;; *ftba*|*ftblauncher*) echo 'FTB App' ;; *minecraft*) echo 'Minecraft Launcher' ;; *) echo 'Unknown launcher' ;; esac } lg_size() { _b="$1" if [ "$_b" -lt 1024 ]; then printf '%s bytes' "$_b" elif [ "$_b" -lt 1048576 ]; then printf '%s KB' "$((_b / 1024))" else printf '%s.%s MB' "$((_b / 1048576))" "$(((_b % 1048576) * 10 / 1048576))" fi } # Escape a literal string for use inside a sed -E pattern (delimiter #). lg_sed_escape() { printf '%s' "$1" | sed -e 's/[][\\.^$*+?(){}|#/]/\\&/g'; } # Copy the log to stdout with private details removed. lg_redact() { _home_re="$(lg_sed_escape "${HOME:-/nonexistent-home}")" lg_cat "$1" | LC_ALL=C sed -E \ -e 's#(--[Aa]ccess[Tt]oken[=, ]+"?)[^] ,"]+#\1[removed]#g' \ -e 's#(--[Ss]ession[=, ]+"?)[^] ,"]+#\1[removed]#g' \ -e 's#(--(xuid|clientId|userProperties|profileProperties)[=, ]+"?)[^] ,"]+#\1[removed]#g' \ -e 's#((([Rr]efresh|[Aa]ccess|[Ii]d|[Cc]lient)_?[Tt]oken|[Ss]ession_?[Ii]d)\\?"?[ ]*[:=][ ]*\\?"?)[^ ,"}\\]+#\1[removed]#g' \ -e 's#(token:)[A-Za-z0-9._-]{6,}(:[0-9A-Fa-f-]{32,36})#\1[removed]\2#g' \ -e 's#(Session ID is token:)[^:) ]+#\1[removed]#g' \ -e 's#([Bb]earer[ ]+)[A-Za-z0-9._~+/=-]{8,}#\1[removed]#g' \ -e 's#eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]*#[removed-token]#g' \ -e "s#${_home_re}#~#g" \ -e 's#(/Users/|/home/|/run/media/|/media/|/Volumes/)[^/ :"'"'"']+#\1[user]#g' \ -e 's#([A-Za-z]:[\\/]+Users[\\/]+)[^\\/:"'"'"']+#\1[user]#g' \ -e 's#[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}#[email]#g' \ -e 's#([0-9A-Fa-f]{1,4}:){5,7}[0-9A-Fa-f]{1,4}#[ip]#g' \ -e 's#(^|[^A-Za-z0-9_:.])([0-9A-Fa-f]{1,4}:){1,7}:([0-9A-Fa-f]{1,4}(:[0-9A-Fa-f]{1,4}){0,6})?#\1[ip]#g' \ | LC_ALL=C awk -v keep="$LG_LITSL_IPS" ' # IPv4 addresses become [ip]. Left alone: the Litsl addresses, and numbers that are versions and not # addresses (mod list lines, "version 1.20.4.2", "foo-1.2.3.4.jar", "1.2.3.4+build", a part over 255). BEGIN { n = split(keep, k, " "); for (i = 1; i <= n; i++) litsl[k[i]] = 1 } { if ($0 ~ /^[ \t]+[-|\\]/ || $0 !~ /[0-9]\.[0-9]/) { print; next } out = ""; rest = $0 while (match(rest, /[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/)) { pre = out substr(rest, 1, RSTART - 1) m = substr(rest, RSTART, RLENGTH) rest = substr(rest, RSTART + RLENGTH) ok = !(m in litsl) split(m, a, ".") for (i = 1; i <= 4; i++) if (length(a[i]) > 3 || a[i] + 0 > 255) ok = 0 if (a[1] + 0 == 0) ok = 0 if (pre ~ /[0-9A-Za-z.+_-]$/) ok = 0 if (rest ~ /^[0-9A-Za-z+_-]/ || rest ~ /^\.[0-9A-Za-z]/) ok = 0 if (tolower(pre) ~ /(version|ver|java|jdk|jre|openjdk|build|release|loader|lwjgl|opengl|driver)[ :=]*$/) ok = 0 out = pre (ok ? "[ip]" : m) } print out rest }' } # ---------------------------------------------------------------- start say '' say ' Litsl log grab' if [ -n "$LG_TICKET" ]; then say " for ban appeal ticket #$LG_TICKET"; fi say '' say " How it works and the full source of this script: $LG_BASE/" say '' for _need in curl gzip find sed awk; do if ! command -v "$_need" >/dev/null 2>&1; then say "This needs \"$_need\", which is not installed here. Nothing was sent." manual_help exit 1 fi done LG_TMP="$(mktemp -d "${TMPDIR:-/tmp}/litsl-loggrab.XXXXXX")" || { say 'Could not create a temporary folder. Nothing was sent.' manual_help exit 1 } say 'Looking for Minecraft logs in the usual launcher folders...' lg_candidates >"$LG_TMP/candidates" LG_COUNT="$(wc -l <"$LG_TMP/candidates" | tr -d ' ')" if [ "$LG_COUNT" -eq 0 ]; then say '' say 'No Minecraft log files were found in the usual launcher folders.' say 'If your launcher keeps its game folder somewhere else, this script cannot see it.' say 'Nothing was sent.' manual_help exit 1 fi # Three kinds of find, best first. Files are looked at newest first. # 1. a log with your Ban ID in it # 2. a log that shows a Litsl ban screen # 3. the log of your most recent Litsl session (no ban screen in it) LG_PICK='' LG_KIND='' LG_BANSCREEN='' LG_SESSION='' while IFS= read -r _f; do [ -n "$_f" ] || continue if [ -n "$LG_BAN" ] && lg_has_ban_id "$_f"; then LG_PICK="$_f" LG_KIND='ban' break fi if [ -z "$LG_BANSCREEN" ] && lg_has_litsl_ban "$_f"; then LG_BANSCREEN="$_f" # Without a Ban ID to look for, the newest log with a Litsl ban screen is the answer. [ -n "$LG_BAN" ] || break fi if [ -z "$LG_SESSION" ] && [ -z "$LG_BANSCREEN" ] && lg_has_litsl_session "$_f"; then LG_SESSION="$_f" fi done <"$LG_TMP/candidates" if [ -z "$LG_PICK" ] && [ -n "$LG_BANSCREEN" ]; then LG_PICK="$LG_BANSCREEN" LG_KIND='ban' elif [ -z "$LG_PICK" ] && [ -n "$LG_SESSION" ]; then LG_PICK="$LG_SESSION" LG_KIND='session' fi if [ -z "$LG_PICK" ]; then say '' say "Looked at $LG_COUNT log file(s). None of them shows your ban or a visit to Litsl." say 'If you play Litsl with a launcher that keeps its game folder somewhere else,' say 'this script cannot see it.' say 'Nothing was sent.' manual_help exit 1 fi LG_NAME="$(basename "$LG_PICK")" LG_LAUNCHER="$(lg_launcher "$LG_PICK")" LG_ORIG_BYTES="$(wc -c <"$LG_PICK" | tr -d ' ')" # How big is it as text? Checked before any copy is made (a .log.gz can unpack to far more). LG_RAW_BYTES="$(lg_cat "$LG_PICK" | head -c "$((LG_MAX_TEXT + 1))" | wc -c | tr -d ' ')" if [ "$LG_RAW_BYTES" -gt "$LG_MAX_TEXT" ]; then say '' say "The log that was found is too big to send this way (over 25 MB of text):" say " $LG_PICK" say 'Nothing was sent.' manual_help exit 1 fi if stat -c '%Y' / >/dev/null 2>&1; then LG_DATE="$(date -d "@$(stat -c '%Y' "$LG_PICK")" '+%Y-%m-%d %H:%M' 2>/dev/null || true)" else LG_DATE="$(stat -f '%Sm' -t '%Y-%m-%d %H:%M' "$LG_PICK" 2>/dev/null || true)" fi # The copy that would be sent: private details removed, then made valid UTF-8 text. lg_redact "$LG_PICK" >"$LG_TMP/redacted.log" if command -v iconv >/dev/null 2>&1 && ! iconv -f UTF-8 -t UTF-8 "$LG_TMP/redacted.log" >/dev/null 2>&1; then iconv -c -f UTF-8 -t UTF-8 "$LG_TMP/redacted.log" >"$LG_TMP/clean.log" 2>/dev/null || true if [ -s "$LG_TMP/clean.log" ]; then mv "$LG_TMP/clean.log" "$LG_TMP/redacted.log"; fi fi LG_TEXT_BYTES="$(wc -c <"$LG_TMP/redacted.log" | tr -d ' ')" gzip -9 -c "$LG_TMP/redacted.log" >"$LG_TMP/upload.log.gz" LG_GZ_BYTES="$(wc -c <"$LG_TMP/upload.log.gz" | tr -d ' ')" say '' if [ "$LG_KIND" = 'ban' ]; then if [ -n "$LG_BAN" ] && LC_ALL=C grep -Eq "#${LG_BAN}([^0-9]|\$)" "$LG_TMP/redacted.log"; then say "This log shows your ban (#$LG_BAN):" elif [ -n "$LG_BAN" ]; then say "This log shows a Litsl ban screen (no log contains Ban ID #$LG_BAN):" else say 'This log shows your ban:' fi else say "No log shows the ban screen (that's normal if you were banned while offline)." say 'This is your most recent Litsl session:' fi say '' say " File: $LG_PICK" say " Date: ${LG_DATE:-unknown}" say " Launcher: $LG_LAUNCHER" say " Size: $(lg_size "$LG_ORIG_BYTES") on disk, $(lg_size "$LG_GZ_BYTES") to send (compressed)" say '' if [ "$LG_KIND" = 'ban' ]; then say ' Lines that show the ban (as they would be sent):' if [ -n "$LG_BAN" ] && LC_ALL=C grep -Eq "#${LG_BAN}([^0-9]|\$)" "$LG_TMP/redacted.log"; then LC_ALL=C grep -E "#${LG_BAN}([^0-9]|\$)" "$LG_TMP/redacted.log" | head -n 3 | cut -c1-200 | sed 's/^/ /' >&2 else LC_ALL=C grep -Ei 'banned|ban id|ban appeal' "$LG_TMP/redacted.log" | head -n 3 | cut -c1-200 | sed 's/^/ /' >&2 fi else say ' Where it joins Litsl (as it would be sent):' LC_ALL=C grep -Ei 'connecting to .*(litsl|170\.205\.24\.78|135\.148\.136\.134)' "$LG_TMP/redacted.log" | head -n 2 | cut -c1-200 | sed 's/^/ /' >&2 fi say '' say ' Removed from the copy: access tokens, your computer username, email addresses,' say ' and IP addresses other than the Litsl server. Your file on disk is not changed.' say '' if [ "$LG_TEXT_BYTES" -gt "$LG_MAX_TEXT" ] || [ "$LG_GZ_BYTES" -gt "$LG_MAX_GZIP" ]; then say 'This log is too big to send this way (limit: 25 MB of text, 5 MB compressed).' say 'Nothing was sent.' manual_help exit 1 fi if [ ! -r "$LG_TTY" ]; then say 'Cannot ask you to confirm here (no terminal), so nothing was sent.' manual_help exit 1 fi printf 'Send this one file to Litsl staff for your ban appeal? [y/N] ' >&2 LG_ANSWER='' IFS= read -r LG_ANSWER <"$LG_TTY" || LG_ANSWER='' case "$LG_ANSWER" in y|Y|yes|Yes|YES) ;; *) say '' say 'Cancelled. Nothing was sent.' exit 0 ;; esac case "$(uname -s 2>/dev/null || echo unknown)" in Darwin) LG_OS='macOS' ;; Linux) LG_OS='Linux' ;; *) LG_OS='Unix' ;; esac say 'Sending...' LG_CODE="$(curl -sS --max-time 180 -o "$LG_TMP/response" -w '%{http_code}' -X POST \ -H 'Content-Type: application/gzip' \ -H "X-Log-File: $LG_NAME" \ -H "X-Log-Launcher: $LG_LAUNCHER" \ -H "X-Log-OS: $LG_OS" \ -H "X-Log-Kind: $LG_KIND" \ --data-binary @"$LG_TMP/upload.log.gz" \ "$LG_BASE/up/$LG_ID" 2>"$LG_TMP/curl-error")" || LG_CODE='000' say '' if [ "$LG_CODE" = '200' ] || [ "$LG_CODE" = '201' ]; then say 'Done. Your log was sent and will show up in your ticket within a couple of minutes.' say 'You can close this window.' exit 0 fi if [ -s "$LG_TMP/response" ]; then say "The log was not sent: $(head -c 300 "$LG_TMP/response")" else say "The log was not sent (could not reach $LG_BASE)." fi manual_help exit 1