diff --git a/README.md b/README.md index e672286..144f12a 100644 --- a/README.md +++ b/README.md @@ -1,139 +1,95 @@ # BackTunnel – Secure Reverse SSH Folder Sharing Toolkit -**BackTunnel** lets you share and mount folders between Linux machines behind NAT or firewalls using just two friendly commands. -No central servers. No cloud uploads. Temporary, peer-to-peer, SSH-based. +**BackTunnel** is a lightweight toolkit to share and mount folders between Linux machines behind NAT or firewalls using **reverse SSH tunnels**. +No third-party relay, no cloud dependency – just peer-to-peer, temporary, SSH-based access. --- -## ⚡ Quick Start (3 steps) - -1. **On the sharing machine (server):** - - ```bash - backtunnel-share ~/Documents with user@remotehost for 2h -i - ``` - - → Opens a tunnel for 2 hours and prints an **invite**. - -2. **Send the invite** (via chat, email, QR). It looks like: - - ```bash - backtunnel-auth-setup -p 2222 user@localhost - backtunnel-access '/home/alice/Documents' from user@remotehost -p 2222 -m "$HOME/remote-rssh" - ``` - -3. **On the accessing machine (client):** - - ```bash - mkdir -p ~/remote-rssh - backtunnel-auth-setup -p 2222 user@localhost - backtunnel-access '/home/alice/Documents' from user@remotehost -p 2222 -m ~/remote-rssh - ``` - - → Folder is mounted at `~/remote-rssh`. - -Unmount afterwards with: +## ⚡ Quick Start +### 1. Accessor: Prepare your key (one-time) ```bash -fusermount -u ~/remote-rssh +backtunnel-keys print ``` +Send the printed public key to the sharer. ---- - -## 🚀 Why BackTunnel? - -* **Works behind NAT/firewalls**: reverse SSH tunnels, no port forwarding needed. -* **Private by design**: no third-party relays or cloud. -* **Temporary by default**: shares auto-stop after chosen duration. -* **Simple invites**: send one-liners (or QR codes) to colleagues. -* **Safe**: restricted SFTP-only keys ensure no shell access. -* **Desktop integration**: right-click folders in Dolphin to share or access. - ---- - -## ✨ Commands - -### `backtunnel-share` — start a share - -*(sharing side)* - +### 2. Sharer: Start sharing and authorize the accessor ```bash -backtunnel-share /path/to/folder with user@remotehost for [options] +backtunnel-share ~/Documents with alice@remotehost for 2h -i --allow-key ./alice.pub ``` +This injects a **temporary, restricted SFTP-only key** and prints an invite. -**Duration formats:** `30m`, `2h`, `1d` -**Key options:** - -* `-p, --tunnel-port ` — Remote port (default: `2222`) -* `-l, --local-ssh-port ` — Local SSHD port (default: `22`) -* `-i, --invite` — Print a ready-to-copy **invite** -* `--invite-mount ` — Suggested mount point (default: `~/remote-rssh`) -* `--qr` — Show invite as QR code - ---- - -### `backtunnel-access` — mount a share - -*(accessing side)* - +### 3. Accessor: Mount the shared folder ```bash -backtunnel-access /path/to/folder from user@remotehost [options] +backtunnel-access '/home/sharer/Documents' from alice@remotehost -p 2222 -m ~/remote-rssh ``` - -**Options:** - -* `-p, --port ` — Remote tunnel port (default: `2222`) -* `-m, --mount-point ` — Local mount point (default: `~/remote-rssh`) - Unmount with: - ```bash fusermount -u ~/remote-rssh ``` --- +## ✨ Features + +- **NAT/firewall friendly**: works without port forwarding. +- **Temporary by design**: shares auto-expire after a set duration. +- **Invite workflow**: sharer sends a one-liner or QR code to accessor. +- **Restricted keys**: accessor keys are usable *only* for SFTP via the tunnel and are auto-removed when the share ends. +- **Profiles**: save defaults and common remotes in `profiles.ini`. +- **Desktop integration**: Dolphin (KDE) service menus for GUI sharing and access. + +--- + +## 🔑 Commands + +### Sharing +```bash +backtunnel-share /path/to/folder with user@host for [options] +``` +- `-p, --tunnel-port` Remote bind port (default: 2222) +- `-l, --local-ssh-port` Local sshd port to expose (default: 22) +- `-i, --invite` Print invite line +- `--qr` Show QR code (requires qrencode) +- `--allow-key FILE` Authorize a provided public key +- `--allow-known NAME` Authorize a stored key (~/.config/backtunnel/authorized/NAME.pub) + +### Accessing +```bash +backtunnel-access /path/to/folder from user@host [options] +``` +- `-p, --port` Tunnel port (default: 2222) +- `-m, --mount-point` Local mount point (default: ~/remote-rssh) + +--- + ## 📁 Profiles -Simplify frequent shares with `~/.config/backtunnel/profiles.ini`. -Example: - +`~/.config/backtunnel/profiles.ini`: ```ini [default] -tunnel_port=4422 -invite_mount=$HOME/shared +tunnel_port=2222 +invite=true +invite_mount=$HOME/remote-rssh [work] user=alice host=vps.example.com -tunnel_port=4423 +tunnel_port=4422 ``` - -Use with: - +Usage: ```bash -backtunnel-share ~/reports with @work for 6h -i +backtunnel-share ~/reports with @work for 6h -i --allow-known alice ``` --- -## 🔒 Security - -* **Restricted keys**: usable only through the tunnel (127.0.0.1). -* **SFTP-only**: enforced with `internal-sftp,restrict`. -* **Temporary**: ends when the tunnel closes. - ---- - ## 🖥️ Dolphin Integration -After install, Dolphin menus appear: - -* **Share via BackTunnel…** — GUI wizard for remote, duration, ports, invite. -* **Access via BackTunnel…** — GUI wizard for remote, port, mount point. - -Logs: +- **Share via BackTunnel…** – GUI dialog for sharer options. +- **Access via BackTunnel…** – GUI dialog for accessor options. +Logs are written to: ``` ~/.local/state/backtunnel/servicemenu.*.log ``` @@ -142,27 +98,27 @@ Logs: ## 📦 Install +### From source ```bash sudo bash scripts/install.sh make init # copy example profiles.ini ``` -Uninstall: - -```bash -sudo bash scripts/uninstall.sh -``` - -Arch Linux: - +### Arch Linux ```bash makepkg -si ``` +Uninstall: +```bash +sudo bash scripts/uninstall.sh +# or with purge of defaults +sudo PURGE=1 bash scripts/uninstall.sh +``` + --- -## 📖 Man Page - +## 📖 Documentation ```bash man backtunnel ``` @@ -170,39 +126,5 @@ man backtunnel --- ## 🧾 License - -Licensed under **GNU GPL v3.0** +GNU GPL v3.0 © 2025 LUXIM d.o.o., Slovenia – Matjaž Mozetič - ---- - -## 📝 Cheat Sheet (TL;DR) - -**Start a share:** - -```bash -backtunnel-share ~/Documents with user@remotehost for 2h -i -``` - -**Send this invite to your colleague:** - -```bash -backtunnel-auth-setup -p 2222 user@localhost -backtunnel-access '/home/user/Documents' from user@remotehost -p 2222 -m ~/remote-rssh -``` - -**On the client:** - -```bash -mkdir -p ~/remote-rssh -backtunnel-auth-setup -p 2222 user@localhost -backtunnel-access '/home/user/Documents' from user@remotehost -p 2222 -m ~/remote-rssh -``` - -**Unmount:** - -```bash -fusermount -u ~/remote-rssh -``` - ---- diff --git a/completions/backtunnel.bash b/completions/backtunnel.bash index d183905..2da2d05 100644 --- a/completions/backtunnel.bash +++ b/completions/backtunnel.bash @@ -1,53 +1,201 @@ #!/usr/bin/env bash +# BackTunnel bash completion for: +# - backtunnel-share +# - backtunnel-access +# +# Features: +# - Positional scaffolding: "with"/"from" / "for" and duration suggestions +# - @profile completion from profiles.ini (user, system, packaged) +# - --allow-key completes *.pub files +# - --allow-known completes names from ~/.config/backtunnel/authorized/*.pub +# - SSH host completion from known_hosts and ssh config Host entries +# - Path completion for first positional and --invite-mount / --mount-point +# - Option-aware value completion (-p/-l suggest typical ports) -# Copyright (c) 2025. LUXIM d.o.o., Slovenia - Matjaž Mozetič. +# ---------- helpers ---------- + +_bt_cfg_files() { + local u="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini" + local s="/etc/backtunnel/profiles.ini" + local p="/usr/share/backtunnel/profiles.ini" + [[ -f "$u" ]] && printf '%s\n' "$u" + [[ -f "$s" ]] && printf '%s\n' "$s" + [[ -f "$p" ]] && printf '%s\n' "$p" +} + +_bt_list_profiles() { + # Output section names excluding [default] + local f + while IFS= read -r f; do + awk ' + /^\[[^]]+\]/ { + name=$0; sub(/^\[/,"",name); sub(/\]$/,"",name); + if (name != "default") print name + }' "$f" + done < <(_bt_cfg_files) 2>/dev/null | sort -u +} + +_bt_list_authorized_names() { + # From ~/.config/backtunnel/authorized/*.pub → basename w/o .pub + local d="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/authorized" + [[ -d "$d" ]] || return 0 + local f + shopt -s nullglob + for f in "$d"/*.pub; do + f="${f##*/}"; printf '%s\n' "${f%.pub}" + done +} + +_bt_list_ssh_hosts() { + # Collect hosts from known_hosts and ~/.ssh/config Host entries (best effort) + local out=() + local kh="$HOME/.ssh/known_hosts" + local cfg="$HOME/.ssh/config" + local f + + if [[ -f "$kh" ]]; then + # First field may be comma-separated list; ignore IP-literals in brackets + # and strip hashed or bracketed entries. + awk -F'[ ,]' ' + { + if ($1 ~ /^\|1\|/) next; # hashed, skip + host=$1 + gsub(/^\[/,"",host); gsub(/\]$/,"",host) + if (host ~ /^[0-9.]+$/) next; # plain IPv4 + if (host ~ /^[0-9a-fA-F:]+$/) next; # plain IPv6 + if (host ~ /^\*/) next; # wildcard + print host + }' "$kh" + fi + + if [[ -f "$cfg" ]]; then + awk ' + tolower($1)=="host" { + for (i=2; i<=NF; i++) if ($i !~ /\*/ ) print $i + }' "$cfg" + fi +} + +_bt_is_backtunnel_share() { [[ ${COMP_WORDS[0]} == backtunnel-share ]]; } +_bt_is_backtunnel_access(){ [[ ${COMP_WORDS[0]} == backtunnel-access ]]; } + +# ---------- main completer ---------- _backtunnel_complete() { - local cur + local cur prev cur="${COMP_WORDS[COMP_CWORD]}" + prev="${COMP_WORDS[COMP_CWORD-1]}" + # Common positional flow: + # share: [0]cmd [1]/path [2]with [3]remote|@prof [4]for [5]duration ... + # access: [0]cmd [1]/path [2]from [3]remote ... + # + # Provide directory completion for the first positional: if [[ ${COMP_CWORD} -eq 1 ]]; then compopt -o filenames 2>/dev/null - mapfile -t COMPREPLY < <(compgen -d -- "${cur}") + mapfile -t COMPREPLY < <(compgen -d -- "$cur") return 0 fi + # Offer "with"/"from" at position 2 depending on command if [[ ${COMP_CWORD} -eq 2 ]]; then - mapfile -t COMPREPLY < <(compgen -W "with from" -- "${cur}") + if _bt_is_backtunnel_share; then + mapfile -t COMPREPLY < <(compgen -W "with" -- "$cur") + else + mapfile -t COMPREPLY < <(compgen -W "from" -- "$cur") + fi return 0 fi - if [[ ${COMP_CWORD} -eq 4 && "${COMP_WORDS[2]}" == "with" ]]; then - mapfile -t COMPREPLY < <(compgen -W "for" -- "${cur}") + # For backtunnel-share: position 3 (REMOTE | @profile) + if _bt_is_backtunnel_share && [[ ${COMP_CWORD} -eq 3 && ${COMP_WORDS[2]} == "with" ]]; then + # Build candidates: @profiles + hosts from ssh files + local profs hosts + profs=$( _bt_list_profiles 2>/dev/null | sed 's/^/@/' ) + hosts=$( _bt_list_ssh_hosts 2>/dev/null ) + mapfile -t COMPREPLY < <(compgen -W "$profs $hosts" -- "$cur") return 0 fi - if [[ ${COMP_CWORD} -eq 5 && "${COMP_WORDS[2]}" == "with" && "${COMP_WORDS[4]}" == "for" ]]; then - mapfile -t COMPREPLY < <(compgen -W "10m 30m 1h 2h 6h 12h 1d 2d" -- "${cur}") + # For backtunnel-share: position 4 expects "for" + if _bt_is_backtunnel_share && [[ ${COMP_CWORD} -eq 4 && ${COMP_WORDS[2]} == "with" ]]; then + mapfile -t COMPREPLY < <(compgen -W "for" -- "$cur") return 0 fi - if [[ "${COMP_WORDS[2]}" == "from" ]]; then - COMPREPLY=() + # For backtunnel-share: position 5 duration + if _bt_is_backtunnel_share && [[ ${COMP_CWORD} -eq 5 && ${COMP_WORDS[2]} == "with" && ${COMP_WORDS[4]} == "for" ]]; then + mapfile -t COMPREPLY < <(compgen -W "10m 30m 1h 2h 6h 12h 1d 2d" -- "$cur") return 0 fi - # backtunnel-share: after positionals, propose flags incl. invite - if [[ ${COMP_WORDS[0]} == backtunnel-share && ${COMP_CWORD} -ge 5 ]]; then - local opts="-p --tunnel-port -l --local-ssh-port -i --invite --invite-mount --invite-file --qr -h --help" - mapfile -t COMPREPLY < <(compgen -W "${opts}" -- "${cur}") + # For backtunnel-access: position 3 remote (after "from") + if _bt_is_backtunnel_access && [[ ${COMP_CWORD} -eq 3 && ${COMP_WORDS[2]} == "from" ]]; then + local hosts + hosts=$( _bt_list_ssh_hosts 2>/dev/null ) + mapfile -t COMPREPLY < <(compgen -W "$hosts" -- "$cur") return 0 fi - # backtunnel-access: after positionals, propose its flags - if [[ ${COMP_WORDS[0]} == backtunnel-access && ${COMP_CWORD} -ge 3 ]]; then + # ----- Option-value specific completions ----- + case "$prev" in + -p|--tunnel-port|--port) + mapfile -t COMPREPLY < <(compgen -W "2222 4422 5522 6622" -- "$cur") + return 0 + ;; + -l|--local-ssh-port) + mapfile -t COMPREPLY < <(compgen -W "22 2222" -- "$cur") + return 0 + ;; + --invite-mount|-m|--mount-point) + compopt -o filenames 2>/dev/null + mapfile -t COMPREPLY < <(compgen -d -- "$cur") + return 0 + ;; + --allow-key) + # complete .pub files + compopt -o filenames 2>/dev/null + # shellcheck disable=SC2207 + COMPREPLY=($(compgen -f -- "$cur")) + # filter to *.pub only (keep if user is typing a dir) + local i out=() + for i in "${COMPREPLY[@]}"; do + [[ -d "$i" || "$i" == *.pub ]] && out+=("$i") + done + COMPREPLY=("${out[@]}") + return 0 + ;; + --allow-known) + mapfile -t COMPREPLY < <(compgen -W "$(_bt_list_authorized_names)" -- "$cur") + return 0 + ;; + esac + + # ----- Generic options after positionals ----- + if _bt_is_backtunnel_share && [[ ${COMP_CWORD} -ge 5 ]]; then + local opts=" + -p --tunnel-port + -l --local-ssh-port + -i --invite + --invite-mount + --invite-file + --qr + --allow-key + --allow-known + -h --help" + mapfile -t COMPREPLY < <(compgen -W "$opts" -- "$cur") + return 0 + fi + + if _bt_is_backtunnel_access && [[ ${COMP_CWORD} -ge 3 ]]; then local opts="-p --port -m --mount-point -h --help" - mapfile -t COMPREPLY < <(compgen -W "${opts}" -- "${cur}") + mapfile -t COMPREPLY < <(compgen -W "$opts" -- "$cur") return 0 fi COMPREPLY=() } +# Register for both commands complete -F _backtunnel_complete backtunnel-share complete -F _backtunnel_complete backtunnel-access diff --git a/man/backtunnel.1 b/man/backtunnel.1 index 9b7991c..06ffb7c 100644 --- a/man/backtunnel.1 +++ b/man/backtunnel.1 @@ -1,7 +1,7 @@ -.TH backtunnel 1 "September 2025" "1.2" "BackTunnel – Reverse SSH Sharing Toolkit" +.TH backtunnel 1 "September 2025" "1.3" "BackTunnel – Reverse SSH Sharing Toolkit" .SH NAME -backtunnel-share, backtunnel-access \- Secure reverse SSH folder sharing and access (with profiles) +backtunnel-share, backtunnel-access \- Secure reverse SSH folder sharing and access (with profiles, temporary key authorization) .SH SYNOPSIS .B backtunnel-share @@ -14,151 +14,56 @@ backtunnel-share, backtunnel-access \- Secure reverse SSH folder sharing and acc \fBbacktunnel-share\fR starts a reverse SSH tunnel from the local (sharing) machine to a remote, reachable host. The tunnel exposes the local sshd (typically port 22) on a loopback port on the remote host using \fBssh -R\fR. Sharing ends automatically after the given \fIduration\fR via \fBtimeout\fR. -Profiles can be used to simplify the remote specification and default options. \fBbacktunnel-access\fR mounts the shared folder from the remote side using \fBsshfs\fR by connecting to \fBlocalhost:\fR on the remote host (the port exposed by \fBbacktunnel-share\fR). +The tools support \fIprofiles\fR (named remotes and defaults) and a secure workflow without +password exchange using temporary, restricted keys. + .SH OPTIONS .SS backtunnel-share options .TP .B -p, --tunnel-port -Remote port to bind with \fB-R\fR (default: 2222). May be provided via profile. - +Remote port to bind with \fB-R\fR (default: 2222). .TP .B -l, --local-ssh-port -Local sshd port to forward to (default: 22). May be provided via profile. - +Local sshd port to forward to (default: 22). .TP .B -i, --invite -Print a ready-to-copy access command for the remote side. - +Print a ready-to-copy access command for the remote side. With \fB--allow-key\fR +or \fB--allow-known\fR, the invite includes only the mount command (no auth step). .TP .B --invite-mount -Suggested mount point included in the invite text (default: $HOME/remote-rssh). May be provided via profile. - +Mount point suggested in invite (default: \fI$HOME/remote-rssh\fR). .TP .B --invite-file -Write the invite text (including unmount hint) to the given file. - +Also write the invite text (with unmount hint) to FILE. .TP .B --qr -Additionally render the invite as a QR code (requires \fBqrencode\fR). - +Also print a QR code (requires \fBqrencode\fR). .TP -.B -h, --help -Show usage. +.B --allow-key +Temporarily authorize the accessor’s public key for the session by injecting a restricted +entry into \fI~/.ssh/authorized_keys\fR and removing it on exit. The line is prefixed with: +\fCfrom="127.0.0.1",command="internal-sftp",restrict\fR. +.TP +.B --allow-known +Authorize a public key from \fI~/.config/backtunnel/authorized/NAME.pub\fR (same restrictions +and auto-removal as \fB--allow-key\fR). .SS backtunnel-access options .TP .B -p, --port Remote port on which the reverse tunnel listens (default: 2222). - .TP .B -m, --mount-point -Local mount point for sshfs (default: $HOME/remote-rssh). - -.SH ARGUMENTS -.TP -.B /path/to/folder -Path to share (server) or to mount (client). - -.TP -.B remoteuser:remotehost -Or \fBremoteuser@remotehost\fR. The remote host that accepts the initial SSH connection. - -.TP -.B @profilename -A named profile that expands to \fBuser@host\fR and may also supply defaults for \fB--tunnel-port\fR, -\fB--local-ssh-port\fR, \fB--invite-mount\fR, and \fIduration\fR. See \fBPROFILES\fR. - -.TP -.B -Time to keep the share active. Accepts \fBn\fR\fIs\fR|\fIm\fR|\fIh\fR|\fId\fR forms, e.g. 30m, 2h, 1d (passed to \fBtimeout\fR). -May be provided via the \fB[default]\fR profile; the positional \fI\fR takes precedence. +Local mount point for sshfs (default: \fI$HOME/remote-rssh\fR). .SH PROFILES -BackTunnel can read defaults and named remote definitions from (searched in order): -.P -\fB~/.config/backtunnel/profiles.ini\fR (per-user) -.br -\fB/etc/backtunnel/profiles.ini\fR (system-wide default) -.br -\fB/usr/share/backtunnel/profiles.ini\fR (packaged example/fallback) -.P -Two kinds of sections are recognized: +Profiles allow you to use \fB@name\fR instead of \fIuser@host\fR and to set defaults. -.TP -.B [default] -Global defaults applied unless overridden by a named profile or CLI flags: -.IP -\fBduration\fR=2h -.br -\fBtunnel_port\fR=2222 -.br -\fBlocal_ssh_port\fR=22 -.br -\fBinvite_mount\fR=~/remote-rssh -.br -\fBinvite\fR=true|false -.br -\fBqr\fR=true|false - -.TP -.B [name] -A named profile providing at least \fBuser\fR and \fBhost\fR, and optional overrides: -.IP -\fBuser\fR=alice -.br -\fBhost\fR=vps.example.com -.br -\fBtunnel_port\fR=4422 -.br -\fBlocal_ssh_port\fR=22 -.br -\fBinvite_mount\fR=$HOME/remote-rssh - -.P -To use a profile, replace the remote with \fB@name\fR, e.g.: -.P -.nf -backtunnel-share /path/to/folder with @workvps for 1d -.fi -.P -Command-line options \fBalways override\fR values from profiles. - -.SH EXAMPLES -.TP -Share for 2 hours on default ports: -.B backtunnel-share -/home/user/docs with alice@vps.example.com for 2h - -.TP -Share for 1 day, using custom ports: -.B backtunnel-share -/home/user/docs with alice:vps.example.com for 1d -p 4422 -l 2222 - -.TP -Share using a profile, then override port: -.B backtunnel-share -/home/user/docs with @workvps for 6h -p 5500 - -.TP -Mount with default port and mount point: -.B backtunnel-access -/home/user/docs from alice@vps.example.com - -.TP -Mount with custom port and mount point: -.B backtunnel-access -/home/user/docs from alice@vps.example.com -p 4422 -m /mnt/alice-docs - -.SH NOTES -By default, \fBssh -R\fR binds to 127.0.0.1 on the remote side, limiting access to local users on the remote machine. -The client connects to \fBlocalhost:\fR from the remote host. -Ensure the sharing host's sshd provides a valid SFTP subsystem for sshfs. - -.SH FILES +The configuration search order is: .TP \fB~/.config/backtunnel/profiles.ini\fR Per-user configuration file containing defaults and named profiles. @@ -169,8 +74,63 @@ System-wide default profiles (editable by admins). \fB/usr/share/backtunnel/profiles.ini\fR Packaged example/fallback used when user/system config is absent. +An example: +.PP +.nf +[default] +tunnel_port=2222 +invite=true +invite_mount=$HOME/remote-rssh + +[work] +user=alice +host=vps.example.com +tunnel_port=4422 +.fi + +Usage: +.PP +\fBbacktunnel-share ~/docs with @work for 6h -i --allow-known alice\fR + +.SH SECURITY MODEL +The temporary authorized key is restricted with: +.PP +\fCfrom="127.0.0.1",command="internal-sftp",restrict\fR +.PP +This limits usage to the reverse tunnel (localhost on the remote end) and SFTP only (no shell, +no agent/X11/port forwarding). The entry is removed when the share ends or the process exits. + +.SH EXAMPLES +.TP +Share for 2 hours on default ports: +\fBbacktunnel-share\fR /home/user/docs with alice@vps.example.com for 2h -i +.TP +Share for 1 day, using custom ports and a known key: +\fBbacktunnel-share\fR /home/user/docs with @work for 1d -p 4422 -l 2222 -i --allow-known alice +.TP +Mount with default port and mount point: +\fBbacktunnel-access\fR /home/user/docs from alice@vps.example.com +.TP +Mount with custom port and mount point: +\fBbacktunnel-access\fR /home/user/docs from alice@vps.example.com -p 4422 -m /mnt/alice-docs + +.SH FILES +.TP +\fB~/.config/backtunnel/profiles.ini\fR +Per-user profiles and defaults. +.TP +\fB/etc/backtunnel/profiles.ini\fR +System-wide defaults. +.TP +\fB/usr/share/backtunnel/profiles.ini\fR +Packaged example. +.TP +\fB~/.config/backtunnel/authorized/*.pub\fR +Accessor public keys saved by \fBbacktunnel-authorize\fR for \fB--allow-known\fR. + .SH SEE ALSO -ssh(1), sshfs(1), timeout(1), autossh(1), fusermount(1) +ssh(1), sshfs(1), timeout(1), autossh(1), fusermount(1), +\fBbacktunnel-keys\fR(1), \fBbacktunnel-auth-setup\fR(1) .SH AUTHOR Matjaž Mozetič diff --git a/scripts/backtunnel-authorize b/scripts/backtunnel-authorize new file mode 100644 index 0000000..002b6e4 --- /dev/null +++ b/scripts/backtunnel-authorize @@ -0,0 +1,9 @@ +#!/usr/bin/env bash +set -euo pipefail +name="${1:-}" +file="${2:-}" +[[ -n "$name" && -n "$file" && -f "$file" ]] || { echo "Usage: backtunnel-authorize "; exit 1; } +dir="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/authorized" +mkdir -p "$dir" +install -m 644 "$file" "$dir/$name.pub" +echo "Saved: $dir/$name.pub" diff --git a/scripts/backtunnel-keys b/scripts/backtunnel-keys new file mode 100644 index 0000000..9d7eb66 --- /dev/null +++ b/scripts/backtunnel-keys @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# backtunnel-keys: manage accessor-side keys +# Usage: +# backtunnel-keys print # print (and generate if missing) the public key +# backtunnel-keys path # print the private/public key paths + +set -euo pipefail + +KEY="$HOME/.ssh/id_ed25519_backtunnel" +PUB="$KEY.pub" + +cmd="${1:-print}" + +case "$cmd" in + print) + if [[ ! -f "$KEY" ]]; then + ssh-keygen -t ed25519 -f "$KEY" -N "" -C "backtunnel" >/dev/null + fi + if [[ ! -f "$PUB" ]]; then + echo "Missing public key $PUB" >&2; exit 1 + fi + cat "$PUB" + ;; + path) + echo "private: $KEY" + echo "public : $PUB" + ;; + *) + echo "Usage: $0 {print|path}" >&2 + exit 1 + ;; +esac diff --git a/scripts/backtunnel-share b/scripts/backtunnel-share index 0dce337..dffe4d7 100644 --- a/scripts/backtunnel-share +++ b/scripts/backtunnel-share @@ -1,21 +1,34 @@ #!/usr/bin/env bash - # Copyright (c) 2025. LUXIM d.o.o., Slovenia - Matjaž Mozetič. # Licensed under the GNU GPL v3.0 - +# # backtunnel-share: Share a folder using reverse SSH for a limited duration -# Syntax: backtunnel-share /path/to/folder with remoteuser:remotehost for 2h -# Options: -p|--tunnel-port -l|--local-ssh-port -# -i|--invite [--invite-mount ] [--invite-file ] [--qr] -h|--help +# Syntax: +# backtunnel-share /path/to/folder with remoteuser:remotehost for 2h [options] +# +# Options: +# -p|--tunnel-port Remote port to expose with -R (default: 2222) +# -l|--local-ssh-port Local sshd port to forward to (default: 22) +# -i|--invite Print an invite command (and/or QR) for chat +# --invite-mount Suggested mount point in invite (default: $HOME/remote-rssh) +# --invite-file Also write invite text (with unmount hint) to FILE +# --qr Also render the invite command as a QR code (qrencode) +# --allow-key Temporarily authorize accessor's public key for this session +# --allow-known Temporarily authorize ~/.config/backtunnel/authorized/NAME.pub +# -h|--help Show help +# +# Profiles: ~/.config/backtunnel/profiles.ini (overrides /etc/backtunnel/profiles.ini then /usr/share/backtunnel/profiles.ini) +# [default] tunnel_port=2222 local_ssh_port=22 invite_mount=$HOME/remote-rssh invite=true qr=false +# [alice] user=alice host=vps.example.com tunnel_port=4422 set -euo pipefail -# Config search order: user → system → packaged example +# ---------------------------- +# Config discovery +# ---------------------------- CONFIG_USER="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/profiles.ini" CONFIG_SYS="/etc/backtunnel/profiles.ini" CONFIG_PKG="/usr/share/backtunnel/profiles.ini" - -# resolve CONFIG_FILE to first existing if [[ -f "$CONFIG_USER" ]]; then CONFIG_FILE="$CONFIG_USER" elif [[ -f "$CONFIG_SYS" ]]; then @@ -24,7 +37,10 @@ else CONFIG_FILE="$CONFIG_PKG" fi -# shellcheck disable=SC2317 # invoked later at runtime; ShellCheck can't see call path +# ---------------------------- +# INI helpers +# ---------------------------- +# shellcheck disable=SC2317 ini_get() { # ini_get SECTION KEY -> value local sec="$1" key="$2" awk -v s="[""$sec""]" -v k="$key" ' @@ -78,15 +94,23 @@ profile_apply_defaults() { # set globals if unset; named overrides default fi } +# ---------------------------- +# Defaults +# ---------------------------- +TUNNEL_PORT=2222 # remote-side port exposed via -R +LOCAL_SSH_PORT=22 # local sshd port to forward to +DURATION="" # required: e.g. 30m, 2h, 1d -TUNNEL_PORT=2222 # remote-side port exposed via -R -LOCAL_SSH_PORT=22 # local sshd port to forward to -DURATION="" # required: e.g. 30m, 2h, 1d - -INVITE=false # print a ready-to-copy access command +INVITE=false # print a ready-to-copy access command INVITE_MOUNT="$HOME/remote-rssh" INVITE_FILE="" -QR=false # render invite as terminal QR (requires qrencode) +QR=false # render invite as terminal QR (requires qrencode) + +# Accessor authorization (session-scoped) +ALLOW_KEY_FILE="" +ALLOW_KNOWN_NAME="" +AUTHORIZED_STORE="${XDG_CONFIG_HOME:-$HOME/.config}/backtunnel/authorized" +ADDED_MARKER_ID="" # unique ID for cleanup removal usage() { cat >&2 < e.g. 30m, 2h, 1d (passed to 'timeout') @@ -107,17 +131,23 @@ Options: --invite-mount PATH Mount point suggested in invite (default: ${INVITE_MOUNT}) --invite-file FILE Also write the invite text (with unmount hint) to FILE --qr Also print a QR code (requires 'qrencode') + + --allow-key FILE Authorize this public key for this session (restricted SFTP-only, tunnel-only) + --allow-known NAME Authorize ~/.config/backtunnel/authorized/NAME.pub + -h, --help Show this help Examples: $(basename "$0") ~/projects with alice:vps.example.com for 2h $(basename "$0") ~/projects with alice@vps.example.com for 1d -p 4422 -l 2222 - $(basename "$0") ~/projects with alice@vps.example.com for 2h -i --qr + $(basename "$0") ~/projects with @work for 2h -i --qr --allow-known alice EOF exit 1 } -# --- basic positional parsing --- +# ---------------------------- +# Positional parsing +# ---------------------------- [[ $# -lt 5 ]] && usage FOLDER=$1 @@ -139,7 +169,9 @@ profile_apply_defaults "$profile_name" # Expand @name -> user@host for actual ssh use REMOTE="$(profile_expand_remote "$REMOTE")" -# --- optional flags --- +# ---------------------------- +# Optional flags +# ---------------------------- while [[ $# -gt 0 ]]; do case "$1" in -p|--tunnel-port) @@ -170,6 +202,16 @@ while [[ $# -gt 0 ]]; do QR=true shift ;; + --allow-key) + [[ $# -lt 2 ]] && usage + ALLOW_KEY_FILE=$2 + shift 2 + ;; + --allow-known) + [[ $# -lt 2 ]] && usage + ALLOW_KNOWN_NAME=$2 + shift 2 + ;; -h|--help) usage ;; @@ -180,13 +222,17 @@ while [[ $# -gt 0 ]]; do esac done -# --- validate duration (timeout supports s,m,h,d) --- +# ---------------------------- +# Duration validation +# ---------------------------- if [[ ! "$DURATION" =~ ^[0-9]+[smhd]$ ]]; then echo "Invalid duration '$DURATION' (use forms like 30m, 2h, 1d)." >&2 exit 1 fi -# --- split remote user/host --- +# ---------------------------- +# Split remote user/host +# ---------------------------- REMOTE_USER="" REMOTE_HOST="" if [[ "$REMOTE" == *:* ]]; then REMOTE_USER=${REMOTE%%:*} @@ -195,30 +241,120 @@ elif [[ "$REMOTE" == *"@"* ]]; then REMOTE_USER=${REMOTE%%@*} REMOTE_HOST=${REMOTE#*@} else - echo "Invalid remote format. Use remoteuser:remotehost or remoteuser@remotehost" >&2 + echo "Invalid remote format. Use remoteuser:remotehost or remoteuser@remotehost or @profile" >&2 exit 1 fi -# --- deps check --- +# ---------------------------- +# Deps check +# ---------------------------- command -v ssh >/dev/null 2>&1 || { echo "ssh not found."; exit 1; } command -v timeout >/dev/null 2>&1 || { echo "timeout not found."; exit 1; } -command -v tail >/dev/null 2>&1 || { echo "tail not found."; exit 1; } # used for --pid wait +command -v tail >/dev/null 2>&1 || { echo "tail not found."; exit 1; } +# ---------------------------- +# Accessor temporary authorization +# ---------------------------- +restrict_key_line() { # usage: restrict_key_line + local pk="$1" + # Normalize + pk="${pk//$'\r'/}" + pk="${pk//$'\n'/}" + if [[ "$pk" != ssh-* ]]; then + echo "Provided key does not look like an OpenSSH public key." >&2 + return 1 + fi + printf 'from="127.0.0.1",command="internal-sftp",restrict %s' "$pk" +} + +add_temp_authorized_key() { + local pubkey_text="$1" + local ak="$HOME/.ssh/authorized_keys" + # SC2155: Declare and assign separately to avoid masking return values. + # shellcheck disable=SC2155 + local marker + marker="BACKTUNNEL-TEMP-$(date +%s)-$$-$RANDOM" + local start="# ${marker} START" + local end="# ${marker} END" + + mkdir -p "$HOME/.ssh" + touch "$ak" + chmod 700 "$HOME/.ssh" + chmod 600 "$ak" + + local restricted + restricted="$(restrict_key_line "$pubkey_text")" || return 1 + + { + echo "$start" + echo "$restricted" + echo "$end" + } >> "$ak" + + ADDED_MARKER_ID="$marker" +} + +# shellcheck disable=SC2317 +remove_temp_authorized_key() { + local ak="$HOME/.ssh/authorized_keys" + [[ -z "${ADDED_MARKER_ID:-}" ]] && return 0 + [[ -f "$ak" ]] || return 0 + awk -v m="$ADDED_MARKER_ID" ' + $0 ~ "# " m " START" {skip=1; next} + $0 ~ "# " m " END" {skip=0; next} + !skip {print} + ' "$ak" > "${ak}.btnew" && mv "${ak}.btnew" "$ak" +} + +ACCESSOR_PUBKEY_TEXT="" + +if [[ -n "$ALLOW_KNOWN_NAME" ]]; then + f="${AUTHORIZED_STORE}/${ALLOW_KNOWN_NAME}.pub" + if [[ -f "$f" ]]; then + ACCESSOR_PUBKEY_TEXT="$(cat "$f")" + else + echo "Known accessor key not found: $f" >&2 + exit 1 + fi +fi + +if [[ -n "$ALLOW_KEY_FILE" ]]; then + if [[ -f "$ALLOW_KEY_FILE" ]]; then + ACCESSOR_PUBKEY_TEXT="$(cat "$ALLOW_KEY_FILE")" + else + echo "Given key file not found: $ALLOW_KEY_FILE" >&2 + exit 1 + fi +fi + +if [[ -n "$ACCESSOR_PUBKEY_TEXT" ]]; then + add_temp_authorized_key "$ACCESSOR_PUBKEY_TEXT" +fi + +# ---------------------------- +# Banner +# ---------------------------- echo "⏳ Sharing '${FOLDER}' via reverse SSH:" echo " local sshd port : ${LOCAL_SSH_PORT}" echo " remote bind port : ${TUNNEL_PORT} (on ${REMOTE_HOST})" echo " remote user : ${REMOTE_USER}" echo " duration : ${DURATION}" +if [[ -n "$ACCESSOR_PUBKEY_TEXT" ]]; then + echo " accessor key : temporarily authorized (restricted)" +fi echo -# --- print invite (optional) --- +# ---------------------------- +# Invite (optional) +# If accessor key was pre-authorized, we can omit the auth-setup line. +# ---------------------------- if $INVITE; then INVITE_CMD="backtunnel-access '${FOLDER}' from ${REMOTE_USER}@${REMOTE_HOST} -p ${TUNNEL_PORT} -m '${INVITE_MOUNT}'" - # Also provide a one-time auth bootstrap that installs a restricted, tunnel-only SFTP key - AUTH_CMD="backtunnel-auth-setup -p ${TUNNEL_PORT} ${REMOTE_USER}@localhost" - INVITE_TEXT=$( - cat </dev/null 2>&1 && nc -z 127.0.0.1 ${TUNNEL_PORT}"; then echo "⚠️ Port ${TUNNEL_PORT} on remote 127.0.0.1 appears in use; choose another with -p." >&2 - # Uncomment the next line to make it a hard failure: - # exit 1 + # You may 'exit 1' here if you prefer a hard failure fi -# --- run ssh in background; trap Ctrl-C/TERM/EXIT to stop it cleanly --- +# ---------------------------- +# Cleanup & run SSH +# ---------------------------- SSH_PID="" -# shellcheck disable=SC2317 # cleanup is invoked indirectly via trap +# shellcheck disable=SC2317 cleanup() { + # stop ssh child if running if [[ -n "${SSH_PID:-}" ]] && kill -0 "$SSH_PID" 2>/dev/null; then echo "⏹️ Stopping share..." kill -TERM "$SSH_PID" 2>/dev/null || true - # give ssh a moment to exit gracefully wait "$SSH_PID" 2>/dev/null || true fi + # remove temporary authorized key if we added one + remove_temp_authorized_key } trap cleanup INT TERM EXIT diff --git a/scripts/install.sh b/scripts/install.sh index 3a4006a..a12f627 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# BackTunnel installer (terminal/TUI service menus) +# BackTunnel installer (scripts, man page, completions, Dolphin service menus) set -euo pipefail PREFIX=${PREFIX:-/usr} @@ -12,60 +12,89 @@ KSVC5="$DESTDIR$PREFIX/share/kservices5/ServiceMenus" APPDIR="$DESTDIR$PREFIX/share/applications" BCOMP="$DESTDIR$PREFIX/share/bash-completion/completions" SHARE_DIR="$DESTDIR$PREFIX/share/backtunnel" +ETC_DIR="$DESTDIR/etc/backtunnel" -say() { printf '[BackTunnel] %s\n' "$*"; } +say() { printf '[BackTunnel] %s\n' "$@"; } -say "Installing to PREFIX=${PREFIX} DESTDIR=${DESTDIR}" +# --- Create dirs --- +install -d -m 755 "$BINDIR" "$MANDIR" "$KIO_SM" "$KSVC5" "$BCOMP" "$SHARE_DIR" "$APPDIR" +install -d -m 755 "$ETC_DIR" -# --- Binaries (CLI) --- -install -Dm755 "scripts/backtunnel-share" "$BINDIR/backtunnel-share" -install -Dm755 "scripts/backtunnel-access" "$BINDIR/backtunnel-access" -install -Dm755 "scripts/backtunnel-auth-setup" "$BINDIR/backtunnel-auth-setup" - -# --- GUI wrappers (optional) --- -install -Dm755 "scripts/backtunnel-share-gui" "$BINDIR/backtunnel-share-gui" -install -Dm755 "scripts/backtunnel-access-gui" "$BINDIR/backtunnel-access-gui" - -# --- Terminal opener + TUIs (used by service menus) --- -install -Dm755 "scripts/backtunnel-open-term" "$BINDIR/backtunnel-open-term" -install -Dm755 "scripts/backtunnel-share-tui" "$BINDIR/backtunnel-share-tui" -install -Dm755 "scripts/backtunnel-access-tui" "$BINDIR/backtunnel-access-tui" +# --- Install binaries --- +for f in \ + scripts/backtunnel-share \ + scripts/backtunnel-access \ + scripts/backtunnel-share-gui \ + scripts/backtunnel-access-gui \ + scripts/backtunnel-open-term \ + scripts/backtunnel-share-tui \ + scripts/backtunnel-access-tui \ + scripts/backtunnel-keys \ + scripts/backtunnel-authorize \ + scripts/backtunnel-auth-setup +do + if [[ -f "$f" ]]; then + install -m 755 "$f" "$BINDIR/$(basename "$f")" + say "Installed $(basename "$f")" + fi +done # --- Man page --- -install -Dm644 "man/backtunnel.1" "$MANDIR/backtunnel.1" +if [[ -f man/backtunnel.1 ]]; then + install -m 644 man/backtunnel.1 "$MANDIR/backtunnel.1" + say "Installed man page" +fi -# --- Bash completions (install for both command names) --- -install -Dm644 "completions/backtunnel.bash" "$BCOMP/backtunnel-share" -install -Dm644 "completions/backtunnel.bash" "$BCOMP/backtunnel-access" +# --- Bash completion (same file serves both commands) --- +if [[ -f completions/backtunnel.bash ]]; then + install -m 644 completions/backtunnel.bash "$BCOMP/backtunnel-share" + install -m 644 completions/backtunnel.bash "$BCOMP/backtunnel-access" + say "Installed bash completions" +fi -# --- Dolphin service menus (Plasma 6) --- -install -Dm644 "servicemenus/backtunnel_share.desktop" "$KIO_SM/backtunnel_share.desktop" -install -Dm644 "servicemenus/backtunnel_access.desktop" "$KIO_SM/backtunnel_access.desktop" - -# --- Plasma 5 legacy path (harmless if unused) --- -install -Dm644 "servicemenus/backtunnel_share.desktop" "$KSVC5/backtunnel_share.desktop" -install -Dm644 "servicemenus/backtunnel_access.desktop" "$KSVC5/backtunnel_access.desktop" +# --- Dolphin service menus (Plasma 6 primary, Plasma 5 fallback) --- +if [[ -f servicemenus/backtunnel_share.desktop ]]; then + install -m 644 servicemenus/backtunnel_share.desktop "$KIO_SM/backtunnel_share.desktop" + install -m 644 servicemenus/backtunnel_share.desktop "$KSVC5/backtunnel_share.desktop" + say "Installed Dolphin: Share" +fi +if [[ -f servicemenus/backtunnel_access.desktop ]]; then + install -m 644 servicemenus/backtunnel_access.desktop "$KIO_SM/backtunnel_access.desktop" + install -m 644 servicemenus/backtunnel_access.desktop "$KSVC5/backtunnel_access.desktop" + say "Installed Dolphin: Access" +fi # --- Optional desktop launcher --- -if [[ -f "desktop/backtunnel.desktop" ]]; then - install -Dm644 "desktop/backtunnel.desktop" "$APPDIR/backtunnel.desktop" +if [[ -f desktop/backtunnel.desktop ]]; then + install -m 644 desktop/backtunnel.desktop "$APPDIR/backtunnel.desktop" + say "Installed desktop launcher" fi -# --- Example profiles (system default + packaged fallback) --- -install -Dm644 "docs/profiles.ini.example" "$DESTDIR/etc/backtunnel/profiles.ini" -install -Dm644 "docs/profiles.ini.example" "$SHARE_DIR/profiles.ini" - -# --- Refresh desktop/KDE cache (best-effort) --- -if command -v update-desktop-database >/dev/null 2>&1; then - say "Refreshing desktop database..." - update-desktop-database -q || true +# --- Profiles defaults --- +if [[ -f docs/profiles.ini.example ]]; then + install -m 644 docs/profiles.ini.example "$SHARE_DIR/profiles.ini" + # install system default if not present (don’t overwrite local admin config) + if [[ ! -f "$ETC_DIR/profiles.ini" ]]; then + install -m 644 docs/profiles.ini.example "$ETC_DIR/profiles.ini" + say "Installed default /etc/backtunnel/profiles.ini" + else + say "Preserved existing /etc/backtunnel/profiles.ini" + fi fi -if command -v kbuildsycoca6 >/dev/null 2>&1; then - say "Rebuilding KDE sycoca (Plasma 6)..." - kbuildsycoca6 --noincremental >/dev/null 2>&1 || true -elif command -v kbuildsycoca5 >/dev/null 2>&1; then - say "Rebuilding KDE sycoca (Plasma 5)..." - kbuildsycoca5 --noincremental >/dev/null 2>&1 || true + +# --- Refresh desktop/KDE cache (best-effort; skip during DESTDIR packaging) --- +if [[ -z "${DESTDIR}" ]]; then + if command -v update-desktop-database >/dev/null 2>&1; then + say "Refreshing desktop database..." + update-desktop-database -q || true + fi + if command -v kbuildsycoca6 >/dev/null 2>&1; then + say "Rebuilding KDE sycoca (Plasma 6)..." + kbuildsycoca6 --noincremental >/dev/null 2>&1 || true + elif command -v kbuildsycoca5 >/dev/null 2>&1; then + say "Rebuilding KDE sycoca (Plasma 5)..." + kbuildsycoca5 --noincremental >/dev/null 2>&1 || true + fi fi say "Install complete." diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh index dea8277..da084f4 100644 --- a/scripts/uninstall.sh +++ b/scripts/uninstall.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash -# BackTunnel uninstaller (with optional PURGE=1 to remove shared defaults) +# BackTunnel uninstaller (use PURGE=1 to remove /usr/share/backtunnel and /etc/backtunnel) set -euo pipefail PREFIX=${PREFIX:-/usr} DESTDIR=${DESTDIR:-} -PURGE=${PURGE:-0} # set PURGE=1 to remove /usr/share/backtunnel and /etc/backtunnel +PURGE=${PURGE:-0} BINDIR="$DESTDIR$PREFIX/bin" MANDIR="$DESTDIR$PREFIX/share/man/man1" @@ -15,73 +15,45 @@ BCOMP="$DESTDIR$PREFIX/share/bash-completion/completions" SHARE_DIR="$DESTDIR$PREFIX/share/backtunnel" ETC_DIR="$DESTDIR/etc/backtunnel" -say() { printf '[BackTunnel] %s\n' "$*"; } -say_warn() { printf '[BackTunnel] WARN: %s\n' "$*" >&2; } +say() { printf '[BackTunnel] %s\n' "$@"; } +# ... existing code ... +rm -f \ + "$BINDIR/backtunnel-share" \ + "$BINDIR/backtunnel-access" \ + "$BINDIR/backtunnel-share-gui" \ + "$BINDIR/backtunnel-access-gui" \ + "$BINDIR/backtunnel-open-term" \ + "$BINDIR/backtunnel-share-tui" \ + "$BINDIR/backtunnel-access-tui" \ + "$BINDIR/backtunnel-keys" \ + "$BINDIR/backtunnel-authorize" \ + "$BINDIR/backtunnel-auth-setup" \ + "$BINDIR/backtunnel-init" -# Portable directory prune: remove dir if empty, then move up until stop boundary -prune_dir() { - local dir="${1%/}" - local stop="${2%/}" - while [[ -n "$dir" && "$dir" != "/" && "$dir" != "$stop" ]]; do - rmdir "$dir" 2>/dev/null || break - dir="$(dirname "$dir")" - done -} - -# Friendly notice if uninstalling the live system without root -if [[ -z "$DESTDIR" && ${EUID:-$(id -u)} -ne 0 ]]; then - say_warn "Running without root; some files may not be removed due to permissions." -fi - -say "Uninstalling from PREFIX=${PREFIX} DESTDIR=${DESTDIR} (PURGE=${PURGE})" - -# --- Remove binaries --- -rm -f "$BINDIR/backtunnel-share" \ - "$BINDIR/backtunnel-access" \ - "$BINDIR/backtunnel-auth-setup" \ - "$BINDIR/backtunnel-share-gui" \ - "$BINDIR/backtunnel-access-gui" \ - "$BINDIR/backtunnel-open-term" \ - "$BINDIR/backtunnel-share-tui" \ - "$BINDIR/backtunnel-access-tui" -# Optionally remove helper that may be present on some installs -rm -f "$BINDIR/backtunnel-init" - -# --- Man page --- rm -f "$MANDIR/backtunnel.1" -# --- Bash completions --- -rm -f "$BCOMP/backtunnel-share" \ - "$BCOMP/backtunnel-access" +rm -f \ + "$BCOMP/backtunnel-share" \ + "$BCOMP/backtunnel-access" -# --- Dolphin service menus (Plasma 6 + legacy) --- -rm -f "$KIO_SM/backtunnel_share.desktop" \ - "$KIO_SM/backtunnel_access.desktop" \ - "$KSVC5/backtunnel_share.desktop" \ - "$KSVC5/backtunnel_access.desktop" +rm -f \ + "$KIO_SM/backtunnel_share.desktop" \ + "$KIO_SM/backtunnel_access.desktop" \ + "$KSVC5/backtunnel_share.desktop" \ + "$KSVC5/backtunnel_access.desktop" -# --- Optional desktop launcher --- rm -f "$APPDIR/backtunnel.desktop" - -# --- Shared defaults (only if PURGE=1) --- +# ... existing code ... +# Optional purge of packaged defaults if [[ "$PURGE" = "1" ]]; then - say "Purging shared defaults under $SHARE_DIR and $ETC_DIR" - rm -f "$SHARE_DIR/profiles.ini" 2>/dev/null || true - rm -f "$ETC_DIR/profiles.ini" 2>/dev/null || true - # Remove directories if empty (and prune empty parents up to safe boundaries) - prune_dir "$SHARE_DIR" "$DESTDIR$PREFIX/share" - prune_dir "$ETC_DIR" "$DESTDIR/etc" -else - # Optionally clean up empty share dir if package manager removed files already - prune_dir "$SHARE_DIR" "$DESTDIR$PREFIX/share" - say "Keeping shared defaults: $SHARE_DIR/ and $ETC_DIR/ (set PURGE=1 to remove)" + rm -rf "$SHARE_DIR" || true + rm -rf "$ETC_DIR" || true + say "Purged /usr/share/backtunnel and /etc/backtunnel" fi -# --- Refresh desktop/KDE cache (best-effort, skip during packaging) --- -if [[ -z "$DESTDIR" ]]; then - if command -v update-desktop-database >/dev/null 2>&1; then - update-desktop-database -q || true - fi +# Refresh caches only if not in DESTDIR (packaging) +if [[ -z "${DESTDIR}" ]]; then + command -v update-desktop-database >/dev/null 2>&1 && update-desktop-database -q || true if command -v kbuildsycoca6 >/dev/null 2>&1; then kbuildsycoca6 --noincremental >/dev/null 2>&1 || true elif command -v kbuildsycoca5 >/dev/null 2>&1; then