Files
Your Name d817c37652
gates / consistency-and-conventions (push) Successful in 26s
fix: stabilization pass — fail-closed auth, ai flag validation, lint/config/security hardening, regression tests
17-point code-level audit executed via Explorer->Architect->Builder->Tester->Reviewer;
Reviewer accepted (APPROVE_WITH_NOTES; 3 block-list items resolved):

- security: telegram sender-owner AND-gate + TELEGRAM_OWNER_ID, matrix
  MATRIX_ROOM_ID fail-closed, gpg --passphrase-fd 3 (no argv secret),
  /dev/tcp positional-arg form (checkport/smb-client/share-lib/NET_PROBE),
  eval deny-by-default + --no-command-execution carried by both chat bridges,
  tty-gated --trust; config/{telegram,matrix}.env reference templates
- ai: all ExecStart flags validated against installed llama.cpp
  (requested->error, default->omit+warn, CONFIG_REQUESTED_FLAGS); single-file
  hf download failure rc=1 + no .hf-meta; LLAMACPP_HOST coherent;
  POS_SUBCMDS + metadata gaps closed
- tooling: lint-conventions Bash-native rewrite (~24-30x faster, rules and
  output byte-identical, :num restored); pos system uninstall covers all 12
  libs + scale-tail + flags dir + systemd user units (|| true) + plugin
  markers; anchored .bash_completion/.bashrc removal replaces sed -i '/pos/d'
- config: canonical load_env_file in lib/config-ui.sh (CRLF strip, env-wins,
  XDG, LOADED_ENV_KEYS); 9 tools migrated; entertainment-lib collapsed to
  wrappers; docker-compose deliberately unmigrated (source semantics)
- tests: first committed regression suite — tests/run-tests.sh zero-dep
  runner + make test; 12 files / 179 checks / 0 skip / ~52s; hard skip
  contract; systemd-analyze verify on generated unit PASS

Verified: make gen idempotent; make check green; make lint 0 FAIL, 0 WARN;
make test green; bash -n clean; git diff --check clean. Audit deliverables +
agent reports + AGENT_TODO Done entry included.
2026-09-06 07:25:44 -04:00

321 lines
14 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# lib/share-lib.sh — precondition probes, remote listings and advisories for
# the `pos share` suite (nfs/smb/usb tools), plus compat shims to the
# category-neutral menu layer (lib/menu-lib.sh).
#
# Contracts (all of them, no exceptions):
# * Defines ONLY `share_*` functions — sourcing never clobbers a tool's own
# helpers (same discipline as lib/notify.sh).
# * Requires common.sh to be sourced by the CALLER first (log/warn/err/
# confirm/run/spawn + CYAN/RESET colors are used, never defined here).
# * NEVER exits and never terminates the caller: every function returns,
# failures are signalled through the return code.
# * Display goes to stderr, results go to stdout — any function whose result
# is meant to be command-substituted prints ONLY the result on stdout
# (matches the ui_pick contract in the telegram/matrix listeners).
# * Performs NO file writes of its own — every path a tool persists stays
# tool-owned (the env-seam surface does not grow here).
#
# Function index:
# share_menu_run <title> <item...> shim → menu_run (lib/menu-lib.sh)
# share_pick <prompt> <item...> shim → menu_pick (lib/menu-lib.sh)
# share_ask_value <label> [default] shim → menu_ask_value (lib/menu-lib.sh)
# share_menu_guard shim → menu_guard (lib/menu-lib.sh)
# share_require_bin <bin> <hint> dependency probe (rc only)
# share_port_probe <host> <port> TCP reachability probe (rc only)
# share_service_active <unit> systemd unit state probe (rc only)
# share_ufw_blocks_ports <egrep> ufw-blocking decision (rc only)
# share_offer_fix <desc> <cmd...> advisory remediation offer (rc always 0)
# share_nfs_exports <host> remote export list via showmount
# share_smb_shares <host> [user] remote Disk-share list via smbclient
# share_usb_devices / _clients usbsrv listing records as "ID|display"
# share_folder_candidates mountpoint/dir candidates for sharing
# ── Menu primitives live in lib/menu-lib.sh (category-neutral) ─
# The generic interactive layer was extracted there; these thin shims keep the
# public `share_*` names/contracts identical for all five share tools. Delegation
# preserves rc semantics 1:1 (guard rc, EOF → rc 1, index/value → stdout only).
source "$(dirname "${BASH_SOURCE[0]}")/../lib/menu-lib.sh" 2>/dev/null \
|| source "$(dirname "${BASH_SOURCE[0]}")/menu-lib.sh" 2>/dev/null \
|| source "$(dirname "$0")/../lib/menu-lib.sh" 2>/dev/null \
|| source "$(dirname "$0")/menu-lib.sh"
share_menu_guard() { menu_guard "$@"; }
share_menu_run() { menu_run "$@"; }
share_pick() { menu_pick "$@"; }
share_ask_value() { menu_ask_value "$@"; }
# ── Dependency probe (NOT an err wrapper) ──────────────────────
# rc 0 present · rc 1 absent. Callers decide between err() and graceful
# degradation; <hint> documents intent at call sites and is intentionally
# not printed here (message policy belongs to the caller).
share_require_bin() {
command -v "$1" >/dev/null 2>&1
}
# ── TCP reachability probe (generalized probe_server core) ─────
# rc 0 reachable within 3s · rc 1 unreachable/no-route. Message policy (targeted
# hints, firewall wording) belongs to the caller.
share_port_probe() {
# host/port are positional args ($1/$2), never interpolated into the
# command source — a hostile host string stays a literal argument.
timeout 3 bash -c 'exec 3<>/dev/tcp/$1/$2' _ "${1}" "${2}" 2>/dev/null
}
# ── systemd unit state probe ───────────────────────────────────
# rc 0 active · rc 1 inactive/unqueryable (nonzero systemctl codes normalized).
share_service_active() {
if systemctl is-active --quiet "$1" 2>/dev/null; then
return 0
fi
return 1
}
# ── Bounded path probe ─────────────────────────────────────────
# share_path_probe <-d|-w> <path> — rc 0 when the stat answers within 2s.
# Guards discovery against wedged network mountpoints where a plain
# `[ -d … ]` would block forever.
share_path_probe() {
timeout 2 bash -c '[ "$1" "$2" ]' _ "$1" "$2" 2>/dev/null
}
# ── ufw blocking decision ──────────────────────────────────────
# Core moved verbatim from pos-share-smb-server's ufw_blocks_samba, with the
# rule pattern parameterized. rc 0 = ufw active but no matching rule (traffic
# blocked) · rc 1 = ufw absent/inactive OR a matching rule exists (not blocking).
# Callers own all message text.
share_ufw_blocks_ports() {
command -v ufw >/dev/null 2>&1 || return 1
local st
st="$(sudo ufw status 2>/dev/null)" || return 1
grep -q "^Status: active" <<<"$st" || return 1
if grep -qiE "$1" <<<"$st"; then
return 1
fi
return 0
}
# ── Advisory remediation offer ─────────────────────────────────
# On a terminal: confirm "<desc>. Fix it now?" (default No) and run/spawn the
# command, reporting the outcome. Without a terminal: print desc + manual
# command as a hint-only warning. ALWAYS rc 0 — purely advisory; aborting on a
# declined offer stays the caller's confirm/err decision.
share_offer_fix() {
local desc="$1"; shift
if [ -t 0 ]; then
echo >&2
if confirm "$desc. Fix it now?" n; then
if run "$@"; then
ok "Fixed: $*"
else
warn "Fix command failed: $*"
fi
else
warn "Declined — run manually: $*"
fi
else
warn "$desc — run manually: $*"
fi
return 0
}
# ── Remote NFS export enumeration ──────────────────────────────
# stdout: export paths, one per row (column 1 of the showmount table, header
# skipped). rc 0 ok · rc 1 unavailable (showmount missing / timeout / RPC
# failure / no exports) with the reason warned on stderr.
share_nfs_exports() {
local host="$1" out rows
if ! command -v showmount >/dev/null 2>&1; then
printf '[!] showmount not found (install nfs-common) — cannot list exports from %s\n' "$host" >&2
return 1
fi
if ! out="$(timeout 5 showmount -e "$host" 2>&1)"; then
printf "[!] could not list exports from %s (server down, RPC/firewall blocked, or timeout)\n" "$host" >&2
return 1
fi
rows="$(awk 'NF > 0 && $1 !~ /^Export/ {print $1}' <<<"$out")"
if [ -z "$rows" ]; then
printf "[!] no exports visible on %s\n" "$host" >&2
return 1
fi
printf '%s\n' "$rows"
}
# ── Remote SMB share enumeration ───────────────────────────────
# stdout: Disk share names, one per line (-g parse; IPC$/printer `*$` names
# dropped). Sets SMB_AUTH_USER="" at entry, and to the account that ended up
# authenticating, so callers can reuse it for the actual mount:
# * user argument given → authenticate immediately (password prompted,
# travels via the PASSWD environment, never argv); no guest attempt.
# * no user argument → guest query first; on ACCESS_DENIED /
# LOGON_FAILURE a Samba user + password are asked once (TTY required)
# and the query retries.
# rc 0 ok · rc 1 unavailable / failed-after-retry (reason warned on stderr).
share_smb_shares() {
local host="$1" user="${2:-}" out names pw u
local -r GPARSE='BEGIN { FS = "|" } $1 == "Disk" && $2 != "" && $2 != "IPC$" && $2 !~ /\$$/ { print $2 }'
SMB_AUTH_USER=""
if ! command -v smbclient >/dev/null 2>&1; then
printf '[!] smbclient not found — cannot enumerate shares (install the smbclient package)\n' >&2
return 1
fi
if [ -n "$user" ]; then
if ! [ -t 0 ]; then
printf '[!] Samba login for %s needs a terminal (password prompt)\n' "$host" >&2
return 1
fi
read -rsp "Samba password for $user: " pw || { echo >&2; return 1; }
echo >&2
if out="$(PASSWD="$pw" smbclient -L "//${host}/" -g -t 5 -U "$user" 2>&1)"; then
SMB_AUTH_USER="$user"
else
printf '[!] share enumeration failed for %s@%s (wrong user/password?)\n' "$user" "$host" >&2
return 1
fi
else
if ! out="$(smbclient -L "//${host}/" -N -g -t 5 2>&1)"; then
case "$out" in
*ACCESS_DENIED*|*LOGON_FAILURE*|*NOT_GRANTED*)
if ! [ -t 0 ]; then
printf '[!] %s requires authentication — rerun interactively (menu) to enter a Samba user\n' "$host" >&2
return 1
fi
printf '[!] %s rejected guest access — a Samba login is required\n' "$host" >&2
u="$(share_ask_value "Samba user for ${host}")" || return 1
read -rsp "Samba password for $u: " pw || { echo >&2; return 1; }
echo >&2
if ! out="$(PASSWD="$pw" smbclient -L "//${host}/" -g -t 5 -U "$u" 2>&1)"; then
printf '[!] share enumeration failed for %s@%s after retry\n' "$u" "$host" >&2
return 1
fi
SMB_AUTH_USER="$u"
;;
*)
printf '[!] could not enumerate shares on %s: %s\n' "$host" "$(head -1 <<<"$out")" >&2
return 1
;;
esac
fi
fi
names="$(printf '%s\n' "$out" | awk "$GPARSE")"
if [ -z "$names" ]; then
printf '[!] no Disk shares visible on %s\n' "$host" >&2
return 1
fi
printf '%s\n' "$names"
}
# ── USB Redirector listings ────────────────────────────────────
# Both emit blank-line-split records as "ID|display-line" rows; rc 0 parsed ≥1
# record · rc 1 unparsable/down — callers fall back to the raw listing plus a
# manual ID entry (= today's UX).
share_usb_records() { # internal helper: stdin = raw listing, $1 = ID-line regex
awk -v idre="$1" '
BEGIN { RS = "" }
{
n = split($0, L, "\n")
id = ""; desc = ""; fb = ""
for (i = 1; i <= n; i++) {
line = L[i]
if (line ~ idre && id == "") {
id = line
sub(/^[^:]*:[ \t]*/, "", id)
} else if (line ~ /^Description:/) {
desc = line
sub(/^Description:[ \t]*/, "", desc)
} else if (fb == "") {
fb = line
}
}
if (id != "") print id "|" (desc != "" ? desc : fb)
}'
}
share_usb_devices() {
local raw recs
if ! command -v usbsrv >/dev/null 2>&1; then
printf '[!] usbsrv not found — install the USB Redirector server first\n' >&2
return 1
fi
if ! raw="$(usbsrv -list-devices 2>&1)"; then
printf '[!] usbsrv -list-devices failed — is the USB Redirector server running?\n' >&2
return 1
fi
recs="$(printf '%s\n' "$raw" | share_usb_records '^ID:')"
if [ -z "$recs" ]; then
printf '[!] no USB device records could be parsed from the server listing\n' >&2
return 1
fi
printf '%s\n' "$recs"
}
share_usb_clients() {
local raw recs
if ! command -v usbsrv >/dev/null 2>&1; then
printf '[!] usbsrv not found — install the USB Redirector server first\n' >&2
return 1
fi
if ! raw="$(usbsrv -list-clients 2>&1)"; then
printf '[!] usbsrv -list-clients failed — is the USB Redirector server running?\n' >&2
return 1
fi
recs="$(printf '%s\n' "$raw" | share_usb_records '^Client ID:')"
if [ -z "$recs" ]; then
printf '[!] no connected clients could be parsed from the server listing\n' >&2
return 1
fi
printf '%s\n' "$recs"
}
# ── Share-folder candidates ────────────────────────────────────
# stdout: candidate paths, one per line — writable real-filesystem findmnt
# targets (pseudo-fs denylist and read-only mounts excluded) immediate
# directories under /mnt,/srv,/media,/export. LC_ALL=C sorted, deduplicated;
# entries that are mountpoints carry a "(mounted <fstype>)" annotation.
# rc 0 always; an empty list is allowed (caller falls back to manual entry).
share_folder_candidates() {
local -A seen=()
local line fs tgt opts entry child r path fst
local -a cands=()
# findmnt targets: split TARGET FSTYPE OPTIONS (TARGET may hold escaped
# spaces, OPTIONS/FSTYPE never do) — pseudo-fs denylist + ro exclusion.
while IFS=$'\t' read -r fs tgt opts; do
[ -n "$tgt" ] || continue
case "$fs" in
proc | sysfs | devtmpfs | devpts | tmpfs | cgroup | cgroup2 | squashfs | \
overlay | mqueue | hugetlbfs | debugfs | tracefs | configfs | fusectl | \
securityfs | pstore | efivarfs | bpf | ramfs | nsfs | binfmt_misc | autofs | \
iso9660 | swap | fuse.* | cgroupfs) continue ;;
esac
case ",$opts," in *,ro,*) continue ;; esac
share_path_probe -w "$tgt" || continue
cands+=("${tgt}|${fs}")
done < <(timeout 5 findmnt -rn -o TARGET,FSTYPE,OPTIONS 2>/dev/null |
awk '{ opts=$NF; fstype=$(NF-1); tgt=substr($0, 1, length($0)-length(opts)-length(fstype)-1); sub(/[ \t]+$/, "", tgt); print fstype "\t" tgt "\t" opts }')
# immediate directories under the conventional share roots
for r in /mnt /srv /media /export; do
[ -d "$r" ] || continue
for child in "$r"/*; do
share_path_probe -d "$child" || continue
cands+=("${child}|")
done
done
# annotate mountpoints, prefer annotated duplicates, sort byte-order
for line in "${cands[@]}"; do
path="${line%%|*}"
fst="${line#*|}"
if [ -n "$fst" ]; then
seen["$path"]="${path} (mounted ${fst})"
elif [ -z "${seen[$path]:-}" ]; then
seen["$path"]="$path"
fi
done
for path in "${!seen[@]}"; do
printf '%s\n' "${seen[$path]}"
done | LC_ALL=C sort -u
}