#!/usr/bin/env bash # # LibrePortal bootstrap installer. # # curl -fsSL https://get.libreportal.org/install.sh | sudo bash # curl -fsSL https://get.libreportal.org/install.sh | sudo bash -s -- \ # --system-dir=/mnt/ssd/lp --backups-dir=/mnt/usb/lp-backups --manager-user=admin # # Fetches a versioned, checksum-verified release tarball over plain HTTPS (no git, # no auth), extracts it to the system root, and hands off to init.sh. Git/local # remain available for development. Self-contained on purpose — it's downloaded and # run on its own, before any LibrePortal code exists on the box. set -euo pipefail # --- defaults --------------------------------------------------------------- CHANNEL="stable" VERSION_PIN="" MODE="release" # release | local | git LOCAL_SRC="" GIT_URL="" ; GIT_USER="" ; GIT_TOKEN="" RELEASE_BASE_URL="${LP_RELEASE_BASE_URL:-https://get.libreportal.org}" PASSWORD="${LP_PASSWORD:-}" SYSTEM_DIR="" ; CONTAINERS_DIR="" ; BACKUPS_DIR="" ; MANAGER_USER="" ; ALLOW_HOME="" DRY_RUN=0 NO_VERIFY_SIG=0 # minisign public key. Keep the SECRET key offline. Once you run `minisign -G`, # paste the public key here AND into libreportal.pub. While it contains REPLACE_ME, # signature verification is skipped (the sha256 still runs); once replaced, a valid # signature becomes REQUIRED for release installs. LP_MINISIGN_PUBKEY="RWRBf8o+a1KSvF08fA3oKrVz/71D60YeF4GO66ntVeJvzAkI57sjgM1S" usage() { sed -n '3,12p' "$0" | sed 's/^# \{0,1\}//' cat <<'EOF' Options: --channel=stable|edge Release channel (default stable) --version=X.Y.Z Pin a specific version (else latest in channel) --system-dir=PATH Manager-owned control plane (default /libreportal-system) --containers-dir=PATH Live app data (default /libreportal-containers) --backups-dir=PATH Backup repos (default /libreportal-backups) --manager-user=NAME Control-plane user (default libreportal) --allow-home Permit containers/backups inside /home/ --password=PASS Manager/WebUI password (else a random one is generated) --base-url=URL Override the release host (also: LP_RELEASE_BASE_URL) --local=PATH Dev: install from a local source folder --git-url=URL Dev: install by cloning a git repo --git-user=U --git-token=T Dev: git credentials --dry-run Fetch + verify + stage, but don't install EOF } # --- parse flags ------------------------------------------------------------ for arg in "$@"; do case "$arg" in --channel=*) CHANNEL="${arg#*=}" ;; --version=*) VERSION_PIN="${arg#*=}" ;; --system-dir=*) SYSTEM_DIR="${arg#*=}" ;; --containers-dir=*) CONTAINERS_DIR="${arg#*=}" ;; --backups-dir=*) BACKUPS_DIR="${arg#*=}" ;; --manager-user=*) MANAGER_USER="${arg#*=}" ;; --allow-home) ALLOW_HOME="--allow-home" ;; --password=*) PASSWORD="${arg#*=}" ;; --base-url=*) RELEASE_BASE_URL="${arg#*=}" ;; --local=*) MODE="local"; LOCAL_SRC="${arg#*=}" ;; --git-url=*) MODE="git"; GIT_URL="${arg#*=}" ;; --git-user=*) GIT_USER="${arg#*=}" ;; --git-token=*) GIT_TOKEN="${arg#*=}" ;; --dry-run) DRY_RUN=1 ;; --no-verify-signature) NO_VERIFY_SIG=1 ;; -h|--help) usage; exit 0 ;; *) echo "install.sh: unknown option '$arg' (try --help)" >&2; exit 2 ;; esac done err() { echo "install.sh: $*" >&2; exit 1; } need() { command -v "$1" >/dev/null 2>&1 || err "missing required tool: $1"; } [[ $DRY_RUN -eq 1 || $EUID -eq 0 ]] || err "must run as root (try: sudo). Use --dry-run to test fetch only." # Resolve the system root (honour env; the default matches paths.sh). SYSTEM_DIR="${SYSTEM_DIR:-${LP_SYSTEM_DIR:-/libreportal-system}}" INSTALL_DIR="$SYSTEM_DIR/install" need tar if command -v curl >/dev/null 2>&1; then DL=curl elif command -v wget >/dev/null 2>&1; then DL=wget else err "need curl or wget"; fi SHACMD="" command -v sha256sum >/dev/null 2>&1 && SHACMD="sha256sum" [[ -z "$SHACMD" ]] && command -v shasum >/dev/null 2>&1 && SHACMD="shasum -a 256" [[ -n "$SHACMD" || "$MODE" != "release" ]] || err "need sha256sum or shasum to verify the release" fetch() { # fetch [outfile] (outfile omitted -> stdout) if [[ "$DL" == curl ]]; then [[ -n "${2:-}" ]] && curl -fsSL "$1" -o "$2" || curl -fsSL "$1" else [[ -n "${2:-}" ]] && wget -qO "$2" "$1" || wget -qO- "$1" fi } json_str() { printf '%s' "$1" | grep -oE "\"$2\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" | head -1 | sed -E 's/.*"([^"]*)"$/\1/'; } STAGE="$(mktemp -d)"; trap 'rm -rf "$STAGE"' EXIT SRC="" # --- obtain the code -------------------------------------------------------- case "$MODE" in release) local_ver="$VERSION_PIN" tarname="" want_sha="" if [[ -z "$local_ver" ]]; then echo "Resolving $CHANNEL channel from $RELEASE_BASE_URL ..." manifest="$(fetch "$RELEASE_BASE_URL/$CHANNEL/latest.json")" || err "cannot fetch channel manifest" local_ver="$(json_str "$manifest" version)" tarname="$(json_str "$manifest" url)" want_sha="$(json_str "$manifest" sha256)" fi [[ -n "$local_ver" ]] || err "could not determine version" [[ -n "$tarname" ]] || tarname="libreportal-${local_ver}.tar.gz" tarurl="$RELEASE_BASE_URL/$CHANNEL/$tarname" echo "Downloading $tarurl ..." fetch "$tarurl" "$STAGE/$tarname" || err "download failed" if [[ -z "$want_sha" ]]; then fetch "$tarurl.sha256" "$STAGE/$tarname.sha256" || err "no checksum available for $tarname" want_sha="$(cut -d' ' -f1 < "$STAGE/$tarname.sha256")" fi got_sha="$($SHACMD "$STAGE/$tarname" | cut -d' ' -f1)" [[ "$got_sha" == "$want_sha" ]] || err "CHECKSUM MISMATCH — refusing to install. expected $want_sha, got $got_sha" echo "✓ checksum verified ($got_sha)" # Signature: once the public key is real (not the REPLACE_ME placeholder), a # valid minisign signature is REQUIRED — it survives a compromised host (the # checksum alone doesn't, since the host serves it too). --no-verify-signature # is a dev escape hatch only. if [[ "$LP_MINISIGN_PUBKEY" != *REPLACE_ME* && "$NO_VERIFY_SIG" -ne 1 ]]; then command -v minisign >/dev/null 2>&1 || { echo "Installing minisign to verify the release ..."; apt-get install -y minisign >/dev/null 2>&1 || true; } command -v minisign >/dev/null 2>&1 || err "minisign required to verify the release but couldn't be installed" fetch "$tarurl.minisig" "$STAGE/$tarname.minisig" || err "release signature (.minisig) missing — refusing" # -P takes the public key as a string (our inline constant is just the # key line, not a full 2-line pubkey file). minisign -Vm "$STAGE/$tarname" -P "$LP_MINISIGN_PUBKEY" -x "$STAGE/$tarname.minisig" >/dev/null 2>&1 \ || err "SIGNATURE INVALID — refusing to install (the release isn't authentic)" echo "✓ signature verified" fi mkdir -p "$STAGE/x" tar xzf "$STAGE/$tarname" -C "$STAGE/x" --strip-components=1 SRC="$STAGE/x" ;; local) [[ -d "$LOCAL_SRC" ]] || err "local source folder not found: $LOCAL_SRC" SRC="$LOCAL_SRC" ;; git) need git [[ -n "$GIT_URL" ]] || err "--git-url is required for a git install" local_url="$GIT_URL" [[ -n "$GIT_USER" ]] && local_url="${GIT_URL%://*}://${GIT_USER}:${GIT_TOKEN}@${GIT_URL#*://}" git clone -q "$local_url" "$STAGE/x" || err "git clone failed" SRC="$STAGE/x" ;; esac [[ -f "$SRC/init.sh" && -d "$SRC/scripts" ]] || err "fetched tree is not a LibrePortal source (missing init.sh/scripts)" if [[ $DRY_RUN -eq 1 ]]; then echo "[dry-run] verified $MODE source at: $SRC" echo "[dry-run] would copy -> $INSTALL_DIR and run init.sh with:" echo " system=$SYSTEM_DIR containers=${CONTAINERS_DIR:-(default)} backups=${BACKUPS_DIR:-(default)} manager=${MANAGER_USER:-(default)} $ALLOW_HOME" exit 0 fi # --- place the code + hand off to init.sh ----------------------------------- [[ -n "$PASSWORD" ]] || { PASSWORD="$(head -c 18 /dev/urandom | base64 | tr -d '/+=' | cut -c1-20)"; GEN_PW=1; } mkdir -p "$INSTALL_DIR" cp -a "$SRC/." "$INSTALL_DIR/" cp -f "$INSTALL_DIR/init.sh" /root/ 2>/dev/null || true flags=( --unattended ) [[ -n "$SYSTEM_DIR" ]] && flags+=( "--system-dir=$SYSTEM_DIR" ) [[ -n "$CONTAINERS_DIR" ]] && flags+=( "--containers-dir=$CONTAINERS_DIR" ) [[ -n "$BACKUPS_DIR" ]] && flags+=( "--backups-dir=$BACKUPS_DIR" ) [[ -n "$MANAGER_USER" ]] && flags+=( "--manager-user=$MANAGER_USER" ) [[ -n "$ALLOW_HOME" ]] && flags+=( "$ALLOW_HOME" ) # Positional contract: init case "$MODE" in git) set -- init "$PASSWORD" "${GIT_USER:-empty}" "${GIT_TOKEN:-empty}" "$GIT_URL" true git ;; local) set -- init "$PASSWORD" empty empty empty true local ;; *) set -- init "$PASSWORD" empty empty empty true release ;; esac [[ "${GEN_PW:-0}" == 1 ]] && echo "Generated manager/WebUI password: $PASSWORD (also recorded in the install log)" echo "Running installer ..." cd "$INSTALL_DIR" # Tell init.sh the code is already staged + verified, so initGIT skips re-fetching. export LP_ALREADY_FETCHED=1 export LP_RELEASE_BASE_URL # carry the (possibly overridden) host through exec bash "$INSTALL_DIR/init.sh" "${flags[@]}" "$@"