Replace patchelf crack with Freeloader LD_PRELOAD approach
- Multi-stage Dockerfile: discover patterns from PMS binary (capstone), compile .so with zig (musl), layer onto lscr.io/linuxserver/plex - Uses LD_PRELOAD instead of patchelf (which corrupts Plex's musl loader) - Auto-discovery: broad structural patterns with string-anchored fallback (//feature) and relationship-based fallback (BITSET_REF within BS_INIT) - hook.cpp uses __has_include for generated patterns with hardcoded fallbacks - Custom wrapper.sh (no traffic_logger preload) - Vendored Freeloader source (github.com/authrequest/Freeloader, AGPL-3.0) - Removed stale plexmediaserver_crack.so binary - Supports Plex 1.43.3+ (verified against 1.43.2 and 1.43.3)
This commit is contained in:
1 parent
4399a8288d
commit
72f4661bdc
72 files changed
+77927
-17
No files matched your search
@@ -0,0 +1,76 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
# shellcheck shell=bash
|
||||
#
|
||||
# Shared helpers for the plex-tailnet scripts. SOURCE this file; do not run it.
|
||||
# Keeping the generic concerns (logging, dry-run execution, prompts, guards,
|
||||
# secret redaction) here removes duplication between the setup scripts and keeps
|
||||
# each script focused on its own orchestration.
|
||||
|
||||
# --- colour-aware logging (colours only on a TTY) ---------------------------
|
||||
_c() { [[ -t 1 ]] && printf '%s' "$1" || true; }
|
||||
log() { printf '%s[*]%s %s\n' "$(_c $'\033[1;34m')" "$(_c $'\033[0m')" "$*"; }
|
||||
ok() { printf '%s[+]%s %s\n' "$(_c $'\033[1;32m')" "$(_c $'\033[0m')" "$*"; }
|
||||
warn() { printf '%s[!]%s %s\n' "$(_c $'\033[1;33m')" "$(_c $'\033[0m')" "$*" >&2; }
|
||||
die() { printf '%s[x]%s %s\n' "$(_c $'\033[1;31m')" "$(_c $'\033[0m')" "$*" >&2; exit 1; }
|
||||
|
||||
# --- command execution that honours DRY_RUN ---------------------------------
|
||||
: "${DRY_RUN:=0}"
|
||||
run() {
|
||||
if [[ $DRY_RUN -eq 1 ]]; then
|
||||
printf ' +'; printf ' %q' "$@"; echo
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# --- fail fast with a located diagnostic ------------------------------------
|
||||
# Usage: enable_error_trap (after sourcing). Tolerated failures must be
|
||||
# guarded with `|| true` / `|| warn ...` as usual.
|
||||
__err_trap() { warn "aborted (exit $1) near line $2"; exit "$1"; }
|
||||
enable_error_trap() { trap '__err_trap "$?" "$LINENO"' ERR; }
|
||||
|
||||
# --- guards / predicates ----------------------------------------------------
|
||||
require_root() { [[ "${EUID:-$(id -u)}" -eq 0 ]] || die "must run as root (use sudo)"; }
|
||||
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"; }
|
||||
have_cmd() { command -v "$1" >/dev/null 2>&1; }
|
||||
is_port() { [[ "$1" =~ ^[0-9]+$ ]] && (( 10#$1 >= 1 && 10#$1 <= 65535 )); }
|
||||
|
||||
# --- interactive prompts (read the controlling terminal directly) -----------
|
||||
ask_yes_no() { # question [default Y|N] -> 0 = yes, 1 = no
|
||||
local q="$1" def="${2:-Y}" ans prompt
|
||||
[[ "$def" == "Y" ]] && prompt="[Y/n]" || prompt="[y/N]"
|
||||
read -r -p "$(printf '%s[?]%s %s %s ' "$(_c $'\033[1;36m')" "$(_c $'\033[0m')" "$q" "$prompt")" ans </dev/tty || ans=""
|
||||
ans="${ans:-$def}"
|
||||
[[ "$ans" =~ ^[Yy] ]]
|
||||
}
|
||||
|
||||
ask_choice() { # question default opt... -> echoes the chosen value (prompt on stderr)
|
||||
local q="$1" def="$2"; shift 2
|
||||
local opts=("$@") i ans o
|
||||
{
|
||||
printf '%s[?]%s %s\n' "$(_c $'\033[1;36m')" "$(_c $'\033[0m')" "$q"
|
||||
for i in "${!opts[@]}"; do
|
||||
printf ' %d) %s%s\n' "$((i + 1))" "${opts[$i]}" "$([[ ${opts[$i]} == "$def" ]] && echo ' (default)')"
|
||||
done
|
||||
printf ' choice [%s]: ' "$def"
|
||||
} >&2
|
||||
read -r ans </dev/tty || ans=""
|
||||
[[ -z "$ans" ]] && { printf '%s' "$def"; return; }
|
||||
if [[ "$ans" =~ ^[0-9]+$ ]] && (( ans >= 1 && ans <= ${#opts[@]} )); then
|
||||
printf '%s' "${opts[$((ans - 1))]}"; return
|
||||
fi
|
||||
for o in "${opts[@]}"; do [[ "$ans" == "$o" ]] && { printf '%s' "$o"; return; }; done
|
||||
printf '%s' "$def"
|
||||
}
|
||||
|
||||
# --- secret redaction for logging -------------------------------------------
|
||||
# redact_after FLAG ARG... -> echoes ARGs with the value following FLAG masked.
|
||||
redact_after() {
|
||||
local flag="$1"; shift
|
||||
local out=() mask=0 a
|
||||
for a in "$@"; do
|
||||
if [[ $mask -eq 1 ]]; then out+=("***"); mask=0
|
||||
else out+=("$a"); [[ "$a" == "$flag" ]] && mask=1; fi
|
||||
done
|
||||
printf '%s' "${out[*]}"
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env python3
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""Read and edit Plex ``Preferences.xml`` attributes.
|
||||
|
||||
Used by ``plex-tailscale-setup.sh``. The XML logic lives here -- not in a bash
|
||||
heredoc -- so it is cohesive, reviewable, and independently testable. The shell
|
||||
owns the lifecycle (stop Plex, back up, restore ownership, restart); this owns
|
||||
the document.
|
||||
|
||||
plex_prefs.py merge PREFS [--custom-url URL] [--lan CIDR[,CIDR...]]
|
||||
[--secure 0|1|2] [--relay 0|1]
|
||||
plex_prefs.py get PREFS ATTR
|
||||
|
||||
``merge`` is additive and idempotent: list attributes gain only missing values;
|
||||
scalar attributes are set only when a value is supplied. Unrelated attributes
|
||||
(tokens, machine identity, ...) are preserved.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from collections.abc import Callable
|
||||
from typing import Protocol, cast
|
||||
|
||||
try:
|
||||
import defusedxml.ElementTree as ET
|
||||
except ModuleNotFoundError:
|
||||
sys.exit("plex_prefs: missing dependency: install python3-defusedxml")
|
||||
|
||||
|
||||
class _PrefsElement(Protocol):
|
||||
tag: str
|
||||
|
||||
def get(self, key: str, default: str = "") -> str: ...
|
||||
def set(self, key: str, value: str) -> None: ...
|
||||
|
||||
|
||||
class _PrefsTree(Protocol):
|
||||
def getroot(self) -> _PrefsElement: ...
|
||||
def write(self, file_or_filename: str, encoding: str, xml_declaration: bool) -> None: ...
|
||||
|
||||
|
||||
def _load(path: str) -> tuple[_PrefsTree, _PrefsElement]:
|
||||
try:
|
||||
tree = cast(_PrefsTree, cast(object, ET.parse(path)))
|
||||
except (OSError, ET.ParseError) as exc:
|
||||
sys.exit(f"plex_prefs: cannot read {path}: {exc}")
|
||||
root = tree.getroot()
|
||||
if root.tag != "Preferences":
|
||||
sys.exit(f"plex_prefs: unexpected root <{root.tag}>; refusing to edit {path}")
|
||||
return tree, root
|
||||
|
||||
|
||||
def _merge_csv(root: _PrefsElement, attr: str, additions: list[str]) -> None:
|
||||
items = [x for x in (s.strip() for s in root.get(attr, "").split(",")) if x]
|
||||
for value in additions:
|
||||
if value and value not in items:
|
||||
items.append(value)
|
||||
root.set(attr, ",".join(items))
|
||||
|
||||
|
||||
def cmd_merge(args: argparse.Namespace) -> int:
|
||||
prefs = cast(str, args.prefs)
|
||||
custom_url = cast(str, args.custom_url)
|
||||
lan = cast(str, args.lan)
|
||||
secure = cast(str, args.secure)
|
||||
relay = cast(str, args.relay)
|
||||
|
||||
tree, root = _load(prefs)
|
||||
if custom_url:
|
||||
_merge_csv(root, "customConnections", [custom_url])
|
||||
if lan:
|
||||
_merge_csv(root, "LanNetworksBandwidth", [c for c in lan.split(",") if c])
|
||||
if secure in ("0", "1", "2"):
|
||||
root.set("secureConnections", secure)
|
||||
if relay in ("0", "1"):
|
||||
root.set("RelayEnabled", relay)
|
||||
tree.write(prefs, encoding="utf-8", xml_declaration=True)
|
||||
return 0
|
||||
|
||||
|
||||
def cmd_get(args: argparse.Namespace) -> int:
|
||||
prefs = cast(str, args.prefs)
|
||||
attr = cast(str, args.attr)
|
||||
|
||||
_, root = _load(prefs)
|
||||
print(root.get(attr, ""))
|
||||
return 0
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
parser = argparse.ArgumentParser(prog="plex_prefs", description=__doc__)
|
||||
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||
|
||||
m = sub.add_parser("merge", help="merge tailnet settings into Preferences.xml")
|
||||
_ = m.add_argument("prefs")
|
||||
_ = m.add_argument("--custom-url", default="")
|
||||
_ = m.add_argument("--lan", default="")
|
||||
_ = m.add_argument("--secure", default="", help="0=Required 1=Preferred 2=Disabled")
|
||||
_ = m.add_argument("--relay", default="", help="0=disable 1=enable Plex Relay")
|
||||
m.set_defaults(func=cmd_merge)
|
||||
|
||||
g = sub.add_parser("get", help="print one Preferences.xml attribute")
|
||||
_ = g.add_argument("prefs")
|
||||
_ = g.add_argument("attr")
|
||||
g.set_defaults(func=cmd_get)
|
||||
|
||||
args = parser.parse_args(argv)
|
||||
func = cast(Callable[[argparse.Namespace], int], args.func)
|
||||
return func(args)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in new issue
Block a user