Compare commits

...
3 Commits
Author SHA1 Message Date
jetfir3 f4b9f7d20f Fix Linux rollback handling 2026-07-29 14:19:27 -04:00
jetfir3 852acb2acb Improve SPA safety 2026-07-29 13:57:25 -04:00
jetfir3 7f190c6660 Harden Linux client patching 2026-07-29 12:40:29 -04:00
+257 -21
View File
@@ -107,7 +107,7 @@ while getopts ':BcdefF:hilopP:SvV:-:' flag; do
noexp) excludeExp='true' ;; noexp) excludeExp='true' ;;
oldui) oldUi='true' ;; oldui) oldUi='true' ;;
premium) paidPremium='true' ;; premium) paidPremium='true' ;;
rollback) [[ "${platformType}" == "macOS" ]] && rollback='true'; installMac='true' ;; rollback) [[ "${platformType}" == "macOS" ]] && { rollback='true'; installMac='true'; } ;;
skipcodesign) [[ "${platformType}" == "macOS" ]] && skipCodesign='true' ;; skipcodesign) [[ "${platformType}" == "macOS" ]] && skipCodesign='true' ;;
stable) [[ "${platformType}" == "Linux" ]] && stableVar='true' ;; stable) [[ "${platformType}" == "Linux" ]] && stableVar='true' ;;
uninstall) uninstallSpotx='true' ;; uninstall) uninstallSpotx='true' ;;
@@ -486,9 +486,22 @@ run_prepare() {
} }
check_write_permission() { check_write_permission() {
local target_user="${SUDO_USER:-$(id -un)}" local writePath
[[ "${platformType}" == "Linux" && -z "${SPOTX_BUILD_MODE}" ]] && ((EUID == 0)) && {
stagedInstall='true'
protectedInstall='true'
}
for path_to_check in "$@"; do for path_to_check in "$@"; do
[[ ! -w "${path_to_check}" ]] && { [[ -d "${path_to_check}" ]] && writePath="${path_to_check}" || writePath="${path_to_check%/*}"
[[ ! -w "${path_to_check}" ]] && stagedInstall='true'
[[ ! -w "${writePath}" ]] && {
stagedInstall='true'
protectedInstall='true'
((EUID == 0)) && continue
command -v sudo >/dev/null || {
echo -e "\n${red}Error:${clr} sudo command not found. Install sudo or run this script as root.\n" >&2
exit 1
}
sudo -n true 2>/dev/null || { sudo -n true 2>/dev/null || {
echo -e "${yellow}Warning:${clr} SpotX-Bash does not have write permission in client directory.\nRequesting sudo permission..." >&2 echo -e "${yellow}Warning:${clr} SpotX-Bash does not have write permission in client directory.\nRequesting sudo permission..." >&2
sudo -v || { sudo -v || {
@@ -496,17 +509,172 @@ check_write_permission() {
exit 1 exit 1
} }
} }
sudo chown -R "${target_user}" "${path_to_check}"
sudo chmod -R u+rwX,go-w "${path_to_check}"
} }
done done
} }
sudo_run() {
if ((EUID == 0)) || [[ -z "${protectedInstall+x}" ]]; then
command "$@"
else
sudo "$@"
fi
}
protected_stage_copy() {
local source="${1}" destination="${2}"
sudo_run cat -- "${source}" > "${destination}" || {
rm -f -- "${destination}"
return 1
}
chmod 600 "${destination}"
}
protected_stage_prepare() {
targetAppBinary="${appBinary}"
targetAppBak="${appBak}"
targetXpuiPath="${xpuiPath}"
targetXpuiSpa="${xpuiSpa}"
targetXpuiBak="${xpuiBak}"
linux_working_dir
appPath="${workDir}/client"
appBinary="${appPath}/spotify"
appBak="${appBinary}.bak"
xpuiPath="${appPath}/Apps"
xpuiBak="${xpuiPath}/xpui.bak"
xpuiDir="${xpuiPath}/xpui"
xpuiSpa="${xpuiPath}/xpui.spa"
dwpPanelSectionJs="${xpuiDir}/dwp-panel-section.js"
homeHptoJs="${xpuiDir}/home-hpto.js"
homeV2Js="${xpuiDir}/home-v2.js"
indexHtml="${xpuiDir}/index.html"
vendorXpuiJs="${xpuiDir}/vendor~xpui.js"
xpuiCss="${xpuiDir}/xpui.css"
xpuiDesktopModalsJs="${xpuiDir}/xpui-desktop-modals.js"
xpuiJs="${xpuiDir}/xpui.js"
xpuiSnapshotJs="${xpuiDir}/xpui-snapshot.js"
mkdir -p "${xpuiPath}" || exit 1
protected_stage_copy "${targetAppBinary}" "${appBinary}" || exit 1
protected_stage_copy "${targetXpuiSpa}" "${xpuiSpa}" || exit 1
[[ -f "${targetAppBak}" ]] && protected_stage_copy "${targetAppBak}" "${appBak}"
[[ -f "${targetXpuiBak}" ]] && protected_stage_copy "${targetXpuiBak}" "${xpuiBak}"
}
protected_prepare_file() {
local source="${1}" destination="${2}" reference="${3}" tempFile
tempFile=$(sudo_run mktemp "${destination}.spotx.XXXXXXXX") || return 1
sudo_run cp -a -- "${reference}" "${tempFile}" &&
sudo_run cp -- "${source}" "${tempFile}" &&
sudo_run chown --reference="${reference}" "${tempFile}" &&
sudo_run chmod --reference="${reference}" "${tempFile}" &&
sudo_run touch -r "${reference}" "${tempFile}" || {
sudo_run rm -f -- "${tempFile}"
return 1
}
printf '%s\n' "${tempFile}"
}
protected_save_destination() {
local destination="${1}" tempFile
[[ -e "${destination}" ]] || return 0
tempFile=$(sudo_run mktemp "${destination}.spotx-rollback.XXXXXXXX") || return 1
sudo_run cp -a -- "${destination}" "${tempFile}" || {
sudo_run rm -f -- "${tempFile}"
return 1
}
printf '%s\n' "${tempFile}"
}
protected_restore_destination() {
local backup="${1}" destination="${2}" existed="${3}"
[[ "${existed}" ]] && sudo_run mv -f -- "${backup}" "${destination}" || sudo_run rm -f -- "${destination}"
}
protected_remove_files() {
local file
for file in "$@"; do
[[ -n "${file}" ]] && sudo_run rm -f -- "${file}"
done
}
protected_commit() {
local uninstall="${1:-}" appFile spaFile appBakFile='' xpuiBakFile=''
local appRollback='' spaRollback='' appBakRollback='' xpuiBakRollback=''
local appExisted='' spaExisted='' appBakExisted='' xpuiBakExisted=''
local appBakReference="${targetAppBinary}" xpuiBakReference="${targetXpuiSpa}"
[[ -f "${appBinary}" && -f "${xpuiSpa}" ]] || return 1
unzip -tqq "${xpuiSpa}" || return 1
appFile=$(protected_prepare_file "${appBinary}" "${targetAppBinary}" "${targetAppBinary}") || return 1
spaFile=$(protected_prepare_file "${xpuiSpa}" "${targetXpuiSpa}" "${targetXpuiSpa}") || {
sudo_run rm -f -- "${appFile}"
return 1
}
[[ -z "${uninstall}" ]] && {
[[ -e "${targetAppBak}" ]] && appBakReference="${targetAppBak}"
[[ -e "${targetXpuiBak}" ]] && xpuiBakReference="${targetXpuiBak}"
appBakFile=$(protected_prepare_file "${appBak}" "${targetAppBak}" "${appBakReference}") || {
sudo_run rm -f -- "${appFile}" "${spaFile}"
return 1
}
xpuiBakFile=$(protected_prepare_file "${xpuiBak}" "${targetXpuiBak}" "${xpuiBakReference}") || {
sudo_run rm -f -- "${appFile}" "${spaFile}" "${appBakFile}"
return 1
}
}
[[ -e "${targetAppBinary}" ]] && appExisted='true'
[[ -e "${targetXpuiSpa}" ]] && spaExisted='true'
[[ -e "${targetAppBak}" ]] && appBakExisted='true'
[[ -e "${targetXpuiBak}" ]] && xpuiBakExisted='true'
{
appRollback=$(protected_save_destination "${targetAppBinary}") &&
spaRollback=$(protected_save_destination "${targetXpuiSpa}")
[[ "${uninstall}" ]] || {
appBakRollback=$(protected_save_destination "${targetAppBak}") &&
xpuiBakRollback=$(protected_save_destination "${targetXpuiBak}")
}
} || {
protected_remove_files "${appFile}" "${spaFile}" "${appBakFile}" "${xpuiBakFile}"
protected_remove_files "${appRollback}" "${spaRollback}" "${appBakRollback}" "${xpuiBakRollback}"
return 1
}
{
{ [[ "${uninstall}" ]] || sudo_run mv -f -- "${appBakFile}" "${targetAppBak}"; } &&
{ [[ "${uninstall}" ]] || sudo_run mv -f -- "${xpuiBakFile}" "${targetXpuiBak}"; } &&
sudo_run mv -f -- "${appFile}" "${targetAppBinary}" &&
sudo_run mv -f -- "${spaFile}" "${targetXpuiSpa}"
} || {
protected_restore_destination "${appRollback}" "${targetAppBinary}" "${appExisted}"
protected_restore_destination "${spaRollback}" "${targetXpuiSpa}" "${spaExisted}"
[[ "${uninstall}" ]] || protected_restore_destination "${appBakRollback}" "${targetAppBak}" "${appBakExisted}"
[[ "${uninstall}" ]] || protected_restore_destination "${xpuiBakRollback}" "${targetXpuiBak}" "${xpuiBakExisted}"
protected_remove_files "${appFile}" "${spaFile}" "${appBakFile}" "${xpuiBakFile}"
return 1
}
protected_remove_files "${appRollback}" "${spaRollback}" "${appBakRollback}" "${xpuiBakRollback}"
[[ "${uninstall}" ]] && sudo_run rm -f -- "${targetAppBak}" "${targetXpuiBak}"
return 0
}
atomic_copy() {
local source="${1}" destination="${2}" tempFile result
copyTempDir=$(mktemp -d "${destination}.spotx.XXXXXXXX") || return 1
tempFile="${copyTempDir}/${destination##*/}"
cp "${source}" "${tempFile}" && mv -f "${tempFile}" "${destination}"
result=$?
rm -rf "${copyTempDir}" 2>/dev/null
[[ ! -d "${copyTempDir}" ]] && unset copyTempDir
return "${result}"
}
backup_spotx() {
atomic_copy "${xpuiSpa}" "${xpuiBak}" && atomic_copy "${appBinary}" "${appBak}" && return 0
rm -f "${appBak}" "${xpuiBak}" 2>/dev/null
return 1
}
uninstall_spotx() { uninstall_spotx() {
rm -f "${appBinary}" 2>/dev/null atomic_copy "${appBak}" "${appBinary}" && atomic_copy "${xpuiBak}" "${xpuiSpa}" || return 1
mv -f "${appBak}" "${appBinary}" rm -f "${appBak}" "${xpuiBak}" 2>/dev/null
rm -f "${xpuiSpa}" 2>/dev/null
mv -f "${xpuiBak}" "${xpuiSpa}"
rm -rf "${xpuiDir}" 2>/dev/null rm -rf "${xpuiDir}" 2>/dev/null
} }
@@ -517,12 +685,23 @@ run_uninstall_check() {
exit 1 exit 1
} }
check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}" check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}"
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && protected_stage_prepare
[[ "${cleanAB}" ]] && { [[ "${cleanAB}" ]] && {
echo -e "${yellow}Warning:${clr} SpotX-Bash has detected abnormal behavior.\nClient reinstallation may be required...\n" >&2 echo -e "${yellow}Warning:${clr} SpotX-Bash has detected abnormal behavior.\nClient reinstallation may be required...\n" >&2
rm -f "${appBak}" 2>/dev/null rm -f "${appBak}" 2>/dev/null
rm -f "${xpuiBak}" 2>/dev/null rm -f "${xpuiBak}" 2>/dev/null
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && sudo_run rm -f -- "${targetAppBak}" "${targetXpuiBak}"
} || { } || {
uninstall_spotx uninstall_spotx || {
echo -e "\n${red}Error:${clr} Failed to restore client. Backups were preserved.\n" >&2
exit 1
}
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && {
protected_commit 'true' || {
echo -e "\n${red}Error:${clr} Failed to restore client. Original files restored.\n" >&2
exit 1
}
}
} }
printf "\xE2\x9C\x94\x20\x46\x69\x6E\x69\x73\x68\x65\x64\x20\x75\x6E\x69\x6E\x73\x74\x61\x6C\x6C\n\n" printf "\xE2\x9C\x94\x20\x46\x69\x6E\x69\x73\x68\x65\x64\x20\x75\x6E\x69\x6E\x73\x74\x61\x6C\x6C\n\n"
exit 0 exit 0
@@ -584,7 +763,26 @@ sudo_check() {
} }
} }
linux_working_dir() { [[ -d "/tmp" ]] && workDir="/tmp" || workDir="$HOME"; } cleanup_temp_dirs() {
[[ -n "${workDir:-}" && -d "${workDir}" && "${workDir##*/}" == spotx-bash.* ]] && rm -rf -- "${workDir}"
[[ -n "${copyTempDir:-}" && -d "${copyTempDir}" && "${copyTempDir##*/}" == *.spotx.* ]] && rm -rf -- "${copyTempDir}"
[[ -n "${spaTempDir:-}" && -d "${spaTempDir}" && "${spaTempDir##*/}" == .spotx-spa.* ]] && rm -rf -- "${spaTempDir}"
[[ "${xpuiTempCreated:-}" && -n "${xpuiDir:-}" && -d "${xpuiDir}" && "${xpuiDir##*/}" == "xpui" ]] && rm -rf -- "${xpuiDir}"
}
linux_working_dir() {
local tempBase="${TMPDIR:-/tmp}"
[[ -n "${workDir:-}" && -d "${workDir}" && "${workDir##*/}" == spotx-bash.* ]] && return
[[ "${tempBase}" == /* && -d "${tempBase}" && -w "${tempBase}" ]] || tempBase="/tmp"
workDir=$(mktemp -d "${tempBase%/}/spotx-bash.XXXXXXXX") || {
echo -e "${red}Error:${clr} Failed to create temporary working directory.\n" >&2
exit 1
}
chmod 700 "${workDir}" || {
echo -e "${red}Error:${clr} Failed to secure temporary working directory.\n" >&2
exit 1
}
}
linux_deb_install() { linux_deb_install() {
sudo_check sudo_check
@@ -712,26 +910,34 @@ perlVar() {
xpui_detect() { xpui_detect() {
[[ (-f "${appBak}" || -f "${xpuiBak}") && "${cleanAB}" ]] && { [[ (-f "${appBak}" || -f "${xpuiBak}") && "${cleanAB}" ]] && {
rm -f "${appBak}" 2>/dev/null; rm -f "${xpuiBak}" 2>/dev/null backup_spotx || {
cp "${xpuiSpa}" "${xpuiBak}"; cp "${appBinary}" "${appBak}" echo -e "\n${red}Error:${clr} Failed to create client backup. Exiting...\n" >&2
exit 1
}
printf "\xE2\x9C\x94\x20\x43\x72\x65\x61\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n" printf "\xE2\x9C\x94\x20\x43\x72\x65\x61\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
return return
} }
[[ (-f "${appBak}" || -f "${xpuiBak}") && "${forceSpotx}" ]] && { [[ (-f "${appBak}" || -f "${xpuiBak}") && "${forceSpotx}" ]] && {
[[ -f "${appBak}" ]] && { rm -f "${appBinary}"; cp "${appBak}" "${appBinary}"; } { [[ ! -f "${appBak}" ]] || atomic_copy "${appBak}" "${appBinary}"; } &&
[[ -f "${xpuiBak}" ]] && { rm -f "${xpuiSpa}"; cp "${xpuiBak}" "${xpuiSpa}"; } { [[ ! -f "${xpuiBak}" ]] || atomic_copy "${xpuiBak}" "${xpuiSpa}"; } || {
echo -e "\n${red}Error:${clr} Failed to restore client backup. Exiting...\n" >&2
exit 1
}
printf "\xE2\x9C\x94\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x26\x20\x72\x65\x73\x74\x6F\x72\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n" printf "\xE2\x9C\x94\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x26\x20\x72\x65\x73\x74\x6F\x72\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
return return
} }
[[ (-f "${appBak}" || -f "${xpuiBak}") && -z "${forceSpotx+x}" ]] && { [[ (-f "${appBak}" || -f "${xpuiBak}") && -z "${forceSpotx+x}" ]] && {
rm -rf "${xpuiDir}" 2>/dev/null
xpuiSkip='true' xpuiSkip='true'
printf "\xE2\x9C\x94\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n" printf "\xE2\x9C\x94\x20\x44\x65\x74\x65\x63\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
echo -e "\n${yellow}Warning:${clr} SpotX-Bash has already been installed." >&2 echo -e "\n${yellow}Warning:${clr} SpotX-Bash has already been installed." >&2
echo -e "Use the '-f' flag to force SpotX-Bash to run.\n" >&2 echo -e "Use the '-f' flag to force SpotX-Bash to run.\n" >&2
return return
} }
cp "${xpuiSpa}" "${xpuiBak}" backup_spotx || {
cp "${appBinary}" "${appBak}" echo -e "\n${red}Error:${clr} Failed to create client backup. Exiting...\n" >&2
exit 1
}
printf "\xE2\x9C\x94\x20\x43\x72\x65\x61\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n" printf "\xE2\x9C\x94\x20\x43\x72\x65\x61\x74\x65\x64\x20\x62\x61\x63\x6B\x75\x70\n"
} }
@@ -788,7 +994,9 @@ snapshot_check() {
} }
xpui_open() { xpui_open() {
rm -rf "${xpuiDir}" 2>/dev/null
mkdir -p "${xpuiDir}" mkdir -p "${xpuiDir}"
xpuiTempCreated='true'
unzip -qq "${xpuiSpa}" -d "${xpuiDir}" || { unzip -qq "${xpuiSpa}" -d "${xpuiDir}" || {
rm -rf "${xpuiDir}" 2>/dev/null rm -rf "${xpuiDir}" 2>/dev/null
echo -e "\n${red}Error:${clr} Failed to unpack xpui.spa. Reinstall client. Exiting...\n" >&2 echo -e "\n${red}Error:${clr} Failed to unpack xpui.spa. Reinstall client. Exiting...\n" >&2
@@ -826,6 +1034,7 @@ xpui_open() {
run_core_start() { run_core_start() {
final_setup_check final_setup_check
check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}" check_write_permission "${appPath}" "${appBinary}" "${xpuiPath}" "${xpuiSpa}"
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && protected_stage_prepare
xpui_detect xpui_detect
[[ "${xpuiSkip}" ]] && { printf "\xE2\x9C\x94\x20\x46\x69\x6E\x69\x73\x68\x65\x64\n\n"; exit 0; } [[ "${xpuiSkip}" ]] && { printf "\xE2\x9C\x94\x20\x46\x69\x6E\x69\x73\x68\x65\x64\n\n"; exit 0; }
xpui_open xpui_open
@@ -882,14 +1091,38 @@ run_patches() {
} }
run_finish() { run_finish() {
local spaTemp
echo -e "\n//# SpotX was here" >> "${xpuiJs}" echo -e "\n//# SpotX was here" >> "${xpuiJs}"
rm -f "${xpuiSpa}" spaTempDir=$(mktemp -d "${xpuiPath}/.spotx-spa.XXXXXXXX") || {
(cd "${xpuiDir}" && zip -qq -r ../xpui.spa .) || { uninstall_spotx && \
echo -e "\n${red}Error:${clr} Failed to repackage client." >&2 echo -e "\n${red}Error:${clr} Failed to create temporary SPA directory. Original client restored.\n" >&2 || \
echo -e "Spotify is now in a broken state. Please reinstall client.\n" >&2 echo -e "\n${red}Error:${clr} Failed to create temporary SPA directory or restore client. Backups were preserved.\n" >&2
exit 1 exit 1
} }
spaTemp="${spaTempDir}/xpui.spa"
(cd "${xpuiDir}" && zip -qq -r "${spaTemp}" .) &&
unzip -tqq "${spaTemp}" &&
mv -f "${spaTemp}" "${xpuiSpa}" || {
rm -rf "${spaTempDir}" 2>/dev/null
uninstall_spotx && {
echo -e "\n${red}Error:${clr} Failed to repackage client." >&2
echo -e "Original client restored.\n" >&2
} || {
echo -e "\n${red}Error:${clr} Failed to repackage or restore client." >&2
echo -e "Backups were preserved.\n" >&2
}
exit 1
}
rm -rf "${spaTempDir}" 2>/dev/null
[[ ! -d "${spaTempDir}" ]] && unset spaTempDir
rm -rf "${xpuiDir}" rm -rf "${xpuiDir}"
unset xpuiTempCreated
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && protected_commit || {
[[ "${platformType}" == "Linux" && "${stagedInstall}" ]] && {
echo -e "\n${red}Error:${clr} Failed to install patched client. Original files restored.\n" >&2
exit 1
}
}
[[ "${platformType}" == "macOS" ]] && { [[ "${platformType}" == "macOS" ]] && {
[[ "${skipCodesign}" ]] && /usr/bin/xattr -cr "${appPath}" 2>/dev/null || { [[ "${skipCodesign}" ]] && /usr/bin/xattr -cr "${appPath}" 2>/dev/null || {
/usr/bin/xattr -cr "${appPath}" 2>/dev/null /usr/bin/xattr -cr "${appPath}" 2>/dev/null
@@ -1225,6 +1458,9 @@ premiumExpEx=(
'enableYourSoundCapsuleModal&showing a modal on desktop to users who have clicked on a Your Sound Capsule share link",default:\K!1&true&s&xpuiJs&1.2.38.720' 'enableYourSoundCapsuleModal&showing a modal on desktop to users who have clicked on a Your Sound Capsule share link",default:\K!1&true&s&xpuiJs&1.2.38.720'
) )
trap cleanup_temp_dirs EXIT
trap 'exit 130' HUP INT TERM
run_prepare run_prepare
run_uninstall_check run_uninstall_check
run_interactive_check run_interactive_check