#!/usr/bin/env bash # nxios.sh — NxIOS toolbox client # # Server: https://www.nxios.ca # # Commands: # nxios.sh [-s|--silent] [-4|-6] COMMAND [options] # # nxios.sh push [--path=CODE] [--file:NAME | --dotfile:NAME] [--name=DESTNAME | --as:DESTNAME] # nxios.sh get [--path=CODE] [--file:NAME | --dotfile:NAME] [--as:DEST] [--hashonly|-h] # nxios.sh show [--path=CODE] [--file:NAME | --dotfile:NAME] [--diff:LOCALFILE] # nxios.sh install [--path=CODE] [--file:NAME | --dotfile:NAME] # nxios.sh exec [--path=CODE] [--file:NAME | --dotfile:NAME] [--params:"ARGS"] [-- ARGS...] # nxios.sh ls|dir [--path=CODE] # nxios.sh rm|del [--path=CODE] [--file:NAME | --dotfile:NAME] # nxios.sh ren [--path=CODE] --from:OLD --to:NEW # # nxios.sh smb --mount:NAME --using:UNC --as:PROFILE [--ro|--read-only|-r] # nxios.sh smb --unmount:NAME # nxios.sh smb --createProfile:USER --password:PASSWORD # # nxios.sh ssh --init [--path:CODE] [--server:LABEL] [--host:ADDR] [--port:PORT] [--opts:Opt=Val;...] [--rekey] # nxios.sh ssh --forget:SERVER|HOST [--path:CODE] # nxios.sh ssh --forget-all:SERVER|HOST [--path:CODE] # nxios.sh ssh --server:USER@HOST [--path:CODE] [--opts:Opt=Val;...] [--cmd:"REMOTE CMD"] [-- ...ssh args...] # nxios.sh ssh --server:HOST [--user:USER] [--path:CODE] [--opts:Opt=Val;...] [--cmd:"REMOTE CMD"] [-- ...ssh args...] # # nxios.sh vyos-save [--run[:name]] [--start[:name]] [--path:vyos,{host}] # nxios.sh vyos-load --path:vyos,{host} --file:.config [--action:[compare|commit|save|confirm]] # # nxios.sh tls --show[:set-name] # nxios.sh tls --expire: # nxios.sh tls --install: --target:[.sh|.ps1] # nxios.sh tls --get-pem: [--to:DESTDIR] [--as-zip] # nxios.sh tls --get-pfx: [--as:DESTFILE] # # nxios.sh update [--path=CODE] # stub - needs list.php # nxios.sh mkdir [--path=CODE] # stub - needs mkdir.php # nxios.sh rmdir [--path=CODE] # stub - needs rmdir.php # # Roots/aliases are fetched from: # https://www.nxios.ca/?init=roots # (lines: "alias" => "Canonical\\Path") set -euo pipefail # Ensure $USER is set (Termux and some non-login shells may not define it) if [[ -z "${USER-}" ]]; then if [[ -n "${LOGNAME-}" ]]; then USER="$LOGNAME" else USER="$(id -un 2>/dev/null || whoami 2>/dev/null || printf 'brett')" fi fi if ! declare -A __test 2>/dev/null; then echo "ERROR: nxios.sh requires bash with associative arrays (bash 4+)." >&2 exit 1 fi BASE_URL="${NXIOS_URL:-https://www.nxios.ca}" ROOTS_ENDPOINT="${BASE_URL}/?init=roots" # plain-text; used only by 'roots' display command INIT_ENDPOINT="${BASE_URL}/?init=roots&json=" # JSON; used by load_roots() for all internal work DIR_ENDPOINT="${BASE_URL}/dir.php" UPLOAD_ENDPOINT="${BASE_URL}/upload.php" CHUNK_ENDPOINT="${BASE_URL}/chunk-upload.php" DELETE_ENDPOINT="${BASE_URL}/delete.php" RENAME_ENDPOINT="${BASE_URL}/rename.php" # DOTFILE downloads now go through utils.php (fn=dfdl) to bypass IIS extension blocks. UTILS_ENDPOINT="${BASE_URL}/utils.php" HOME_FOLDER="/home/brett/scripts" declare -A ROOT_MAP ROOTS_LOADED=0 DEFAULT_PATH_ALIAS="" NXIOS_START_CHUNKING=0 # byte threshold above which chunked upload is used NXIOS_PHP_FILE_MAX="" # server's upload_max_filesize (e.g. "32M") NXIOS_PHP_POST_MAX="" # server's post_max_size (e.g. "32M") # Global flags (can be set via CLI or env) NXIOS_SILENT="${NXIOS_SILENT:-0}" # 1 = force silent, 0 = allow progress if enabled NXIOS_IP_FAMILY="${NXIOS_IP_FAMILY:-}" # 4 or 6; empty = system default # NXIOS_PROGRESS (env-only): 1 = show progress bar (if not silent and TTY) # NXIOS_DEBUG (env-only): 1 = echo curl commands # NXIOS_USER (env/override): user identity header override # NXIOS_KEY (env/optional): API key header # Prefer --fail-with-body when supported so HTTP 4xx/5xx include server text. NXIOS_CURL_FAIL_FLAG="--fail" if curl -h 2>/dev/null | grep -q -- '--fail-with-body'; then NXIOS_CURL_FAIL_FLAG="--fail-with-body" fi usage() { cat >&2 <.config [--action:[compare|commit|save|confirm]] tls --show[:set-name] | --expire: | --install: --target:[.sh|.ps1] tls --get-pem: [--to:DESTDIR] [--as-zip] tls --get-pfx: [--as:DESTFILE] update|mkdir|rmdir (stubs) NOTE: Use 'nxios.sh refresh' to update the current nxios script with the latest version from the server. See: man nxios EOF exit 1 } nx_curl() { # Generic curl wrapper for all NxIOS usage. # # Defaults: # -L follow redirects # fail flag comes from $NXIOS_CURL_FAIL_FLAG (e.g. -f or --fail-with-body) # # Env toggles: # NXIOS_PROGRESS=1 -> show progress meter for interactive sessions # NXIOS_DEBUG=1 -> echo curl command to stderr before running # NXIOS_USER -> override user identity header # NXIOS_KEY -> optional API key header # NXIOS_SILENT=1 -> force silent mode unless overridden # NXIOS_IP_FAMILY=4 -> force IPv4 # NXIOS_IP_FAMILY=6 -> force IPv6 # # Internal per-call overrides: # --nxios-bar -> force curl progress bar, even if NXIOS_SILENT=1 # --nxios-meter -> force curl progress meter, even if NXIOS_SILENT=1 # --nxios-no-silent -> disable automatic -s for this call # # Notes: # - curl progress output goes to stderr, so it still works when stdout # is captured via command substitution. # - By default, non-progress calls are silent but still show errors. local -a args passthru local show_progress="${NXIOS_PROGRESS:-0}" local debug="${NXIOS_DEBUG:-0}" local silent="${NXIOS_SILENT:-0}" local ip_family="${NXIOS_IP_FAMILY:-}" local force_progress=0 local no_silent=0 # Consume internal control flags first while [[ $# -gt 0 ]]; do case "$1" in --nxios-meter) force_progress=2 shift ;; --nxios-bar) force_progress=1 shift ;; --nxios-no-silent) no_silent=1 shift ;; *) break ;; esac done args=(curl) [[ -n "$NXIOS_CURL_FAIL_FLAG" ]] && args+=("$NXIOS_CURL_FAIL_FLAG") args+=(-L) case "$ip_family" in 4) args+=(-4) ;; 6) args+=(-6) ;; esac # Progress / silence policy # 1. explicit bar override # 2. explicit meter override # 3. explicit no-silent # 4. global silent # 5. default interactive progress # 6. otherwise silent if (( force_progress == 1 )); then args+=(--progress-bar) elif (( force_progress == 2 )); then : elif (( no_silent )); then : elif [[ "$silent" == "1" ]]; then args+=(-s) elif [[ "$show_progress" == "1" && -t 2 ]]; then : else args+=(-s) fi local user="${NXIOS_USER:-$USER}" if [[ -n "$user" ]]; then args+=(-H "X-NXIOS-User:${user}") fi local key="${NXIOS_KEY:-}" if [[ -n "$key" ]]; then args+=(-H "X-NXIOS-Key:${key}") fi passthru=("$@") args+=("${passthru[@]}") if [[ "$debug" == "1" ]]; then printf '[nx_curl] ' >&2 printf '%q ' "${args[@]}" >&2 printf '\n' >&2 fi "${args[@]}" } load_roots() { if [[ "$ROOTS_LOADED" -eq 1 ]]; then return fi printf "Fetching roots from nxios.ca..." >&2 local tmp tmp="$(nx_mktemp "nxios.roots.XXXXXX")" if nx_curl "$INIT_ENDPOINT" -o "$tmp"; then : else local rc=$? echo "ERROR: failed to fetch roots from $INIT_ENDPOINT (curl exit code $rc)." >&2 rm -f "$tmp" exit 1 fi DEFAULT_PATH_ALIAS="" # Parse the flat JSON returned by ?init=roots&json= using regex — no jq/python needed. # # The response has two sections we care about: # # "roots": { # "key": "Value", <- ROOT_MAP entries # ... # }, # "start_chunking": 10485760, <- bare integer # "php_file_max": "32M", <- quoted string # "php_post_max": "32M" <- quoted string # # Strategy: one-pass line scan with an $in_roots flag to separate the two # sections. Regex patterns handle both quoted values ("32M") and bare # integers (10485760) without branching. local in_roots=0 while IFS= read -r line; do [[ -z "$line" ]] && continue # ── entering the "roots" object ────────────────────────────────── if [[ "$line" =~ \"roots\"[[:space:]]*: ]]; then in_roots=1 continue fi # ── closing brace ends the roots block ─────────────────────────── # Matches a line that is only whitespace + } (with optional trailing # comma), which is what the roots closing brace looks like. if [[ $in_roots -eq 1 && "$line" =~ ^[[:space:]]*\}[[:space:],]*$ ]]; then in_roots=0 continue fi if [[ $in_roots -eq 1 ]]; then # ── roots entry: "alias": "Path" ───────────────────────────── local alias path alias=$(sed -n 's/^[[:space:]]*"\([^"]\+\)"[[:space:]]*:[[:space:]]*".*/\1/p' <<<"$line" || true) path=$( sed -n 's/^[[:space:]]*"[^"]\+"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' <<<"$line" || true) [[ -z "$alias" || -z "$path" ]] && continue ROOT_MAP["$alias"]="$path" if [[ -z "$DEFAULT_PATH_ALIAS" ]]; then DEFAULT_PATH_ALIAS="$alias" fi else # ── top-level scalar keys (chunking values) ─────────────────── # Matches both "key": "StringVal" and "key": 12345 local key val key=$(sed -n 's/^[[:space:]]*"\([^"]\+\)"[[:space:]]*:[[:space:]]*.\+/\1/p' <<<"$line" || true) val=$(sed -n 's/^[[:space:]]*"[^"]\+"[[:space:]]*:[[:space:]]*"\?\([^",}[:space:]]*\)"\?.*/\1/p' <<<"$line" || true) [[ -z "$key" || -z "$val" ]] && continue case "$key" in start_chunking) NXIOS_START_CHUNKING="$val" ;; php_file_max) NXIOS_PHP_FILE_MAX="$val" ;; php_post_max) NXIOS_PHP_POST_MAX="$val" ;; esac fi done <"$tmp" rm -f "$tmp" ROOTS_LOADED=1 # Safety fallback if [[ -z "$DEFAULT_PATH_ALIAS" ]]; then DEFAULT_PATH_ALIAS="toolbox" fi printf " Done. (%d roots; default: '%s')\n\n" "${#ROOT_MAP[@]}" "$DEFAULT_PATH_ALIAS" >&2 } # Ensure a PATH-like variable has a value; if empty, use the first root alias ensure_path_default() { local __varname="$1" local __current="${!__varname:-}" if [[ -z "$__current" ]]; then load_roots printf -v "$__varname" '%s' "$DEFAULT_PATH_ALIAS" elif [[ "$__current" == ,* ]]; then # If the caller passed ",foo", prepend the default root alias. load_roots printf -v "$__varname" '%s' "${DEFAULT_PATH_ALIAS}${__current}" fi } nx_normalize_codeword_host() { # Normalize a comma-separated codeword, expanding host tokens to $(hostname). local code="$1" [[ -z "$code" ]] && { printf '%s\n' "$code"; return; } local IFS=',' parts=() seg IFS=',' read -r -a parts <<< "$code" local host_short="" local host_fqdn="" if command -v hostname >/dev/null 2>&1; then host_short="$(hostname)" host_fqdn="$(hostname -f 2>/dev/null || hostname 2>/dev/null || "$host_short")" fi [[ -z "$host_fqdn" ]] && host_fqdn="$host_short" local -a host_tokens=( "@@host" "__host__" "@host@" "%host%" "<>" "[[host]]" ) local -a fqdn_tokens=( "@@fqdn" "__fqdn__" "@fqdn@" "%fqdn%" "<>" "[[fqdn]]" ) local i t for i in "${!parts[@]}"; do seg="${parts[$i]}" for t in "${host_tokens[@]}"; do if [[ "${seg,,}" == "$t" ]]; then parts[i]="$host_short" break fi done for t in "${fqdn_tokens[@]}"; do if [[ "${seg,,}" == "$t" ]]; then parts[i]="$host_fqdn" break fi done done (IFS=','; printf '%s\n' "${parts[*]}") } decode_codeword_to_webpath() { # Resolve a codeword like 'cfg,[[host]]' into a web path using ROOT_MAP. local code code="$(nx_normalize_codeword_host "$1")" local lower="${code,,}" IFS=',' read -r -a parts <<< "$lower" if (( ${#parts[@]} == 0 )) || [[ -z "${parts[0]}" ]]; then echo "ERROR: Empty or invalid codeword: '$code'" >&2 exit 1 fi local rootKey="${parts[0]}" if [[ -z "${ROOT_MAP[$rootKey]+x}" ]]; then echo "ERROR: Unknown root in codeword: '$rootKey'" >&2 echo "Valid roots/aliases:" >&2 for k in "${!ROOT_MAP[@]}"; do echo " $k" >&2 done exit 1 fi local canon="${ROOT_MAP[$rootKey]}" canon="${canon//\\//}" # backslashes -> forward slashes for URLs if (( ${#parts[@]} > 1 )); then local i seg for (( i=1; i<${#parts[@]}; i++ )); do seg="${parts[$i]}" [[ -z "$seg" ]] && continue canon="${canon}/${seg}" done fi printf '%s\n' "$canon" } normalize_shell_script() { local file="$1" local tmp="${file}.tmp.$$" tr -d '\r' < "$file" > "$tmp" && mv "$tmp" "$file" chmod +x "$file" 2>/dev/null || true } parse_bool() { # Usage: parse_bool → echoes "true" or "false" local v="${1:-}" v="${v,,}" # to lowercase case "$v" in ""|"1"|"true"|"yes"|"on") echo "true" ;; *) echo "false" ;; esac } nx_get_file_url() { local code="$1" local name="$2" local root="${code%%,*}" if [[ "$root" == "smb" ]]; then printf '%s\n' "${UTILS_ENDPOINT}?fn=smb&op=pull&name=${name}" return 0 fi printf '%s\n' "${UTILS_ENDPOINT}?fn=get-file&code=${code}&name=${name}" } nx_sha256_file() { local file="$1" if command -v sha256sum >/dev/null 2>&1; then sha256sum "$file" | awk '{print $1}' return 0 fi if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$file" | awk '{print $1}' return 0 fi if command -v openssl >/dev/null 2>&1; then openssl dgst -sha256 "$file" | awk '{print $2}' return 0 fi return 1 } # Create temp files/directories under ${TMPDIR:-/tmp} nx_mktemp() { local tmpl="${1:-nxios.tmp.XXXXXX}" local base="${TMPDIR:-/tmp}" mktemp -p "$base" "$tmpl" } nx_mktemp_dir() { local tmpl="${1:-nxios.tmpdir.XXXXXX}" local base="${TMPDIR:-/tmp}" mktemp -p "$base" -d "$tmpl" } # -------------------- tls helpers ------------------------------------ tls_fetch_xml() { local tmp tmp="$(nx_mktemp "nxios.tls.XXXXXX.xml")" local url url="$(nx_get_file_url "tls" "san-mappings.xml")" if nx_curl "$url" -o "$tmp"; then : else local rc=$? echo "ERROR: failed to download san-mappings.xml (exit $rc)." >&2 rm -f "$tmp" return "$rc" fi echo "$tmp" } tls_get_set_info() { local xml_path="$1" local set_name="$2" python - "$xml_path" "$set_name" <<'PY' import re import sys import xml.etree.ElementTree as ET xml_path, set_name = sys.argv[1], sys.argv[2] tree = ET.parse(xml_path) root = tree.getroot() certs = root.find('Certificates') if certs is None: print("ERROR: missing.", file=sys.stderr) sys.exit(2) target = None for cert in certs.findall('Certificate'): if (cert.get('name') or '').strip() == set_name: target = cert break if target is None: print(f"ERROR: set not found: {set_name}", file=sys.stderr) sys.exit(3) renew_after = (target.get('renewAfter') or '').strip() def expand(domain): typ = (domain.get('type') or 'singleton').strip().lower() base = (domain.text or '').strip().strip('.') if base.startswith('*.'): base = base[2:] if not base: return [] tlds_raw = (domain.get('tlds') or '').strip() tlds = [t for t in re.split(r'[,\s;]+', tlds_raw) if t] if tlds_raw else [] out = [] def add(fqdn): fqdn = fqdn.lower() if typ == 'wildcard': out.append(f"*.{fqdn}") out.append(fqdn) else: out.append(fqdn) if tlds: for t in tlds: add(f"{base}.{t}") else: add(base) return out sans = [] for domain in target.findall('Domain'): sans.extend(expand(domain)) sans = sorted(set(sans)) print(renew_after) for s in sans: print(s) PY } tls_get_pfx_mtime() { local set_name="$1" local code="tls-sets,${set_name}" local listing if ! listing="$(nx_curl "${DIR_ENDPOINT}?code=${code}" 2>/dev/null)"; then return 0 fi local target="${set_name}.pfx" local line line="$(printf '%s\n' "$listing" | awk -v f="$target" 'tolower($0) ~ tolower(f) {print; exit}')" if [[ -z "$line" ]]; then return 0 fi # Expected format: MM/DD/YYYY HH:MM AM|PM SIZE NAME local date time ampm date="$(awk '{print $1}' <<<"$line")" time="$(awk '{print $2}' <<<"$line")" ampm="$(awk '{print $3}' <<<"$line")" if [[ -n "$date" && -n "$time" && -n "$ampm" ]]; then printf '%s %s %s\n' "$date" "$time" "$ampm" fi } tls_list_files() { local code="$1" local listing if ! listing="$(nx_curl "${DIR_ENDPOINT}?code=${code}" 2>/dev/null)"; then return 1 fi printf '%s\n' "$listing" | awk ' /^[0-9]{2}\/[0-9]{2}\/[0-9]{4}[[:space:]]+[0-9]{1,2}:[0-9]{2}[[:space:]]+(AM|PM)[[:space:]]+[0-9]+[[:space:]]+/ { name=$NF if (name != "") print name } ' } # -------------------- tls command ------------------------------------ cmd_tls() { local action="" set_name="" target="" dest_dir="" dest_file="" as_zip="false" while [[ $# -gt 0 ]]; do case "$1" in --show) action="show" ;; --show:*) action="show"; set_name="${1#--show:}" ;; --show=*) action="show"; set_name="${1#--show=}" ;; --expire:*) action="expire"; set_name="${1#--expire:}" ;; --expire=*) action="expire"; set_name="${1#--expire=}" ;; --install:*) action="install"; set_name="${1#--install:}" ;; --install=*) action="install"; set_name="${1#--install=}" ;; --get-pem:*) action="get-pem"; set_name="${1#--get-pem:}" ;; --get-pem=*) action="get-pem"; set_name="${1#--get-pem=}" ;; --get-pfx:*) action="get-pfx"; set_name="${1#--get-pfx:}" ;; --get-pfx=*) action="get-pfx"; set_name="${1#--get-pfx=}" ;; --target:*) target="${1#--target:}" ;; --target=*) target="${1#--target=}" ;; --to:*) dest_dir="${1#--to:}" ;; --to=*) dest_dir="${1#--to=}" ;; --as:*) dest_file="${1#--as:}" ;; --as=*) dest_file="${1#--as=}" ;; --as-zip) as_zip="true" ;; --help|-h) echo "Usage: nxios tls --show[:set-name] | --expire: | --install: --target:[.sh|.ps1] | --get-pem: [--to:DIR] [--as-zip] | --get-pfx: [--as:FILE]" >&2 return 0 ;; *) echo "Unknown tls arg: $1" >&2 return 1 ;; esac shift done case "$action" in show) if [[ -z "$set_name" ]]; then if nx_curl "${UTILS_ENDPOINT}" --get --data-urlencode "fn=tls" --data-urlencode "option=all"; then : else local rc=$? echo "ERROR: failed to fetch cert list (exit ${rc})." >&2 return "$rc" fi return 0 fi if nx_curl "${UTILS_ENDPOINT}" --get --data-urlencode "fn=tls" --data-urlencode "option=set" --data-urlencode "set=${set_name}"; then : else local rc=$? echo "ERROR: failed to fetch cert set (exit ${rc})." >&2 return "$rc" fi ;; expire) if [[ -z "$set_name" ]]; then echo "ERROR: tls expire requires a set name." >&2 return 1 fi local tmp out tmp="$(tls_fetch_xml)" || return $? out="$(nx_mktemp "nxios.tls.expire.XXXXXX.xml")" if ! python - "$tmp" "$set_name" "$out" <<'PY' import sys import xml.etree.ElementTree as ET src, name, dest = sys.argv[1], sys.argv[2], sys.argv[3] tree = ET.parse(src) root = tree.getroot() certs = root.find('Certificates') if certs is None: print("ERROR: missing.", file=sys.stderr) sys.exit(2) target = None for cert in certs.findall('Certificate'): if (cert.get('name') or '').strip() == name: target = cert break if target is None: print(f"ERROR: set not found: {name}", file=sys.stderr) sys.exit(3) target.set('renewAfter', 'now') tree.write(dest, encoding='utf-8', xml_declaration=True) PY then local rc=$? rm -f "$tmp" "$out" return "$rc" fi rm -f "$tmp" cmd_push "--path:tls" "--file:${out}" "--name=san-mappings.xml" -o local rc=$? rm -f "$out" return "$rc" ;; install) if [[ -z "$set_name" || -z "$target" ]]; then echo "ERROR: tls install requires set name and --target." >&2 return 1 fi local base="tls-sets,${set_name},scripts" local file="$target" if [[ "$file" != *.sh && "$file" != *.ps1 ]]; then file="${file}.sh" fi if [[ "$file" == *.sh ]]; then cmd_get_like "exec" "--path:${base}" "--file:${file}" return $? fi if [[ "$file" == *.ps1 ]]; then local tmp tmp="$(nx_mktemp "nxios.tls.install.XXXXXX.ps1")" if nx_curl "$(nx_get_file_url "$base" "$file")" -o "$tmp"; then : else local rc=$? rm -f "$tmp" return "$rc" fi if command -v pwsh >/dev/null 2>&1; then pwsh -NoProfile -File "$tmp" elif command -v powershell >/dev/null 2>&1; then powershell -NoProfile -File "$tmp" else echo "ERROR: no PowerShell found to run ${file}." >&2 rm -f "$tmp" return 1 fi local rc=$? rm -f "$tmp" return "$rc" fi ;; get-pem) if [[ -z "$set_name" ]]; then echo "ERROR: tls --get-pem requires a set name." >&2 return 1 fi local code="tls-sets,${set_name},pem" local target_dir="${dest_dir:-.}" target_dir="${target_dir/#\~/$HOME}" mkdir -p "$target_dir" local files if ! files="$(tls_list_files "$code")"; then echo "ERROR: failed to list PEM files for $set_name." >&2 return 1 fi if [[ "$as_zip" == "true" ]]; then local tmpDir tmpDir="$(nx_mktemp_dir "nxios.pem.${set_name}.XXXXXX.dir")" while IFS= read -r f; do [[ -z "$f" ]] && continue cmd_get_like "get" "--path:${code}" "--file:${f}" "--as:${tmpDir}/${f}" done <<< "$files" local out="${target_dir}/${set_name}.tar.gz" if ! tar -czf "$out" -C "$tmpDir" .; then local rc=$? rm -rf "$tmpDir" echo "ERROR: failed to create tar.gz (tar exit $rc)." >&2 return "$rc" fi rm -rf "$tmpDir" echo "Saved: $out" else while IFS= read -r f; do [[ -z "$f" ]] && continue cmd_get_like "get" "--path:${code}" "--file:${f}" "--as:${target_dir}/${f}" done <<< "$files" fi ;; get-pfx) if [[ -z "$set_name" ]]; then echo "ERROR: tls --get-pfx requires a set name." >&2 return 1 fi local code="tls-sets,${set_name}" local file="${set_name}.pfx" if [[ -n "$dest_file" ]]; then cmd_get_like "get" "--path:${code}" "--file:${file}" "--as:${dest_file}" else cmd_get_like "get" "--path:${code}" "--file:${file}" fi ;; *) echo "ERROR: tls requires --show, --expire, --install, --get-pem, or --get-pfx." >&2 return 1 ;; esac } # -------------------- VyOS helpers ----------------------------------- nx_is_vyos() { # Basic check: does this look like a VyOS system? [[ -f /opt/vyatta/etc/functions/script-template ]] } nx_vyos_dump_config_commands() { # Emit "set ..." style running configuration to stdout. if ! nx_is_vyos; then echo "[nxios vyos] Not a VyOS system (script-template missing)." >&2 return 1 fi if ! command -v sudo >/dev/null 2>&1; then echo "[nxios vyos] 'sudo' not found; cannot enter vbash." >&2 return 1 fi sudo /bin/vbash <<'EOF' # Dump running config as "set ..." commands, same as "show configuration commands" if command -v cli-shell-api >/dev/null 2>&1; then cli-shell-api showCfg --show-active-only | vyos-config-to-commands elif [ -x /usr/libexec/vyos/cli-shell-api ]; then /usr/libexec/vyos/cli-shell-api showCfg --show-active-only | vyos-config-to-commands else echo "[nxios vyos] ERROR: cli-shell-api not found on this system." >&2 exit 1 fi EOF } nx_vyos_apply_config_file() { # $1 = path to config file (either "set ..." commands OR config.boot-style) # $2 = action: compare | commit | save | confirm | commit-confirm # $3 = time: how long to delay on commit-confirm actions (in minutes, optional, default 15) local cfg_file="$1" local action="${2:-confirm}" local timer="${3:-3}" if ! nx_is_vyos; then echo "[nxios vyos] Not a VyOS system (script-template missing)." >&2 return 1 fi if [[ -z "$cfg_file" || ! -f "$cfg_file" ]]; then echo "[nxios vyos] Config file not found: $cfg_file" >&2 return 1 fi if ! command -v sudo >/dev/null 2>&1; then echo "[nxios vyos] 'sudo' not found; cannot enter vbash." >&2 return 1 fi case "$action" in ""|compare|commit|save|confirm|commit-confirm) ;; *) echo "[nxios vyos] Invalid action '$action' (use compare|commit|save|confirm|commit-confirm)." >&2 return 1 ;; esac # ---- Detect file format: commands vs config.boot ----------------------- # Grab first non-empty, non-comment line local first_nonempty first_nonempty="$(grep -m1 -E '^[[:space:]]*[^#[:space:]]' "$cfg_file" || true)" # Trim leading whitespace first_nonempty="${first_nonempty#"${first_nonempty%%[![:space:]]*}"}" local mode="config" if [[ "$first_nonempty" =~ ^(set|delete|comment)[[:space:]] ]]; then mode="commands" fi # Normalise timer: must be 30 >= integer >= 3 if ! [[ "$timer" =~ ^[0-9]+$ ]]; then timer=15 fi if (( "$timer" < 3 )); then echo "[nxios vyos] Invalid confirm timer given ($timer) normalizing to 3 minutes." timer=3 fi if (( timer > 30 )); then echo "[nxios vyos] Invalid confirm timer given ($timer) normalizing to 30 minutes." timer=30 fi # ---- Apply config inside vbash ----------------------------------------- sudo /bin/vbash <&2 source "$cfg_file" ;; config) echo "[nxios vyos] Treating '$cfg_file' as full config file (load)." >&2 load "$cfg_file" ;; esac case "$action" in compare) echo "[nxios vyos] Loaded config; running 'compare' only (no commit/save)." >&2 compare ;; commit) echo "[nxios vyos] Loaded config; committing (no save)." >&2 commit ;; commit-confirm|confirm) echo "[nxios vyos] Loaded config; committing (no save, $(timer)-minute confirmation)." >&2 commit-confirm "${timer}" ;; save) echo "[nxios vyos] Loaded config; committing and saving." >&2 commit save ;; esac exit EOF } # -------------------- push ------------------------------------------- # -------------------- chunked upload -------------------------------- # Convert a PHP ini-style size string (e.g. "32M", "512K", "2G") to bytes. # Bare integers are returned as-is. Returns 0 on parse failure. nxios_parse_php_size() { local val="${1:-}" val="${val// /}" # strip spaces [[ -z "$val" ]] && { echo 0; return; } # Match optional leading digits + optional suffix if [[ "$val" =~ ^([0-9]+)([kKmMgG]?)$ ]]; then local n="${BASH_REMATCH[1]}" local u="${BASH_REMATCH[2],,}" # lowercase suffix case "$u" in k) echo $(( n * 1024 )) ;; m) echo $(( n * 1024 * 1024 )) ;; g) echo $(( n * 1024 * 1024 * 1024 )) ;; *) echo "$n" ;; esac else echo 0 fi } # Compute the client-side chunk size: 80% of min(php_file_max, php_post_max). # Returns 0 if either limit is unknown/zero (callers should fall back to single upload). nxios_chunk_size() { local file_max post_max file_max="$(nxios_parse_php_size "${NXIOS_PHP_FILE_MAX:-}")" post_max="$(nxios_parse_php_size "${NXIOS_PHP_POST_MAX:-}")" # Both must be known and > 0 if [[ "$file_max" -le 0 || "$post_max" -le 0 ]]; then echo 0 return fi local limit if [[ "$file_max" -le "$post_max" ]]; then limit="$file_max" else limit="$post_max" fi # 80% of the lower limit, floored at 1 MiB local size=$(( limit * 80 / 100 )) if [[ "$size" -lt $(( 1024 * 1024 )) ]]; then size=$(( 1024 * 1024 )) fi echo "$size" } # nx_chunked_upload FILEPATH CODE NAME OVERWRITE # Implements the server-directed chunked upload protocol against chunk-upload.php. # Each response contains an 'action' directive; we follow it until FINALIZE or ABORT. # Requires: sha256sum or shasum or openssl (for per-chunk hashing). nx_chunked_upload() { local FILEPATH="$1" local CODE="$2" local NAME="$3" local OVERWRITE="${4:-false}" local file_size meter_type="--nxios-bar" # ── prerequisites ────────────────────────────────────────────────── local hash_cmd="" upload_started_at="${SECONDS:-0}" if command -v sha256sum >/dev/null 2>&1; then hash_cmd="sha256sum" elif command -v shasum >/dev/null 2>&1; then hash_cmd="shasum -a 256" elif command -v openssl >/dev/null 2>&1; then hash_cmd="openssl dgst -sha256" # output: "SHA256(file)= hexhash" else echo "ERROR: chunked upload requires sha256sum / shasum / openssl for chunk hashing." >&2 return 1 fi nx_sha256_of() { local f="$1" local out case "$hash_cmd" in sha256sum) out="$(sha256sum "$f" | awk '{print $1}')" ;; "shasum -a 256") out="$(shasum -a 256 "$f" | awk '{print $1}')" ;; *) out="$(openssl dgst -sha256 "$f" | awk '{print $NF}')" ;; esac printf '%s' "${out,,}" # lowercase } # ── sizes ────────────────────────────────────────────────────────── file_size="$(wc -c < "$FILEPATH" | tr -d '[:space:]')" local file_sha256 echo " Computing SHA256 of $(basename "$FILEPATH")..." >&2 file_sha256="$(nx_sha256_of "$FILEPATH")" # Client-side chunk size (server will send its own authoritative value in open response) local client_chunk_size client_chunk_size="$(nxios_chunk_size)" if [[ "$client_chunk_size" -le 0 ]]; then client_chunk_size=$(( 16 * 1024 * 1024 )) # 16 MiB safe fallback fi local endpoint="$CHUNK_ENDPOINT" if [[ "$OVERWRITE" == "true" ]]; then endpoint="${endpoint}?overwrite=true" fi # Generate a session_id (32 hex chars) local session_id session_id="$(dd if=/dev/urandom bs=16 count=1 2>/dev/null | od -A n -t x1 | tr -d ' \n')" # Fallback if od not available [[ -z "$session_id" ]] && session_id="$(date +%s%N | sha256sum 2>/dev/null | cut -c1-32 || date +%s | sha256sum | cut -c1-32)" echo " File: $(basename "$FILEPATH") (${file_size} bytes)" >&2 echo " SHA256: ${file_sha256}" >&2 echo " Session: ${session_id}" >&2 echo " Endpoint: ${endpoint}" >&2 # ── OPEN ─────────────────────────────────────────────────────────── echo " [chunk] Opening upload session..." >&2 local open_resp if ! open_resp="$(nx_curl \ -X POST \ -F "action=open" \ -F "path=${CODE}" \ -F "name=${NAME}" \ -F "file_size=${file_size}" \ -F "file_sha256=${file_sha256}" \ -F "session_id=${session_id}" \ "$endpoint")"; then echo "ERROR: chunk-upload open failed." >&2 return 1 fi # Parse the small JSON payloads returned by chunk-upload.php without jq. # Important: action fields must be parsed from the nested "action" object, # not from the whole response blob, otherwise top-level numbers can be # mistaken for action.length / action.offset / etc. _nxup_json_str() { local key="$1" json="$2" grep -oE "\"${key}\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" <<<"$json" \ | head -1 \ | sed -E 's/^.*:[[:space:]]*"([^"]*)"$/\1/' } _nxup_json_num() { local key="$1" json="$2" grep -oE "\"${key}\"[[:space:]]*:[[:space:]]*[0-9]+" <<<"$json" \ | head -1 \ | grep -oE '[0-9]+' } _nxup_json_bool() { local key="$1" json="$2" grep -oE "\"${key}\"[[:space:]]*:[[:space:]]*(true|false)" <<<"$json" \ | head -1 \ | sed -E 's/^.*:[[:space:]]*(true|false)$/\1/' } _nxup_json_action_obj() { local json="$1" grep -oE '"action"[[:space:]]*:[[:space:]]*\{[^}]*\}' <<<"$json" \ | head -1 \ | sed -E 's/^"action"[[:space:]]*:[[:space:]]*\{(.*)\}$/\1/' } local upload_id action_type action_json local chunk_index chunk_offset chunk_length lease_token action_json="$(_nxup_json_action_obj "$open_resp")" upload_id="$(_nxup_json_str "upload_id" "$open_resp")" action_type="$(_nxup_json_str "type" "$action_json")" # Server-authoritative chunk size (use it from here on) local server_chunk_size server_chunk_size="$(_nxup_json_num "chunk_size" "$open_resp")" [[ -z "$server_chunk_size" || "$server_chunk_size" -le 0 ]] && server_chunk_size="$client_chunk_size" local total_chunks total_chunks="$(_nxup_json_num "total_chunks" "$open_resp")" if [[ -z "$upload_id" ]]; then echo "ERROR: chunk-upload open returned no upload_id. Response: $open_resp" >&2 return 1 fi echo " [chunk] upload_id=${upload_id}" >&2 echo " [chunk] total_chunks=${total_chunks}, server_chunk_size=${server_chunk_size}" >&2 # ── CHUNK LOOP ───────────────────────────────────────────────────── # Follow server action directives until FINALIZE or ABORT. # After open, subsequent SEND directives come from chunk responses. local current_resp="$open_resp" local loop_limit=$(( total_chunks + 10 )) # safety ceiling local iterations=0 local bytes_dispatched=0 # running total of bytes sent so far (for progress display) while true; do (( iterations++ )) || true if [[ "$iterations" -gt "$loop_limit" ]]; then echo "ERROR: chunk loop exceeded safety limit (${loop_limit} iterations)." >&2 return 1 fi # Extract action fields from current response action_json="$(_nxup_json_action_obj "$current_resp")" action_type="$(_nxup_json_str "type" "$action_json")" case "$action_type" in FINALIZE) # ── FINALIZE ─────────────────────────────────────────── printf "\n" >&2 # end the \r progress line cleanly local elapsed=$(( ${SECONDS:-0} - upload_started_at )) printf " [chunk] All chunks received, finalizing... [in %02d:%02d]\n" \ "$(( elapsed / 60 ))" "$(( elapsed % 60 ))" >&2 local fin_resp if ! fin_resp="$(nx_curl \ -X POST \ -F "action=finalize" \ -F "upload_id=${upload_id}" \ -F "session_id=${session_id}" \ "$endpoint")"; then echo "ERROR: chunk-upload finalize failed." >&2 return 1 fi local fin_ok published_name fin_ok="$(_nxup_json_bool "ok" "$fin_resp")" published_name="$(_nxup_json_str "published_name" "$fin_resp")" if [[ "$fin_ok" != "true" ]]; then local fin_err fin_err="$(_nxup_json_str "error" "$fin_resp")" echo "ERROR: finalize failed: ${fin_err}. Response: ${fin_resp}" >&2 return 1 fi local resolved_dest final_name="${published_name:-$NAME}" resolved_dest="$(decode_codeword_to_webpath "$CODE")" if [[ -n "$resolved_dest" ]]; then echo " [chunk] Finalized. Published to: https://nxios.ca/${resolved_dest#/}/${final_name}" >&2 else echo " [chunk] Finalized. Recoverable via: nxios get --path:${CODE} --file:${final_name}" >&2 echo " or (for folders): nxios get --path:${CODE} --folder:${final_name} --as:[destination folder]" >&2 fi return 0 ;; ABORT) printf "\n" >&2 local abort_reason abort_reason="$(_nxup_json_str "reason" "$current_resp")" echo "ERROR: server aborted chunked upload: ${abort_reason}" >&2 return 1 ;; SEND) # ── extract SEND fields ──────────────────────────────── chunk_index="$(_nxup_json_num "index" "$action_json")" chunk_offset="$(_nxup_json_num "offset" "$action_json")" chunk_length="$(_nxup_json_num "length" "$action_json")" lease_token="$(_nxup_json_str "lease_token" "$action_json")" if [[ -z "$chunk_index" || -z "$chunk_offset" || -z "$chunk_length" || -z "$lease_token" ]]; then printf "\n" >&2 echo "ERROR: invalid SEND directive from server." >&2 echo " Response: ${current_resp}" >&2 return 1 fi if (( chunk_length <= 0 )); then printf "\n" >&2 echo "ERROR: server requested a non-positive chunk length (${chunk_length})." >&2 echo " Response: ${current_resp}" >&2 return 1 fi if (( chunk_offset < 0 || chunk_offset + chunk_length > file_size )); then printf "\n" >&2 echo "ERROR: server requested an out-of-range chunk (offset=${chunk_offset}, length=${chunk_length}, file_size=${file_size})." >&2 echo " Response: ${current_resp}" >&2 return 1 fi # ── progress display ────────────────────────────────── # Carve the exact chunk from the source file local chunk_tmp chunk_started_at="${SECONDS:-0}" chunk_tmp="$(nx_mktemp "nxios.chunk.XXXXXX")" #dd if="$FILEPATH" of="$chunk_tmp" \ # bs=1 skip="$chunk_offset" count="$chunk_length" 2>/dev/null dd if="$FILEPATH" of="$chunk_tmp" \ bs=64K iflag=skip_bytes,count_bytes \ skip="$chunk_offset" count="$chunk_length" 2>/dev/null local chunk_actual_size chunk_actual_size=$(stat -c %s "$chunk_tmp" 2>/dev/null || echo 0) if [[ "${NXIOS_DEBUG:-0}" == "1" ]]; then printf "\n [chunk] temp file: %s (%s bytes, expected %s)\n" \ "$(basename "$chunk_tmp")" "$chunk_actual_size" "$chunk_length" >&2 fi #if [[ "$chunk_actual_size" != "$chunk_length" ]]; then if (( chunk_actual_size != chunk_length )); then printf "\n" >&2 echo "ERROR: extracted chunk size mismatch (actual=${chunk_actual_size}, expected=${chunk_length})." >&2 rm -f "$chunk_tmp" return 1 fi if [[ ! -s "$chunk_tmp" ]]; then echo "ERROR: failed to extract chunk ${chunk_index} from file." >&2 rm -f "$chunk_tmp" return 1 fi local chunk_sha256 chunk_sha256="$(nx_sha256_of "$chunk_tmp")" # Show progress for the chunk about to be processed/uploaded. # We later erase this line plus curl's bar and replace them with a # finalized one-line summary including elapsed wall-clock time. local bytes_after_this=$(( bytes_dispatched + chunk_length )) local pct=$(( bytes_after_this * 100 / file_size )) # Format sizes as MiB for readability (awk keeps it dependency-free) local disp_done disp_total disp_done="$(awk "BEGIN{printf \"%.1f\", ${bytes_after_this}/1048576}")" disp_total="$(awk "BEGIN{printf \"%.1f\", ${file_size}/1048576}")" printf " [chunk] %s MiB / %s MiB (%3d%%) - chunk %d/%d...\n" \ "$disp_done" "$disp_total" "$pct" \ "$(( chunk_index + 1 ))" "$total_chunks" >&2 local chunk_resp if ! chunk_resp="$(nx_curl "$meter_type" \ -X POST \ -F "action=chunk" \ -F "upload_id=${upload_id}" \ -F "session_id=${session_id}" \ -F "lease_token=${lease_token}" \ -F "chunk_index=${chunk_index}" \ -F "chunk_sha256=${chunk_sha256}" \ -F "chunk=@${chunk_tmp};type=application/octet-stream" \ "$endpoint")"; then #printf "\n" >&2 if [[ -t 2 ]]; then printf '\033[1A\r\033[2K' >&2 printf '\033[1A\r\033[2K' >&2 fi echo "ERROR: chunk POST failed for chunk ${chunk_index}." >&2 rm -f "$chunk_tmp" return 1 fi rm -f "$chunk_tmp" local chunk_elapsed=$(( ${SECONDS:-0} - chunk_started_at )) # Erase curl's bar line + the original chunk announcement line if [[ -t 2 ]]; then printf '\033[1A\r\033[2K\033[1A\r\033[2K' >&2 fi local mib=$((1024 * 1024)) # Chunk size display: "<1" for sub-1 MiB, otherwise width-2 integer MiB local chunk_size_str if (( chunk_length < mib )); then chunk_size_str="<1" else printf -v chunk_size_str "%2d" $(( (chunk_length + mib - 1) / mib )) fi # Elapsed time display: "15s" or "1m 04s" local mins=$(( chunk_elapsed / 60 )) local secs=$(( chunk_elapsed % 60 )) local time_str if (( mins > 0 )); then printf -v time_str "%dm %02ds" "$mins" "$secs" else printf -v time_str "%02ds" "$secs" fi printf " [chunk] %s MiB / %s MiB (%3d%%) - chunk %d/%d... [%s MiB in %s]\n" \ "$disp_done" "$disp_total" "$pct" \ "$(( chunk_index + 1 ))" "$total_chunks" \ "$chunk_size_str" "$time_str" >&2 # Advance the byte counter now that the chunk was successfully delivered bytes_dispatched=$(( bytes_dispatched + chunk_length )) # Check for server-side ok; on SHA mismatch the server resends SEND for same chunk local chunk_ok chunk_ok="$(_nxup_json_bool "ok" "$chunk_resp")" if [[ "$chunk_ok" != "true" ]]; then local chunk_err chunk_err="$(_nxup_json_str "error" "$chunk_resp")" printf "\n" >&2 echo " [chunk] Server returned error for chunk ${chunk_index}: ${chunk_err}" >&2 # The server includes a corrected action directive; follow it fi current_resp="$chunk_resp" ;; *) echo "" >&2 echo "ERROR: unexpected action type '${action_type}' from server." >&2 echo " Response: ${current_resp}" >&2 return 1 ;; esac done } cmd_push() { local CODE="" FILEPATH="" NAME="" DOTFILE="" local OVERWRITE="false" local FOLDER="" TAR_TEMP="" if [[ $# -eq 0 ]]; then usage; fi while [[ $# -gt 0 ]]; do case "$1" in --path=*) CODE="${1#--path=}" ;; --path:*) CODE="${1#--path:}" ;; -p=*) CODE="${1#-p=}" ;; -p:*) CODE="${1#-p:}" ;; --file:*) FILEPATH="${1#--file:}" ;; --file=*) FILEPATH="${1#--file=}" ;; -f:*) FILEPATH="${1#-f:}" ;; -f=*) FILEPATH="${1#-f=}" ;; --dotfile:*) DOTFILE="${1#--dotfile:}" ;; --dotfile=*) DOTFILE="${1#--dotfile=}" ;; --folder=*) FOLDER="${1#--folder=}" ;; --folder:*) FOLDER="${1#--folder:}" ;; --name=*|--as=*) NAME="${1#--*=}" ;; --as:*) NAME="${1#--as:}" ;; --to:*) NAME="${1#--to:}" ;; # Overwrite flags -o|--overwrite) OVERWRITE="true" ;; -o=*|--overwrite=*) OVERWRITE="$(parse_bool "${1#*=}")" ;; --help|-h) usage ;; *) echo "Unknown argument to push: $1" >&2; usage ;; esac shift done # Default path if not specified / empty ensure_path_default CODE CODE="$(nx_normalize_codeword_host "$CODE")" # Folder mode: --folder is mutually exclusive with --file / --dotfile if [[ -n "$FOLDER" ]]; then if [[ -n "$DOTFILE" || -n "$FILEPATH" ]]; then echo "ERROR: use either --folder: or --file:/--dotfile:, not both." >&2 exit 1 fi # Expand ~ and normalise local folderPath="$FOLDER" folderPath="${folderPath/#\~/$HOME}" if [[ ! -d "$folderPath" ]]; then echo "ERROR: folder not found or not a directory: $folderPath" >&2 exit 1 fi local base base="$(basename "$folderPath")" if [[ -z "$base" ]]; then echo "ERROR: could not derive folder base name from '$folderPath'." >&2 exit 1 fi # Create temp archive TAR_TEMP="$(nx_mktemp "${base}.XXXXXX.tar.gz")" echo "Creating archive from folder:" echo " Folder: $folderPath" echo " Archive: $TAR_TEMP" # Archive contents only (no extra parent dir) if ! tar -czf "$TAR_TEMP" -C "$folderPath" .; then local rc=$? echo "ERROR: failed to create archive from '$folderPath' (tar exit $rc)." >&2 rm -f "$TAR_TEMP" exit "$rc" fi FILEPATH="$TAR_TEMP" # If remote name not explicitly set, use .tar.gz if [[ -z "$NAME" ]]; then NAME="${base}.tar.gz" fi fi if [[ -n "$DOTFILE" && -n "$FILEPATH" ]]; then echo "ERROR: use either --file: or --dotfile:, not both." >&2 exit 1 fi if [[ -n "$DOTFILE" && -n "$NAME" ]]; then echo "ERROR: --dotfile: and --name= cannot be combined." >&2 exit 1 fi if [[ -n "$DOTFILE" ]]; then local df="$DOTFILE" base if [[ "$df" == /* || "$df" == ~* || "$df" == *"/"* ]]; then FILEPATH="${df/#\~/$HOME}" else base="$df" base="${base#.}" FILEPATH="$HOME/.${base}" fi base="$(basename "$FILEPATH")" base="${base#.}" if [[ -z "$base" ]]; then echo "ERROR: could not derive dotfile base name from '$FILEPATH'." >&2 exit 1 fi NAME="dot-${base}" else if [[ -z "$FILEPATH" ]]; then echo "ERROR: --file:PATH is required for push (or use --dotfile:NAME)." >&2 usage fi FILEPATH="${FILEPATH/#\~/$HOME}" if [[ -z "$NAME" ]]; then NAME="$(basename "$FILEPATH")" fi fi if [[ ! -f "$FILEPATH" ]]; then echo "ERROR: file not found: $FILEPATH" >&2 exit 1 fi ensure_path_default CODE CODE="$(nx_normalize_codeword_host "$CODE")" echo "Uploading:" echo " Local file: $FILEPATH" echo " Path: $CODE" echo " Remote name: $NAME" # ── upload method selection ──────────────────────────────────────── # Roots must be loaded so NXIOS_START_CHUNKING / PHP limits are populated. load_roots local file_size_bytes file_size_bytes="$(wc -c < "$FILEPATH" | tr -d '[:space:]')" local use_chunked=0 if [[ "${NXIOS_START_CHUNKING:-0}" -gt 0 && "$file_size_bytes" -gt "$NXIOS_START_CHUNKING" ]]; then use_chunked=1 fi if [[ "$use_chunked" -eq 1 ]]; then # ── chunked path ────────────────────────────────────────────── echo " Method: chunked (${file_size_bytes} bytes > threshold ${NXIOS_START_CHUNKING})" echo if nx_chunked_upload "$FILEPATH" "$CODE" "$NAME" "$OVERWRITE"; then echo echo "1 file(s) uploaded." else local rc=$? echo echo "Chunked upload failed." >&2 if [[ -n "${TAR_TEMP}" && -f "${TAR_TEMP}" ]]; then rm -f "${TAR_TEMP}" fi exit "$rc" fi else # ── single-shot path (original behaviour) ───────────────────── local endpoint="$UPLOAD_ENDPOINT" if [[ "$OVERWRITE" == "true" ]]; then endpoint="${endpoint}?overwrite=true" fi echo " Method: single-shot" echo " Endpoint: $endpoint" echo if nx_curl \ -F "file=@${FILEPATH}" \ -F "code=${CODE}" \ -F "name=${NAME}" \ "$endpoint" then echo echo "1 file(s) uploaded." else local rc=$? echo echo "Upload failed (curl exit code $rc)." >&2 echo "URL: $endpoint" if [[ -n "${TAR_TEMP}" && -f "${TAR_TEMP}" ]]; then rm -f "${TAR_TEMP}" fi exit "$rc" fi fi # Clean up temp archive in folder mode (on success) if [[ -n "${TAR_TEMP}" && -f "${TAR_TEMP}" ]]; then rm -f "${TAR_TEMP}" fi } # -------------------- get / install / exec --------------------------- cmd_get_like() { local mode="$1"; shift local CODE="" FILE="" DOTFILE="" FOLDER="" local OVERWRITE="false" local SAVE_AS="" AS_MAN="false" HASH_ONLY="false" local SELF_MODE="false" SELF_NO_MAN="false" local -a EXTRA_ARGS=() local PARAM_BLOB="" while [[ $# -gt 0 ]]; do case "$1" in --path=*) CODE="${1#--path=}" ;; --path:*) CODE="${1#--path:}" ;; -p=*) CODE="${1#-p=}" ;; -p:*) CODE="${1#-p:}" ;; --file:*) FILE="${1#--file:}" ;; --file=*) FILE="${1#--file=}" ;; -f:*) FILE="${1#-f:}" ;; -f=*) FILE="${1#-f=}" ;; # Folder download --folder=*) FOLDER="${1#--folder=}" ;; --folder:*) FOLDER="${1#--folder:}" ;; # Local save-as override (get/pull only) --as=*) SAVE_AS="${1#--as=}" ;; --as:*) SAVE_AS="${1#--as:}" ;; --to=*) SAVE_AS="${1#--to=}" ;; --to:*) SAVE_AS="${1#--to:}" ;; --asMan|--as-man|--asman) AS_MAN="true" ;; --hashonly|--hash-only|-h) HASH_ONLY="true" ;; # Canonical dotfile flag --dotfile:*) DOTFILE="${1#--dotfile:}" ;; --dotfile=*) DOTFILE="${1#--dotfile=}" ;; --params:*) PARAM_BLOB="${1#--params:}" ;; --params=*) PARAM_BLOB="${1#--params=}" ;; --self) SELF_MODE="true" ;; --self:noman|--self=noman) SELF_MODE="true" SELF_NO_MAN="true" ;; # Local overwrite control for get/install/exec -o|--overwrite) OVERWRITE="true" ;; -o=*|--overwrite=*) OVERWRITE="$(parse_bool "${1#*=}")" ;; --help) usage ;; --) shift EXTRA_ARGS=("$@") break ;; *) echo "Unknown argument to $mode: $1" >&2 usage ;; esac shift done # Default path if not specified / empty ensure_path_default CODE CODE="$(nx_normalize_codeword_host "$CODE")" # Folder mode: only valid for "get"/"pull", and exclusive with file/dotfile if [[ -n "$FOLDER" ]]; then if [[ "$mode" != "get" ]]; then echo "ERROR: --folder is only supported for get/pull mode." >&2 exit 1 fi if [[ -n "$FILE" || -n "$DOTFILE" ]]; then echo "ERROR: use either --folder: or --file:/--dot-file:, not both." >&2 exit 1 fi fi if [[ -n "$FILE" && -n "$DOTFILE" ]]; then echo "ERROR: use either --file: or --dotfile:, not both." >&2 exit 1 fi if [[ -n "$SAVE_AS" && "$mode" != "get" ]]; then echo "ERROR: --as is only supported for get/pull mode." >&2 exit 1 fi if [[ "$AS_MAN" == "true" && "$mode" != "get" ]]; then echo "ERROR: --asMan is only supported for get/pull mode." >&2 exit 1 fi if [[ "$HASH_ONLY" == "true" && "$mode" != "get" ]]; then echo "ERROR: --hashonly is only supported for get/pull mode." >&2 exit 1 fi if [[ "$HASH_ONLY" == "true" && -n "$FOLDER" ]]; then echo "ERROR: --hashonly is not supported with --folder." >&2 exit 1 fi if [[ "$HASH_ONLY" == "true" && "$AS_MAN" == "true" ]]; then echo "ERROR: --hashonly is not supported with --asMan." >&2 exit 1 fi if [[ "$SELF_MODE" == "true" && "$mode" != "install" ]]; then echo "ERROR: --self is only supported for install mode." >&2 exit 1 fi if [[ -n "$PARAM_BLOB" ]]; then mapfile -t _parsed < <(parse_params_blob "$PARAM_BLOB") if (( ${#_parsed[@]} > 0 )); then EXTRA_ARGS+=("${_parsed[@]}") fi fi load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "$CODE")" local remote_name local_name # --- Folder mode ---------------------------------------------------- if [[ -n "$FOLDER" ]]; then local folderPath="$FOLDER" folderPath="${folderPath/#\~/$HOME}" local base base="$(basename "$folderPath")" if [[ -z "$base" ]]; then echo "ERROR: could not derive folder base name from '$folderPath'." >&2 exit 1 fi # Strip .tar.gz suffix if already present before appending it local base_noext="${base%.tar.gz}" remote_name="${base_noext}.tar.gz" # If --as is provided, use it as the extraction destination; # otherwise extract into a folder named after the archive (minus .tar.gz) local extractPath if [[ -n "$SAVE_AS" ]]; then extractPath="${SAVE_AS/#\~/$HOME}" else extractPath="$(dirname "$folderPath")/${base_noext}" fi # Temp archive file local tmpTar tmpTar="$(nx_mktemp "${base_noext}.XXXXXX.tar.gz")" local url url="$(nx_get_file_url "$CODE" "$remote_name")" echo "Requesting folder '${FOLDER}' from path '${CODE}'..." echo " Resolved path: ${WEB_PATH}" echo " Remote archive: ${remote_name}" echo " Temp archive: ${tmpTar}" echo " Extract to: ${extractPath}" echo " Overwrite: ${OVERWRITE}" if nx_curl "$url" -o "$tmpTar"; then : else local rc=$? echo "Download failed (curl exit code $rc)." >&2 rm -f "$tmpTar" exit "$rc" fi # Ensure local extraction folder exists mkdir -p "$extractPath" if [[ "$OVERWRITE" == "true" ]]; then # Simple case: just extract over the top if ! tar -xzf "$tmpTar" -C "$extractPath"; then local rc=$? echo "ERROR: failed to extract archive into '$extractPath' (tar exit $rc)." >&2 rm -f "$tmpTar" exit "$rc" fi else # Careful merge: # - extract into a temp dir # - copy over only missing or older files local tmpDir tmpDir="$(nx_mktemp_dir "${base_noext}.XXXXXX.dir")" if ! tar -xzf "$tmpTar" -C "$tmpDir"; then local rc=$? echo "ERROR: failed to extract archive into temp dir '$tmpDir' (tar exit $rc)." >&2 rm -f "$tmpTar" rm -rf "$tmpDir" exit "$rc" fi # Create directories in target (so we don't fight with cp) ( cd "$tmpDir" || exit 1 find . -type d -print0 ) | while IFS= read -r -d '' d; do [[ "$d" == "." ]] && continue mkdir -p "${extractPath}/${d#.}" done # Copy files: only if missing, or if archive is newer ( cd "$tmpDir" || exit 1 find . -type f -print0 ) | while IFS= read -r -d '' f; do local rel="${f#.}" # strip leading . local src="${tmpDir}${rel}" local dst="${extractPath}${rel}" # Ensure parent dir exists (belt & suspenders) mkdir -p "$(dirname "$dst")" if [[ -e "$dst" ]]; then # If destination is newer than source, keep it if [[ "$dst" -nt "$src" ]]; then # Existing file is newer; skip continue fi fi cp -p "$src" "$dst" done rm -rf "$tmpDir" fi rm -f "$tmpTar" echo "1 folder(s) downloaded into ${extractPath}." return 0 fi # --- Normal file / dotfile mode ------------------------------------ if [[ "$SELF_MODE" == "true" ]]; then FILE="nxios.sh" local SELF_PATH if SELF_PATH="$(readlink -f "$0" 2>/dev/null)"; then : elif SELF_PATH="$(realpath "$0" 2>/dev/null)"; then : else SELF_PATH="$0" fi SAVE_AS="$SELF_PATH" fi if [[ -n "$DOTFILE" ]]; then # Normalize dotfile: always request dot- and save as . local base="$DOTFILE" if [[ "$base" == dot-* ]]; then base="${base#dot-}" elif [[ "$base" == .* ]]; then base="${base#.}" fi remote_name="dot-${base}" local_name=".${base}" else if [[ -z "$FILE" ]]; then echo "ERROR: --file:NAME is required for $mode (or use --dot-file:NAME)." >&2 exit 1 fi remote_name="$FILE" local_name="$FILE" fi # Override local save name if requested (get/pull only) if [[ "$AS_MAN" == "true" ]]; then local_name="$(nx_mktemp "nxios.man.XXXXXX")" elif [[ -n "$SAVE_AS" && "$mode" == "get" ]]; then local_name="${SAVE_AS/#\~/$HOME}" # If a path component is present, create the directory if [[ "$local_name" == */* ]]; then mkdir -p "$(dirname "$local_name")" fi fi local hash_target="$local_name" local url local root="${CODE%%,*}" if [[ -n "$DOTFILE" ]]; then url="${UTILS_ENDPOINT}?fn=dfdl&code=${CODE}&name=${remote_name}" else if [[ "$mode" == "get" && "$root" == "smb" ]]; then url="${UTILS_ENDPOINT}?fn=smb&op=pull&name=${remote_name}" else url="${UTILS_ENDPOINT}?fn=get-file&code=${CODE}&name=${remote_name}" fi fi if [[ "$HASH_ONLY" == "true" ]]; then local hash_url="${url}&hashonly=1" local hash if ! hash="$(nx_curl "$hash_url")"; then local rc=$? echo "Hash request failed (curl exit code $rc)." >&2 exit "$rc" fi hash="${hash//$'\r'/}" hash="${hash//$'\n'/}" if [[ -z "$hash" ]]; then echo "Hash response was empty." >&2 exit 1 fi printf '%s\n' "$hash" return 0 fi echo "Requesting '${remote_name}' from path '${CODE}'..." echo "Resolved path: ${WEB_PATH}" local dl_size="" dl_bytes=0 if command -v curl >/dev/null 2>&1; then local -a head_args=(curl -sI) if [[ "${NXIOS_IP_FAMILY:-}" == "4" ]]; then head_args+=(-4) elif [[ "${NXIOS_IP_FAMILY:-}" == "6" ]]; then head_args+=(-6) fi dl_bytes="$("${head_args[@]}" "$url" 2>/dev/null | awk -F': ' 'tolower($1)=="content-length"{gsub("\r","",$2); print $2; exit}')" if [[ -n "$dl_bytes" && "$dl_bytes" =~ ^[0-9]+$ ]]; then dl_size=" (${dl_bytes} bytes)" else dl_bytes=0 dl_size="" fi fi echo "Downloading: ${url}${dl_size}" # Auto-enable progress meter for large files (>= 10MB), unless already set local _progress_override=0 if [[ "${NXIOS_PROGRESS:-0}" != "1" && "$dl_bytes" -ge 10485760 ]]; then NXIOS_PROGRESS=1 _progress_override=1 fi # --- Self-overwrite-safe download path ---------------------------- # Resolve our own script path local SELF TARGET_RES if SELF="$(readlink -f "$0" 2>/dev/null)"; then : elif SELF="$(realpath "$0" 2>/dev/null)"; then : else SELF="$0" fi # Resolve the intended target path, if possible if TARGET_RES="$(readlink -f "$local_name" 2>/dev/null)"; then : elif TARGET_RES="$(realpath "$local_name" 2>/dev/null)"; then : else TARGET_RES="$local_name" fi if [[ "$TARGET_RES" == "$SELF" ]]; then # We are overwriting ourselves; do temp + move local TMP="${SELF}.tmp.$$" echo "(Self-update-style download: writing to ${TMP} then replacing ${SELF})" if nx_curl "$url" -o "$TMP"; then : else local rc=$? echo "Download failed (curl exit code $rc)." >&2 rm -f "$TMP" exit "$rc" fi case "$local_name" in *.sh|*.zsh) normalize_shell_script "$TMP" ;; esac if ! mv "$TMP" "$SELF"; then local rc=$? echo "Failed to replace ${SELF} with new version." >&2 rm -f "$TMP" exit "$rc" fi else # Normal case: download to the requested file # Special-case: ANY dotfile in "get" mode, when NOT overwriting. if [[ "$mode" == "get" && -n "$DOTFILE" && "$OVERWRITE" != "true" ]]; then local TMP MERGED TMP="$(nx_mktemp "nxios.dotfile.tmp.XXXXXX")" echo "(Merging downloaded dotfile with existing, if any — use -o to overwrite instead.)" if nx_curl "$url" -o "$TMP"; then : else local rc=$? echo "Download failed (curl exit code $rc)." >&2 rm -f "$TMP" exit "$rc" fi # Expand ~ in local path if present local target_local="${local_name/#\~/$HOME}" hash_target="$target_local" if [[ -f "$target_local" ]]; then # Remote defaults first, local config second (local wins) MERGED="$(nx_mktemp "nxios.dotfile.merged.XXXXXX")" cat "$TMP" "$target_local" > "$MERGED" mv "$MERGED" "$target_local" rm -f "$TMP" else mv "$TMP" "$target_local" fi else # Non-dotfile OR overwrite OR non-get modes: normal behavior if nx_curl "$url" -o "$local_name"; then : else local rc=$? echo "Download failed (curl exit code $rc)." >&2 exit "$rc" fi fi case "$mode" in install|exec) case "$local_name" in *.sh|*.zsh) normalize_shell_script "$local_name" ;; esac ;; esac fi # Restore progress setting if we auto-enabled it [[ "$_progress_override" -eq 1 ]] && NXIOS_PROGRESS=0 local save_line="Saving as: ${local_name}" if [[ "$mode" == "get" ]]; then local hash_value="" if hash_value="$(nx_sha256_file "$hash_target")"; then save_line+=" (Hash: ${hash_value})" else save_line+=" (Hash: unavailable)" fi fi echo "$save_line" if [[ "$AS_MAN" == "true" ]]; then local man_name="$remote_name" [[ "$man_name" != *.1 ]] && man_name="${man_name}.1" local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" $sudo_cmd install -d "/usr/local/share/man/man1" || true $sudo_cmd install -m 644 "$local_name" "/usr/local/share/man/man1/${man_name}" || { echo "ERROR: failed to install man page." >&2 rm -f "$local_name" exit 1 } command -v mandb >/dev/null 2>&1 && $sudo_cmd mandb >/dev/null 2>&1 || true rm -f "$local_name" echo "Installed man page: ${man_name}" return 0 fi if [[ "$SELF_MODE" == "true" && "$SELF_NO_MAN" != "true" ]]; then cmd_get_like "get" --path:scripts --file:nxios.1 --asMan fi case "$mode" in get) echo "1 file(s) downloaded." ;; install) echo "1 file(s) downloaded and marked executable." ;; exec) echo "1 file(s) downloaded; executing..." "./$local_name" "${EXTRA_ARGS[@]}" rm -f "./$local_name" # delete the script after ;; esac } # -------------------- ls / dir --------------------------------------- cmd_ls() { local PATH_CODE="" FOLDER="" while [[ $# -gt 0 ]]; do case "$1" in --path=*) PATH_CODE="${1#--path=}" ;; --path:*) PATH_CODE="${1#--path:}" ;; -p=*) PATH_CODE="${1#-p=}" ;; -p:*) PATH_CODE="${1#-p:}" ;; --folder=*) FOLDER="${1#--folder=}" ;; --folder:*) FOLDER="${1#--folder:}" ;; --help|-h) echo "Usage:" >&2 echo " nxios ls|dir [PATH]" >&2 echo " nxios ls|dir --path=PATH" >&2 echo " nxios ls|dir --path=PATH --folder=NAME # list archive contents (NAME.tar.gz)" >&2 echo >&2 echo "If PATH is omitted, the first root alias from the server is used." >&2 return 0 ;; *) if [[ -z "$PATH_CODE" ]]; then PATH_CODE="$1" else echo "Unknown argument to ls/dir: $1" >&2 echo "Usage: nxios ls|dir [PATH] or nxios ls|dir --path=PATH [--folder=NAME]" >&2 return 1 fi ;; esac shift done ensure_path_default PATH_CODE PATH_CODE="$(nx_normalize_codeword_host "$PATH_CODE")" # --- Folder/Archive mode ------------------------------------------- if [[ -n "$FOLDER" ]]; then # We interpret FOLDER as the snapshot name; archive is .tar.gz load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "$PATH_CODE")" local base remote_name tmpTar url base="${FOLDER##*/}" if [[ -z "$base" ]]; then echo "ERROR: could not derive folder base name from '$FOLDER'." >&2 return 1 fi remote_name="${base}.tar.gz" tmpTar="$(nx_mktemp "${base}.XXXXXX.tar.gz")" url="$(nx_get_file_url "$PATH_CODE" "$remote_name")" echo "Archive listing for '${FOLDER}' in path '${PATH_CODE}':" echo " Resolved path: ${WEB_PATH}" echo " Remote file: ${remote_name}" echo " URL: ${url}" echo if nx_curl "$url" -o "$tmpTar"; then : else local rc=$? echo "Failed to download archive (curl exit code $rc)." >&2 rm -f "$tmpTar" return "$rc" fi # List contents of the archive if ! tar -tzf "$tmpTar"; then local rc=$? echo "Failed to list archive contents (tar exit code $rc)." >&2 rm -f "$tmpTar" return "$rc" fi rm -f "$tmpTar" return 0 fi # --- Normal remote directory listing (existing behaviour) ---------- echo "Directory listing for path '${PATH_CODE}':" echo if nx_curl "${DIR_ENDPOINT}?code=${PATH_CODE}"; then : else local rc=$? echo "Failed to fetch directory listing (curl exit code $rc)." >&2 return "$rc" fi } # -------------------- del/rm ----------------------------------------- cmd_rm() { local CODE="" NAME="" DOTFILE="" if [[ $# -eq 0 ]]; then usage fi while [[ $# -gt 0 ]]; do case "$1" in --path=*) CODE="${1#--path=}" ;; --path:*) CODE="${1#--path:}" ;; -p=*) CODE="${1#-p=}" ;; -p:*) CODE="${1#-p:}" ;; --file=*) NAME="${1#--file=}" ;; --file:*) NAME="${1#--file:}" ;; -f=*) NAME="${1#-f=}" ;; -f:*) NAME="${1#-f:}" ;; --name=*) NAME="${1#--name=}" ;; # optional alias --dotfile=*) DOTFILE="${1#--dotfile=}" ;; --dotfile:*) DOTFILE="${1#--dotfile:}" ;; --help|-h) usage ;; *) echo "Unknown argument to rm/del: $1" >&2; usage ;; esac shift done ensure_path_default CODE CODE="$(nx_normalize_codeword_host "$CODE")" if [[ -n "$NAME" && -n "$DOTFILE" ]]; then echo "ERROR: use either --file: / --name= or --dotfile:, not both." >&2 exit 1 fi if [[ -n "$DOTFILE" ]]; then local df="$DOTFILE" local base base="$(basename "$df")" base="${base#.}" if [[ "$base" == dot-* ]]; then NAME="$base" else NAME="dot-${base}" fi fi if [[ -z "$NAME" ]]; then echo "ERROR: you must specify a remote file name with --file:NAME/--name=NAME or a dotfile with --dotfile:NAME." >&2 usage fi echo "Deleting remote file:" echo " Path: $CODE" echo " Name: $NAME" echo " Endpoint: $DELETE_ENDPOINT" echo if nx_curl -X POST \ -d "code=${CODE}" \ -d "name=${NAME}" \ "$DELETE_ENDPOINT" then echo echo "1 file(s) deleted." else local rc=$? echo echo "Delete failed (curl exit code $rc)." >&2 exit "$rc" fi } # -------------------- rename/ren ------------------------------------- cmd_ren() { local CODE="" FROM="" TO="" DFROM="" DTO="" local OVERWRITE="false" if [[ $# -eq 0 ]]; then usage; fi while [[ $# -gt 0 ]]; do case "$1" in --path=*) CODE="${1#--path=}" ;; --path:*) CODE="${1#--path:}" ;; -p=*) CODE="${1#-p=}" ;; -p:*) CODE="${1#-p:}" ;; --from=*|--file=*) FROM="${1#--from=}" ;; --from:*|--file:*) FROM="${1#--from:}" ;; -f=*) FROM="${1#-f=}" ;; -f:*) FROM="${1#-f:}" ;; --to=*) TO="${1#--to=}" ;; --to:*) TO="${1#--to:}" ;; --as=*) TO="${1#--as=}" ;; --as:*) TO="${1#--as:}" ;; --dotfile-from=*) DFROM="${1#--dotfile-from=}" ;; --dotfile-from:*) DFROM="${1#--dotfile-from:}" ;; --df-from=*) DFROM="${1#--df-from=}" ;; --df-from:*) DFROM="${1#--df-from:}" ;; --dotfile-to=*) DTO="${1#--dotfile-to=}" ;; --dotfile-to:*) DTO="${1#--dotfile-to:}" ;; --df-to=*) DTO="${1#--df-to=}" ;; --df-to:*) DTO="${1#--df-to:}" ;; # Overwrite flags -o|--overwrite) OVERWRITE="true" ;; -o=*|--overwrite=*) OVERWRITE="$(parse_bool "${1#*=}")" ;; --help|-h) usage ;; *) echo "Unknown argument to ren: $1" >&2; usage ;; esac shift done ensure_path_default CODE CODE="$(nx_normalize_codeword_host "$CODE")" # Mutually exclusive input types if [[ -n "$FROM" && -n "$DFROM" ]]; then echo "ERROR: use either --from or --dotfile-from, not both." >&2 exit 1 fi if [[ -n "$TO" && -n "$DTO" ]]; then echo "ERROR: use either --to or --dotfile-to, not both." >&2 exit 1 fi # Convert dotfile-from if [[ -n "$DFROM" ]]; then local base="${DFROM##*/}" base="${base#.}" if [[ "$base" == dot-* ]]; then FROM="$base" else FROM="dot-${base}" fi fi # Convert dotfile-to if [[ -n "$DTO" ]]; then local base="${DTO##*/}" base="${base#.}" if [[ "$base" == dot-* ]]; then TO="$base" else TO="dot-${base}" fi fi if [[ -z "$FROM" || -z "$TO" ]]; then echo "ERROR: rename requires --from and --to (or dotfile equivalents)." >&2 usage fi # Decide endpoint URL with overwrite flag local endpoint="$RENAME_ENDPOINT" if [[ "$OVERWRITE" == "true" ]]; then endpoint="${endpoint}?overwrite=true" fi echo "Renaming remote file:" echo " Path: ${CODE}" echo " From: ${FROM}" echo " To: ${TO}" echo " Endpoint: ${endpoint}" echo if nx_curl -X POST \ -d "code=${CODE}" \ -d "from=${FROM}" \ -d "to=${TO}" \ "$endpoint" then echo echo "1 file(s) renamed." else local rc=$? echo echo "Rename failed (curl exit code $rc)." >&2 exit "$rc" fi } # -------------------- roots ------------------------------------------ cmd_show_roots() { echo "Fetching root/alias definitions from server..." echo local resp if ! resp="$(nx_curl "$ROOTS_ENDPOINT")"; then echo "Error: Failed to retrieve roots from server." return 1 fi if [[ -z "$resp" ]]; then echo "Error: Empty roots response from server." return 1 fi echo "Defined roots & aliases:" echo "----------------------------------------" printf "%s\n" "$resp" echo "----------------------------------------" echo } # -------------------- init ------------------------------------------- cmd_init() { # Hard-coded per your spec local TARGET_DIR="/home/brett/scripts" local TARGET_FILE="${TARGET_DIR}/nxios.sh" local ZSHRC="/home/brett/.zshrc" local alias_line='alias nxios="/home/brett/scripts/nxios.sh"' echo "NxIOS init:" echo " Target script path: ${TARGET_FILE}" echo " Zsh rc file: ${ZSHRC}" echo # --- Step 1: Move current nxios.sh into /home/brett/scripts --------- # Resolve our own script path (similar to logic you already use) local SELF if SELF="$(readlink -f "$0" 2>/dev/null)"; then : elif SELF="$(realpath "$0" 2>/dev/null)"; then : else SELF="$0" fi echo " Current script path: ${SELF}" local same_target_file="false" if [[ -e "$TARGET_FILE" && "$SELF" -ef "$TARGET_FILE" ]]; then same_target_file="true" fi if [[ "$SELF" != "$TARGET_FILE" && "$same_target_file" != "true" ]]; then echo " Installing nxios.sh into ${TARGET_DIR} ..." mkdir -p "$TARGET_DIR" if [[ -e "$TARGET_FILE" ]]; then echo " Note: ${TARGET_FILE} already exists and will be overwritten." fi mv "$SELF" "$TARGET_FILE" chmod +x "$TARGET_FILE" 2>/dev/null || true echo " Moved nxios.sh to ${TARGET_FILE}" else echo " nxios.sh is already in ${TARGET_DIR}" fi echo # --- Step 2: Ensure alias in /home/brett/.zshrc --------------------- if [[ ! -f "$ZSHRC" ]]; then echo " Creating ${ZSHRC} ..." touch "$ZSHRC" fi # Check if the exact alias line already exists if grep -Fqx "$alias_line" "$ZSHRC" 2>/dev/null; then echo " Alias already present in ${ZSHRC}:" echo " $alias_line" else echo " Adding alias to ${ZSHRC}:" echo " $alias_line" printf '\n%s\n' "$alias_line" >> "$ZSHRC" fi echo echo "Init complete." echo " - Future zsh sessions will have 'nxios' as an alias." echo " - To use it *right now* in an existing zsh session, run:" echo " source ${ZSHRC}" } # -------------------- diff ------------------------------------------- cmd_diff() { local CODE="" FILE="" DOTFILE="" LOCAL="" local -a EXTRA_ARGS=() if [[ $# -eq 0 ]]; then echo "Usage: nxios diff [--path=CODE] [--file:NAME | --dotfile:NAME] --name:LOCALPATH" >&2 exit 1 fi while [[ $# -gt 0 ]]; do case "$1" in --path=*) CODE="${1#--path=}" ;; --path:*) CODE="${1#--path:}" ;; -p=*) CODE="${1#-p=}" ;; -p:*) CODE="${1#-p:}" ;; --file:*) FILE="${1#--file:}" ;; --file=*) FILE="${1#--file=}" ;; -f=*) FILE="${1#-f=}" ;; -f:*) FILE="${1#-f:}" ;; --dotfile:*) DOTFILE="${1#--dotfile:}" ;; --dotfile=*) DOTFILE="${1#--dotfile=}" ;; --dot-file:*) DOTFILE="${1#--dot-file:}" ;; # legacy alias --dot-file=*) DOTFILE="${1#--dot-file=}" ;; --name=*) LOCAL="${1#--name=}" ;; --name:*) LOCAL="${1#--name:}" ;; --help|-h) echo "Usage: nxios diff [--path=CODE] [--file:NAME | --dotfile:NAME] --name:LOCALPATH" >&2 return 0 ;; *) echo "Unknown argument to diff: $1" >&2 echo "Usage: nxios diff [--path=CODE] [--file:NAME | --dotfile:NAME] --name:LOCALPATH" >&2 exit 1 ;; esac shift done ensure_path_default CODE CODE="$(nx_normalize_codeword_host "$CODE")" if [[ -z "$LOCAL" ]]; then echo "ERROR: --name:LOCALPATH is required for diff." >&2 exit 1 fi if [[ -n "$FILE" && -n "$DOTFILE" ]]; then echo "ERROR: use either --file: or --dotfile:, not both." >&2 exit 1 fi load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "$CODE")" local remote_name if [[ -n "$DOTFILE" ]]; then local base="${DOTFILE##*/}" base="${base#.}" # strip leading '.' if present if [[ "$base" == dot-* ]]; then remote_name="$base" base="${base#dot-}" else remote_name="dot-${base}" fi else if [[ -z "$FILE" ]]; then echo "ERROR: diff requires either --file:NAME or --dotfile:NAME." >&2 exit 1 fi remote_name="$FILE" fi local url if [[ -n "$DOTFILE" ]]; then url="${UTILS_ENDPOINT}?fn=dfdl&code=${CODE}&name=${remote_name}" else url="$(nx_get_file_url "$CODE" "$remote_name")" fi # Expand ~ in local path LOCAL="${LOCAL/#\~/$HOME}" if [[ ! -f "$LOCAL" ]]; then echo "ERROR: local file not found: $LOCAL" >&2 exit 1 fi local TMP TMP="$(nx_mktemp "nxios.diff.remote.XXXXXX")" echo "Fetching remote file for diff:" echo " Path: ${CODE}" echo " Remote: ${WEB_PATH}/${remote_name}" echo " URL: ${url}" echo " Local: ${LOCAL}" echo if nx_curl "$url" -o "$TMP"; then : else local rc=$? echo "Download failed (curl exit code $rc)." >&2 rm -f "$TMP" exit "$rc" fi echo "===== diff -u LOCAL REMOTE =====" echo "LOCAL : ${LOCAL}" echo "REMOTE: ${url}" echo diff -u "$LOCAL" "$TMP" local d_rc=$? rm -f "$TMP" # diff exit codes: # 0 = no differences # 1 = differences found # >1 = some error if (( d_rc > 1 )); then exit "$d_rc" fi } # -------------------- vyos-get / vyos-save ------------------------------ cmd_vyos_get() { local CODE="cfg,[[host]]" FILE="" FILE_SEEN=0 local WANT_RUN=0 WANT_START=0 RUN_NAME="" START_NAME="" if [[ $# -eq 0 ]]; then echo "Usage: nxios vyos-save --path:vyos,{host} [--run[:name]] [--start[:name]]" >&2 return 1 fi while [[ $# -gt 0 ]]; do case "$1" in --path=*|--path:*) CODE="${1#--path=}" CODE="${CODE#--path:}" ;; --file=*|--file:*) FILE="${1#--file=}" FILE="${FILE#--file:}" FILE_SEEN=1 ;; -p=*|-p:*) CODE="${1#-p=}" CODE="${CODE#-p:}" ;; -f=*|-f:*) FILE="${1#-f=}" FILE="${FILE#-f:}" FILE_SEEN=1 ;; --run) WANT_RUN=1 ;; --run=*|--run:*) WANT_RUN=1 RUN_NAME="${1#--run=}" RUN_NAME="${RUN_NAME#--run:}" ;; --start) WANT_START=1 ;; --start=*|--start:*) WANT_START=1 START_NAME="${1#--start=}" START_NAME="${START_NAME#--start:}" ;; --help|-h) echo "Usage: nxios vyos-save --path:vyos,{host} [--run[:name]] [--start[:name]]" >&2 return 0 ;; *) echo "Unknown argument to vyos-save: $1" >&2 return 1 ;; esac shift done ensure_path_default CODE CODE="$(nx_normalize_codeword_host "$CODE")" if [[ "$FILE_SEEN" -eq 1 ]]; then echo "WARNING: --file is ignored for vyos-save; use --run and/or --start." >&2 fi if [[ "$WANT_RUN" -eq 0 && "$WANT_START" -eq 0 ]]; then echo "ERROR: vyos-save requires --run and/or --start." >&2 return 1 fi local date_stamp date_stamp="$(date +%F)" if [[ "$WANT_RUN" -eq 1 && -z "$RUN_NAME" ]]; then RUN_NAME="running-config-${date_stamp}.cfg" fi if [[ "$WANT_START" -eq 1 && -z "$START_NAME" ]]; then START_NAME="startup-config-${date_stamp}.cfg" fi if [[ "$WANT_RUN" -eq 1 && "$WANT_START" -eq 1 && "$RUN_NAME" == "$START_NAME" ]]; then echo "ERROR: --run and --start resolve to the same filename: $RUN_NAME" >&2 return 1 fi if ! nx_is_vyos; then echo "[vyos-save] This command must be run on a VyOS system." >&2 return 1 fi local tmp_cfg rc if [[ "$WANT_RUN" -eq 1 ]]; then tmp_cfg="$(nx_mktemp "vyos-running.XXXXXX")" || return 1 echo "[vyos-save] Capturing running configuration from VyOS..." >&2 if ! nx_vyos_dump_config_commands > "$tmp_cfg"; then rc=$? echo "[vyos-save] Failed to dump running configuration (exit $rc)." >&2 rm -f "$tmp_cfg" return "$rc" fi # Safety net: don't upload an empty running-config if [[ ! -s "$tmp_cfg" ]]; then echo "[vyos-save] WARNING: captured running configuration is empty; aborting upload." >&2 rm -f "$tmp_cfg" return 1 fi echo "[vyos-save] Uploading configuration to server as '$RUN_NAME' (path: $CODE)..." >&2 cmd_push "--path:${CODE}" "--file:${tmp_cfg}" "--name=${RUN_NAME}" --overwrite=true rc=$? rm -f "$tmp_cfg" if [[ $rc -ne 0 ]]; then return "$rc" fi fi if [[ "$WANT_START" -eq 1 ]]; then tmp_cfg="$(nx_mktemp "vyos-startup.XXXXXX")" || return 1 cp /config/config.boot "$tmp_cfg" echo "[vyos-save] Uploading configuration to server as '$START_NAME' (path: $CODE)..." >&2 cmd_push "--path:${CODE}" "--file:${tmp_cfg}" "--name=${START_NAME}" --overwrite=true rc=$? rm -f "$tmp_cfg" return "$rc" fi return 0 } # -------------------- vyos-set / vyos-load ------------------------------ cmd_vyos_set() { local CODE="" FILE="" ACTION="confirm" CONFIRM_TIME=15 if [[ $# -eq 0 ]]; then echo "Usage: nxios vyos-load --path:vyos,{host} --file:.config [--action:[compare|commit|save]] [--commit] [--save] [--compare] [--confirm|--confirm:]" >&2 return 1 fi while [[ $# -gt 0 ]]; do case "$1" in --path=*|--path:*) CODE="${1#--path=}" CODE="${CODE#--path:}" ;; --file=*|--file:*) FILE="${1#--file=}" FILE="${FILE#--file:}" ;; -p=*|-p:*) CODE="${1#-p=}" CODE="${CODE#-p:}" ;; -f=*|-f:*) FILE="${1#-f=}" FILE="${FILE#-f:}" ;; --action=*|--action:*) ACTION="${1#--action=}" ACTION="${ACTION#--action:}" ;; --help|-h) echo "Usage: nxios vyos-load --path:vyos,{host} --file:.config [--action:[compare|commit|confirm|save]]" >&2 return 0 ;; --compare) ACTION="compare" ;; --commit) ACTION="commit" ;; --confirm) ACTION="confirm" ;; --confirm:*|--confirm=*) ACTION="confirm" CONFIRM_TIME="${1#--confirm=}" CONFIRM_TIME="${CONFIRM_TIME#--confirm:}" ;; --save) ACTION="save" ;; *) echo "Unknown argument to vyos-load: $1" >&2 return 1 ;; esac shift done ensure_path_default CODE if [[ -z "$FILE" ]]; then echo "ERROR: vyos-load requires --file:.config" >&2 return 1 fi if ! nx_is_vyos; then echo "[vyos-load] This command must be run on a VyOS system." >&2 return 1 fi case "$ACTION" in ""|compare|commit|save|confirm) ;; *) echo "[vyos-load] Invalid --action value '$ACTION' (use compare|commit|confirm|save)." >&2 return 1 ;; esac load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "$CODE")" local remote_name="$FILE" local url url="$(nx_get_file_url "$CODE" "$remote_name")" local tmp_cfg tmp_cfg="$(nx_mktemp "vyos-config.XXXXXX")" || return 1 echo "[vyos-load] Downloading '$remote_name' from path '$CODE'..." >&2 echo "[vyos-load] URL: $url" >&2 if nx_curl "$url" -o "$tmp_cfg"; then : else local rc=$? echo "[vyos-load] Download failed (curl exit $rc)." >&2 rm -f "$tmp_cfg" return "$rc" fi if ! nx_vyos_apply_config_file "$tmp_cfg" "$ACTION" "$CONFIRM_TIME"; then local rc=$? echo "[vyos-load] Failed to load/apply configuration (exit $rc)." >&2 rm -f "$tmp_cfg" return "$rc" fi rm -f "$tmp_cfg" return 0 } # -------------------- show (remote cat) ------------------------------ cmd_show() { local CODE="" FILE="" DOTFILE="" DIFF_PATH="" if [[ $# -eq 0 ]]; then echo "Usage: nxios show --path=CODE --file:NAME | --dotfile:NAME" >&2 return 1 fi while [[ $# -gt 0 ]]; do case "$1" in --path=*|--path:*) CODE="${1#--path=}" CODE="${CODE#--path:}" ;; --file=*|--file:*) FILE="${1#--file=}" FILE="${FILE#--file:}" ;; -p=*|-p:*) CODE="${1#-p=}" CODE="${CODE#-p:}" ;; -f=*|-f:*) FILE="${1#-f=}" FILE="${FILE#-f:}" ;; --dotfile=*|--dotfile:*) DOTFILE="${1#--dotfile=}" DOTFILE="${DOTFILE#--dotfile:}" ;; --diff=*|--diff:*) DIFF_PATH="${1#--diff=}" DIFF_PATH="${DIFF_PATH#--diff:}" ;; --help|-h) echo "Usage: nxios show --path=CODE --file:NAME | --dotfile:NAME [--diff:LOCALFILE]" >&2 return 0 ;; *) echo "Unknown argument to show: $1" >&2 return 1 ;; esac shift done ensure_path_default CODE load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "$CODE")" local remote_name if [[ -n "$DOTFILE" ]]; then remote_name="$DOTFILE" else if [[ -z "$FILE" ]]; then echo "ERROR: show requires --file:NAME or --dotfile:NAME" >&2 return 1 fi remote_name="$FILE" fi local url local root="${CODE%%,*}" if [[ -n "$DOTFILE" ]]; then url="${UTILS_ENDPOINT}?fn=dfdl&code=${CODE}&name=${remote_name}" else if [[ "$root" == "smb" ]]; then url="" else url="$(nx_get_file_url "$CODE" "$remote_name")" fi fi if [[ -n "$DIFF_PATH" ]]; then local local_path="${DIFF_PATH/#\~/$HOME}" if [[ ! -f "$local_path" ]]; then echo "ERROR: local diff target not found: $local_path" >&2 return 1 fi local tmp tmp="$(nx_mktemp "nxios.show.diff.XXXXXX")" echo "Fetching remote '${remote_name}' for diff..." >&2 if [[ -n "$url" ]]; then if nx_curl "$url" -o "$tmp"; then : else local rc=$? echo "Download failed (curl exit code $rc)." >&2 rm -f "$tmp" return "$rc" fi else if nx_curl "${UTILS_ENDPOINT}" --get --data-urlencode "fn=smb" --data-urlencode "op=show" --data-urlencode "name=${remote_name}" -o "$tmp"; then : else local rc=$? echo "Download failed (curl exit code $rc)." >&2 rm -f "$tmp" return "$rc" fi fi if ! diff -u --label "local:${local_path}" --label "remote:${remote_name}" "$local_path" "$tmp"; then local rc=$? rm -f "$tmp" if [[ "$rc" -gt 1 ]]; then echo "ERROR: diff failed (exit code $rc)." >&2 return "$rc" fi return "$rc" fi rm -f "$tmp" return 0 fi if [[ -n "$url" ]]; then nx_curl "$url" else nx_curl "${UTILS_ENDPOINT}" --get --data-urlencode "fn=smb" --data-urlencode "op=show" --data-urlencode "name=${remote_name}" fi } # -------------------- ssh installation helper ------------------------ nx_get_mode_octal() { local target="$1" local mode="" if mode="$(stat -c '%a' "$target" 2>/dev/null)"; then : elif mode="$(stat -f '%Lp' "$target" 2>/dev/null)"; then : else return 1 fi printf '%s\n' "$mode" } nx_ensure_local_ssh_client_permissions() { local ssh_home="${HOME:-}" local ssh_dir="" ssh_config="" mode="" if [[ -z "$ssh_home" ]]; then return 0 fi ssh_dir="${ssh_home}/.ssh" ssh_config="${ssh_dir}/config" if [[ -d "$ssh_dir" ]]; then chmod 700 "$ssh_dir" 2>/dev/null || true fi if [[ ! -e "$ssh_config" ]]; then return 0 fi echo "Checking SSH client config permissions..." >&2 chmod 600 "$ssh_config" 2>/dev/null || true mode="$(nx_get_mode_octal "$ssh_config" 2>/dev/null || true)" if [[ -n "$mode" && ! "$mode" =~ ^[46]00$ ]]; then echo "ERROR: ${ssh_config} permissions are ${mode}; SSH requires this file to be private." >&2 echo " Run: chmod 600 '${ssh_config}'" >&2 return 1 fi return 0 } nx_ensure_ssh_server_ready() { local desired_port="${1:-22}" local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" [[ -z "$desired_port" ]] && desired_port="22" echo "Checking local SSH tooling..." >&2 # Locate ssh-keygen and sshd even if sbin isn't on PATH local ssh_keygen_bin="" ssh_keygen_bin="$(command -v ssh-keygen 2>/dev/null || true)" [[ -z "$ssh_keygen_bin" && -x /usr/bin/ssh-keygen ]] && ssh_keygen_bin="/usr/bin/ssh-keygen" local sshd_bin="" sshd_bin="$(command -v sshd 2>/dev/null || true)" [[ -z "$sshd_bin" && -x /usr/sbin/sshd ]] && sshd_bin="/usr/sbin/sshd" [[ -z "$sshd_bin" && -x /usr/local/sbin/sshd ]] && sshd_bin="/usr/local/sbin/sshd" # --- ssh-keygen / sshd install (same as before, trimmed) ---------- if [[ -z "$ssh_keygen_bin" ]]; then echo " ssh-keygen not found; attempting to install OpenSSH client..." >&2 if command -v apt-get >/dev/null 2>&1; then $sudo_cmd apt-get update -y >/dev/null 2>&1 || true $sudo_cmd apt-get install -y openssh-client >/dev/null 2>&1 || true else echo "WARNING: apt-get not found; cannot auto-install ssh client tools." >&2 fi ssh_keygen_bin="$(command -v ssh-keygen 2>/dev/null || true)" fi if [[ -z "$sshd_bin" ]]; then echo " sshd not found; attempting to install OpenSSH server..." >&2 if command -v apt-get >/dev/null 2>&1; then $sudo_cmd apt-get update -y >/dev/null 2>&1 || true $sudo_cmd apt-get install -y openssh-server >/dev/null 2>&1 || true else echo "WARNING: apt-get not found; cannot auto-install ssh server." >&2 fi sshd_bin="$(command -v sshd 2>/dev/null || true)" [[ -z "$sshd_bin" && -x /usr/sbin/sshd ]] && sshd_bin="/usr/sbin/sshd" fi if [[ -z "$ssh_keygen_bin" ]]; then echo "ERROR: ssh-keygen is not available even after install attempt." >&2 return 1 fi # --- config: append defaults only if missing ---------------------- local cfg="/etc/ssh/sshd_config" if [[ -f "$cfg" ]]; then echo "Inspecting $cfg (non-destructive)..." >&2 # Port: only add if *no* uncommented Port line present if ! grep -qE '^[[:space:]]*Port[[:space:]]+[0-9]+' "$cfg"; then echo " No Port line found; appending 'Port ${desired_port}'." >&2 echo "Port ${desired_port}" | $sudo_cmd tee -a "$cfg" >/dev/null || \ echo "WARNING: failed to append Port ${desired_port} to $cfg" >&2 fi # ListenAddress: only add defaults if *no* uncommented ListenAddress if ! grep -qE '^[[:space:]]*ListenAddress[[:space:]]+' "$cfg"; then echo " No ListenAddress lines found; appending 0.0.0.0 and ::." >&2 { echo "ListenAddress 0.0.0.0" echo "ListenAddress ::" } | $sudo_cmd tee -a "$cfg" >/dev/null || \ echo "WARNING: failed to append ListenAddress defaults to $cfg" >&2 fi else echo "WARNING: $cfg not found; skipping config defaults." >&2 fi # --- enable/start sshd (same idea as before) ---------------------- local unit="" if command -v systemctl >/dev/null 2>&1; then if systemctl list-unit-files | grep -q '^sshd\.service'; then unit="sshd" elif systemctl list-unit-files | grep -q '^ssh\.service'; then unit="ssh" fi if [[ -n "$unit" ]]; then echo "Enabling and starting ${unit}.service..." >&2 $sudo_cmd systemctl enable --now "$unit" >/dev/null 2>&1 || \ echo "WARNING: failed to enable/start ${unit}.service" >&2 fi fi echo "SSH server/tooling check complete." >&2 } nx_detect_ssh_port() { local cfg="/etc/ssh/sshd_config" local port="22" # sane default if we can't detect if [[ -f "$cfg" ]]; then # First uncommented Port line wins local line line="$(grep -E '^[[:space:]]*Port[[:space:]]+[0-9]+' "$cfg" | head -n1 || true)" if [[ -n "$line" ]]; then port="$(awk '{for(i=1;i<=NF;i++) if($i ~ /^[0-9]+$/){print $i; exit}}' <<<"$line")" [[ -z "$port" ]] && port="22" fi fi printf '%s\n' "$port" } # -------------------- smb ------------------------------------------- cmd_smb() { local MOUNT_NAME="" UNC_PATH="" PROFILE_NAME="" local UNMOUNT_NAME="" local UNMOUNT_MODE="false" local CREATE_USER="" CREATE_PASS="" local CREATE_DOMAIN="" local DIR_UNC="" local COPY_TO="" COPY_FROM="" local LOCAL_FILE="" LOCAL_AS="" local READ_ONLY="false" if [[ $# -eq 0 ]]; then echo "Usage:" >&2 echo " nxios smb --mount:NAME --using:UNC --as:PROFILE [--ro|--read-only|-r]" >&2 echo " nxios smb --unmount:NAME" >&2 echo " nxios smb --dir:UNC [--as:PROFILE]" >&2 echo " nxios smb --copyTo:UNC --file:LOCAL [--as:PROFILE]" >&2 echo " nxios smb --copyFrom:UNC [--to:LOCAL] [--as:PROFILE]" >&2 echo " nxios smb --createProfile:USER --password:PASSWORD" >&2 return 1 fi while [[ $# -gt 0 ]]; do case "$1" in --mount=*|--mount:*) local val="${1#--mount=}" val="${val#--mount:}" MOUNT_NAME="$val" ;; --using=*|--using:*) local val="${1#--using=}" val="${val#--using:}" UNC_PATH="$val" ;; --as=*|--as:*) local val="${1#--as=}" val="${val#--as:}" PROFILE_NAME="$val" ;; --dir=*|--dir:*) local val="${1#--dir=}" val="${val#--dir:}" DIR_UNC="$val" ;; --copyTo=*|--copyTo:*) local val="${1#--copyTo=}" val="${val#--copyTo:}" COPY_TO="$val" ;; --copyFrom=*|--copyFrom:*) local val="${1#--copyFrom=}" val="${val#--copyFrom:}" COPY_FROM="$val" ;; --file=*|--file:*) local val="${1#--file=}" val="${val#--file:}" LOCAL_FILE="$val" ;; --to=*|--to:*) local val="${1#--to=}" val="${val#--to:}" LOCAL_AS="$val" ;; --unmount=*|--unmount:*) local val="${1#--unmount=}" val="${val#--unmount:}" UNMOUNT_NAME="$val" UNMOUNT_MODE="true" ;; --unmount) UNMOUNT_MODE="true" ;; --list|-l) UNMOUNT_MODE="true" ;; --createProfile=*|--createProfile:*) local val="${1#--createProfile=}" val="${val#--createProfile:}" CREATE_USER="$val" ;; --password=*|--password:*) local val="${1#--password=}" val="${val#--password:}" CREATE_PASS="$val" ;; --domain=*|--domain:*) local val="${1#--domain=}" val="${val#--domain:}" CREATE_DOMAIN="$val" ;; -r|--ro|--read-only) READ_ONLY="true" ;; --help|-h) usage ;; *) echo "Unknown argument to smb: $1" >&2 return 1 ;; esac shift done if [[ -z "$PROFILE_NAME" ]]; then PROFILE_NAME="${USER:-brett}" fi if [[ "$UNMOUNT_MODE" == "true" ]]; then if [[ -z "$UNMOUNT_NAME" ]]; then if [[ -r /proc/mounts ]]; then awk '$3=="cifs"{print $1" -> "$2}' /proc/mounts return 0 fi mount | awk '/ type cifs /{print $1" -> "$3}' return 0 fi local mnt_base="${NXIOS_SMB_MOUNT_ROOT:-/mnt}" local mnt_path="${mnt_base}/${UNMOUNT_NAME}" if mountpoint -q "$mnt_path"; then echo "Unmounting ${mnt_path}..." if ! sudo umount "$mnt_path" 2>/dev/null; then sudo umount -l "$mnt_path" || { echo "ERROR: failed to unmount ${mnt_path}" >&2 return 1 } fi else echo "Not mounted: ${mnt_path}" fi return 0 fi if [[ -n "$COPY_TO" || -n "$COPY_FROM" ]]; then local creds_tmp="" if [[ -n "$PROFILE_NAME" ]]; then load_roots local profile_file="$PROFILE_NAME" [[ "$profile_file" != *.txt ]] && profile_file="${profile_file}.txt" local url="${UTILS_ENDPOINT}?fn=smb&op=pull&name=${profile_file}" creds_tmp="$(nx_mktemp "nxios.smb.creds.XXXXXX")" if nx_curl "$url" -o "$creds_tmp"; then : else local rc=$? echo "ERROR: failed to download profile '${profile_file}' (curl exit $rc)." >&2 rm -f "$creds_tmp" return "$rc" fi fi if ! command -v smbclient >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then echo "Fetching SAMBA..." local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" $sudo_cmd DEBIAN_FRONTEND=noninteractive apt-get -qq update >/dev/null 2>&1 || true $sudo_cmd DEBIAN_FRONTEND=noninteractive apt-get -qq -y install smbclient >/dev/null 2>&1 || true echo "done." fi fi if ! command -v smbclient >/dev/null 2>&1; then echo "ERROR: smbclient not found (install samba client tools)." >&2 [[ -n "$creds_tmp" ]] && rm -f "$creds_tmp" return 1 fi local smb_opts smb_opts="${NXIOS_SMBCLIENT_OPTS:--m SMB3}" local -a smb_opts_arr=() read -r -a smb_opts_arr <<<"$smb_opts" local target="" if [[ -n "$COPY_TO" ]]; then target="$COPY_TO" else target="$COPY_FROM" fi target="${target//\\//}" [[ "$target" != //* ]] && target="//${target#/}" local t="${target#//}" local server="${t%%/*}" local rest="${t#*/}" if [[ -z "$server" || "$server" == "$rest" || -z "$rest" ]]; then echo "ERROR: copy expects UNC like //server/share/path/file" >&2 [[ -n "$creds_tmp" ]] && rm -f "$creds_tmp" return 1 fi local share="${rest%%/*}" local subpath="" [[ "$rest" == */* ]] && subpath="${rest#*/}" if [[ -z "$subpath" ]]; then echo "ERROR: copy expects UNC like //server/share/path/file" >&2 [[ -n "$creds_tmp" ]] && rm -f "$creds_tmp" return 1 fi local share_target="//${server}/${share}" local remote_file="" local parent="" if [[ -n "$subpath" ]]; then remote_file="${subpath##*/}" [[ "$subpath" == */* ]] && parent="${subpath%/*}" else remote_file="$rest" fi local esc_remote="${remote_file//\"/\\\"}" local pre_cmd="" if [[ -n "$parent" ]]; then local esc_parent="${parent//\"/\\\"}" pre_cmd="cd \"$esc_parent\"; " fi local cmd="" if [[ -n "$COPY_TO" ]]; then if [[ -z "$LOCAL_FILE" ]]; then echo "ERROR: --file is required with --copyTo." >&2 [[ -n "$creds_tmp" ]] && rm -f "$creds_tmp" return 1 fi local lf="${LOCAL_FILE/#\~/$HOME}" if [[ ! -f "$lf" ]]; then echo "ERROR: local file not found: $lf" >&2 [[ -n "$creds_tmp" ]] && rm -f "$creds_tmp" return 1 fi local esc_local="${lf//\"/\\\"}" cmd="${pre_cmd}put \"$esc_local\" \"$esc_remote\"" else local out="$LOCAL_AS" if [[ -z "$out" ]]; then out="$remote_file" fi out="${out/#\~/$HOME}" local esc_local="${out//\"/\\\"}" cmd="${pre_cmd}get \"$esc_remote\" \"$esc_local\"" fi if [[ -n "$creds_tmp" ]]; then smbclient "${smb_opts_arr[@]}" -A "$creds_tmp" "$share_target" -c "$cmd" local rc=$? rm -f "$creds_tmp" return "$rc" else smbclient "${smb_opts_arr[@]}" -N "$share_target" -c "$cmd" return $? fi fi if [[ -n "$DIR_UNC" ]]; then local creds_tmp="" if [[ -n "$PROFILE_NAME" ]]; then load_roots local profile_file="$PROFILE_NAME" [[ "$profile_file" != *.txt ]] && profile_file="${profile_file}.txt" local url="${UTILS_ENDPOINT}?fn=smb&op=pull&name=${profile_file}" creds_tmp="$(nx_mktemp "nxios.smb.creds.XXXXXX")" if nx_curl "$url" -o "$creds_tmp"; then : else local rc=$? echo "ERROR: failed to download profile '${profile_file}' (curl exit $rc)." >&2 rm -f "$creds_tmp" return "$rc" fi fi if ! command -v smbclient >/dev/null 2>&1; then if command -v apt-get >/dev/null 2>&1; then echo "Fetching SAMBA..." local sudo_cmd="" [[ $EUID -ne 0 ]] && sudo_cmd="sudo" $sudo_cmd DEBIAN_FRONTEND=noninteractive apt-get -qq update >/dev/null 2>&1 || true $sudo_cmd DEBIAN_FRONTEND=noninteractive apt-get -qq -y install smbclient >/dev/null 2>&1 || true echo "done." fi fi if ! command -v smbclient >/dev/null 2>&1; then echo "ERROR: smbclient not found (install samba client tools)." >&2 [[ -n "$creds_tmp" ]] && rm -f "$creds_tmp" return 1 fi local smb_opts smb_opts="${NXIOS_SMBCLIENT_OPTS:--m SMB3}" local -a smb_opts_arr=() read -r -a smb_opts_arr <<<"$smb_opts" local target="${DIR_UNC//\\//}" [[ "$target" != //* ]] && target="//${target#/}" local t="${target#//}" local server="${t%%/*}" local rest="${t#*/}" if [[ -z "$server" || "$server" == "$rest" || -z "$rest" ]]; then echo "ERROR: --dir expects UNC like //server/share[/path]" >&2 [[ -n "$creds_tmp" ]] && rm -f "$creds_tmp" return 1 fi local share="${rest%%/*}" local subpath="" [[ "$rest" == */* ]] && subpath="${rest#*/}" local share_target="//${server}/${share}" local cmd="ls" [[ -n "$subpath" ]] && cmd="cd ${subpath}; ls" if [[ -n "$creds_tmp" ]]; then smbclient "${smb_opts_arr[@]}" -A "$creds_tmp" "$share_target" -c "$cmd" local rc=$? rm -f "$creds_tmp" return "$rc" else smbclient "${smb_opts_arr[@]}" -N "$share_target" -c "$cmd" return $? fi fi if [[ -n "$CREATE_USER" ]]; then if [[ -z "$CREATE_PASS" ]]; then echo "ERROR: --password is required with --createProfile." >&2 return 1 fi local user_only="$CREATE_USER" local domain_from_user="" if [[ "$CREATE_USER" == *"@"* ]]; then user_only="${CREATE_USER%@*}" domain_from_user="${CREATE_USER#*@}" fi if [[ -z "$CREATE_DOMAIN" ]]; then CREATE_DOMAIN="$domain_from_user" fi if [[ -z "$CREATE_DOMAIN" && -n "${SMB_DOMAIN-}" ]]; then CREATE_DOMAIN="$SMB_DOMAIN" fi local tmp tmp="$(nx_mktemp "nxios.smb.profile.XXXXXX.txt")" { echo "username=${user_only}" echo "password=${CREATE_PASS}" if [[ -n "$CREATE_DOMAIN" ]]; then echo "domain=${CREATE_DOMAIN}" fi } >"$tmp" cmd_push --path:smb --file:"$tmp" --name:"${user_only}.txt" -o rm -f "$tmp" return 0 fi if [[ -z "$MOUNT_NAME" || -z "$UNC_PATH" || -z "$PROFILE_NAME" ]]; then echo "ERROR: --mount, --using, and --as are required for smb mount." >&2 return 1 fi # Normalize UNC to //server/share for Linux mount.cifs. UNC_PATH="${UNC_PATH//\\//}" if [[ "$UNC_PATH" == /* && "$UNC_PATH" != //* ]]; then UNC_PATH="/${UNC_PATH}" fi if [[ "$UNC_PATH" == //* && "$UNC_PATH" != //*/* ]]; then if [[ "$UNC_PATH" =~ ^//([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)(.+)$ ]]; then UNC_PATH="//${BASH_REMATCH[1]}/${BASH_REMATCH[2]}" fi fi load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "smb")" local profile_file="$PROFILE_NAME" if [[ "$profile_file" != *.txt ]]; then profile_file="${profile_file}.txt" fi local url="${UTILS_ENDPOINT}?fn=smb&op=pull&name=${profile_file}" local creds_tmp creds_tmp="$(nx_mktemp "nxios.smb.creds.XXXXXX")" if nx_curl "$url" -o "$creds_tmp"; then : else local rc=$? echo "ERROR: failed to download profile '${profile_file}' (curl exit $rc)." >&2 rm -f "$creds_tmp" return "$rc" fi local mnt_base="${NXIOS_SMB_MOUNT_ROOT:-/mnt}" local mnt_path="${mnt_base}/${MOUNT_NAME}" sudo mkdir -p "$mnt_path" || { echo "ERROR: mkdir $mnt_path failed"; rm -f "$creds_tmp"; return 1; } local opts opts="credentials=${creds_tmp},iocharset=utf8,noserverino,cache=none,actimeo=0,uid=$(id -u),gid=$(id -g)" if [[ "$READ_ONLY" == "true" ]]; then opts="${opts},ro" fi echo "Mounting ${UNC_PATH} -> ${mnt_path}" if ! sudo mount -t cifs "$UNC_PATH" "$mnt_path" -o "$opts"; then echo "ERROR: mount failed (check profile and SMB settings)." >&2 rm -f "$creds_tmp" return 2 fi rm -f "$creds_tmp" return 0 } # -------------------- ssh -------------- nx_format_ssh_host() { local h="$1" # If stored as [IPv6], strip brackets for ssh. if [[ "$h" == \[*\] ]]; then printf '%s\n' "${h:1:${#h}-2}" return 0 fi # For ssh: never wrap IPv6. printf '%s\n' "$h" } nx_format_scp_host() { local h="$1" # For scp/rsync: wrap IPv6 literals to avoid host:port/path ambiguity. if [[ "$h" == \[*\] ]]; then printf '%s\n' "$h" return 0 fi if [[ "$h" == *:* ]]; then printf '[%s]\n' "$h" else printf '%s\n' "$h" fi } nx_ssh_forget_host() { local h="$1" local p="$2" if [[ -z "$h" ]]; then return 0 fi if ! command -v ssh-keygen >/dev/null 2>&1; then echo "ERROR: ssh-keygen not found; cannot forget host keys." >&2 return 1 fi h="$(nx_format_ssh_host "$h")" if [[ -n "$p" && "$p" != "22" ]]; then ssh-keygen -R "[${h}]:${p}" >/dev/null 2>&1 || true fi ssh-keygen -R "${h}" >/dev/null 2>&1 || true return 0 } cmd_ssh() { local INIT_MODE="false" local REKEY_MODE="false" local SERVER="" PATH_CODE="" local OVERRIDE_USER="" OVERRIDE_PORT="" OVERRIDE_HOST="" local SSH_OPTS_RAW="" local FORGET_TARGET="" local FORGET_ALL="false" local RUN_CMD="" local TTY_MODE="none" # none | t | tt local -a SSH_ARGS=() if [[ $# -eq 0 ]]; then echo "Usage:" >&2 echo " nxios ssh --init [--path:CODE] [--server:LABEL] [--host:ADDR]" >&2 echo " nxios ssh --forget:SERVER|HOST [--path:CODE]" >&2 echo " nxios ssh --forget-all:SERVER|HOST [--path:CODE]" >&2 echo " nxios ssh --server:USER@HOST [--path:CODE] [-- ...ssh args...]" >&2 echo " nxios ssh --server:HOST [--user:USER] [--path:CODE] [-- ...ssh args...]" >&2 return 1 fi while [[ $# -gt 0 ]]; do case "$1" in --init) INIT_MODE="true" ;; --rekey) REKEY_MODE="true" ;; --forget=*|--forget:*) local val="${1#--forget=}" val="${val#--forget:}" FORGET_TARGET="$val" ;; --forget-all|--forget-all=*|--forget-all:*) FORGET_ALL="true" local val="${1#--forget-all=}" val="${val#--forget-all:}" if [[ "$val" != "--forget-all" ]]; then FORGET_TARGET="$val" fi ;; --server=*|--server:*) local val="${1#--server=}" val="${val#--server:}" if [[ "$val" == *"@"* ]]; then OVERRIDE_USER="${val%@*}" SERVER="${val#*@}" else SERVER="$val" fi ;; --user=*|--user:*) local val="${1#--user=}" val="${val#--user:}" OVERRIDE_USER="$val" ;; --port=*|--port:*) local val="${1#--port=}" val="${val#--port:}" OVERRIDE_PORT="$val" ;; --host=*|--host:*) local val="${1#--host=}" val="${val#--host:}" OVERRIDE_HOST="$val" ;; --path=*|--path:*) local val="${1#--path=}" val="${val#--path:}" PATH_CODE="$val" ;; --opts=*|--opts:*) local val="${1#--opts=}" val="${val#--opts:}" SSH_OPTS_RAW="$val" ;; --cmd=*|--cmd:*) local val="${1#--cmd=}" val="${val#--cmd:}" RUN_CMD="$val" ;; --tty:force|--tty=force|--tty:tt|--tty=f) TTY_MODE="tt" ;; --tty|--tty:t|--tty=*|--tty:*) TTY_MODE="t" ;; --) shift SSH_ARGS+=("$@") break ;; *) SSH_ARGS+=("$1") ;; esac shift done # Default path code if not specified if [[ -z "$PATH_CODE" ]]; then PATH_CODE="ssh" fi if [[ "$REKEY_MODE" == "true" ]]; then INIT_MODE="true" fi if [[ "$INIT_MODE" == "true" && -n "$RUN_CMD" ]]; then echo "ERROR: --cmd is not supported with ssh --init/--rekey." >&2 return 1 fi if [[ "$FORGET_ALL" == "true" && -z "$FORGET_TARGET" && -z "$SERVER" && ${#SSH_ARGS[@]} -gt 0 ]]; then FORGET_TARGET="${SSH_ARGS[0]}" SSH_ARGS=("${SSH_ARGS[@]:1}") fi if [[ "$FORGET_ALL" == "true" && -z "$FORGET_TARGET" && -n "$SERVER" ]]; then FORGET_TARGET="$SERVER" fi if [[ -n "$FORGET_TARGET" || "$FORGET_ALL" == "true" ]]; then if [[ "$INIT_MODE" == "true" ]]; then echo "ERROR: --forget is not supported with ssh --init/--rekey." >&2 return 1 fi if [[ -z "$SERVER" ]]; then if [[ "$FORGET_TARGET" == *"@"* ]]; then SERVER="${FORGET_TARGET#*@}" else SERVER="$FORGET_TARGET" fi fi local FORGET_HOST="" FORGET_FQDN="" FORGET_PORT="" local USED_BUNDLE=0 if [[ -n "$SERVER" ]]; then load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "$PATH_CODE")" local BUNDLE_USER="${OVERRIDE_USER:-${USER:-brett}}" local ARCHIVE_NAME="${BUNDLE_USER}@${SERVER}.tar.gz" local URL URL="$(nx_get_file_url "$PATH_CODE" "$ARCHIVE_NAME")" local TMP_BASE="${TMPDIR:-/tmp}" local TMP_TAR TMP_DIR TMP_TAR="$(mktemp -p "$TMP_BASE" "nxios.ssh.forget.XXXXXX.tar.gz")" TMP_DIR="$(mktemp -p "$TMP_BASE" -d "nxios.ssh.forget.XXXXXX.dir")" if nx_curl "$URL" -o "$TMP_TAR"; then if tar -xzf "$TMP_TAR" -C "$TMP_DIR"; then local CONF="${TMP_DIR}/connect-data.conf" if [[ -f "$CONF" ]]; then while IFS='=' read -r rawK rawV; do local k v k="${rawK%%#*}" v="${rawV%%#*}" k="${k#"${k%%[![:space:]]*}"}" k="${k%"${k##*[![:space:]]}"}" v="${v#"${v%%[![:space:]]*}"}" v="${v%"${v##*[![:space:]]}"}" [[ -z "$k" ]] && continue case "$k" in host) FORGET_HOST="$v" ;; fqdn) FORGET_FQDN="$v" ;; port) FORGET_PORT="$v" ;; esac done < "$CONF" USED_BUNDLE=1 fi fi fi rm -f "$TMP_TAR" rm -rf "$TMP_DIR" fi if [[ $USED_BUNDLE -eq 1 ]]; then [[ -z "$FORGET_PORT" ]] && FORGET_PORT="22" echo "Forgetting SSH known_hosts entries for '${SERVER}':" echo " Host: ${FORGET_HOST:-}" echo " FQDN: ${FORGET_FQDN:-}" echo " Port: ${FORGET_PORT}" echo if [[ -n "$FORGET_HOST" ]]; then nx_ssh_forget_host "$FORGET_HOST" "$FORGET_PORT" fi if [[ -n "$FORGET_FQDN" && "$FORGET_FQDN" != "$FORGET_HOST" ]]; then nx_ssh_forget_host "$FORGET_FQDN" "$FORGET_PORT" fi if [[ -n "$FORGET_TARGET" && "$FORGET_TARGET" != "$FORGET_HOST" && "$FORGET_TARGET" != "$FORGET_FQDN" ]]; then local RAW_TARGET="$FORGET_TARGET" if [[ "$RAW_TARGET" == *"@"* ]]; then RAW_TARGET="${RAW_TARGET#*@}" fi nx_ssh_forget_host "$RAW_TARGET" "$FORGET_PORT" fi else local RAW_TARGET="$FORGET_TARGET" if [[ -z "$RAW_TARGET" ]]; then RAW_TARGET="$SERVER" fi if [[ -z "$RAW_TARGET" ]]; then echo "ERROR: --forget requires a server name or host." >&2 return 1 fi if [[ "$RAW_TARGET" == *"@"* ]]; then RAW_TARGET="${RAW_TARGET#*@}" fi echo "Forgetting SSH known_hosts entry for '${RAW_TARGET}'..." nx_ssh_forget_host "$RAW_TARGET" "" fi if [[ "$FORGET_ALL" == "true" ]]; then local SERVER_FORGET="${SERVER}" if [[ -z "$SERVER_FORGET" && -n "$FORGET_TARGET" ]]; then if [[ "$FORGET_TARGET" == *"@"* ]]; then SERVER_FORGET="${FORGET_TARGET#*@}" else SERVER_FORGET="$FORGET_TARGET" fi fi if [[ -n "$SERVER_FORGET" ]]; then echo "Removing SSH profiles from server:" echo " Path: ${PATH_CODE}" echo " Server: ${SERVER_FORGET}" if nx_curl "${UTILS_ENDPOINT}" --get \ --data-urlencode "fn=ssh-forget" \ --data-urlencode "ssh-forget=${SERVER_FORGET}" \ --data-urlencode "path=${PATH_CODE}"; then : else local rc=$? echo "ERROR: failed to delete remote bundles (exit ${rc})." >&2 return "$rc" fi fi fi echo "Done." return 0 fi if [[ "$INIT_MODE" == "true" ]]; then # ---------------- INIT / REKEY MODE ---------------- # Run on the TARGET box. Generates keypair, authorizes pubkey locally, # builds @.tar.gz and uploads it to the ssh root (or PATH_CODE). # Determine label/hostname for naming the bundle if [[ -z "$SERVER" ]]; then SERVER="$(hostname -s 2>/dev/null || hostname 2>/dev/null || echo "host")" fi local LABEL="$SERVER" # For ssh --init, we always use the current user and do NOT allow --user override. if [[ -n "$OVERRIDE_USER" ]]; then echo "ERROR: --user is not supported with ssh --init." >&2 echo " Run 'nxios ssh --init' as the account you want to allow SSH access for." >&2 return 1 fi local LOGIN_USER LOGIN_USER="$(id -un 2>/dev/null || echo "${USER:-brett}")" nx_ensure_local_ssh_client_permissions || { echo "ERROR: local SSH client configuration is not secure enough for ssh --init." >&2 return 1 } # If rekeying, try to fetch existing bundle to reuse host/port/opts defaults local EXIST_HOST="" EXIST_PORT="" EXIST_OPTS="" EXIST_FQDN="" if [[ "$REKEY_MODE" == "true" ]]; then ensure_path_default PATH_CODE load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "$PATH_CODE")" local EXIST_ARCHIVE="${LOGIN_USER}@${LABEL}.tar.gz" local EXIST_URL EXIST_URL="$(nx_get_file_url "$PATH_CODE" "$EXIST_ARCHIVE")" local EXIST_TAR EXIST_DIR EXIST_TAR="$(nx_mktemp "nxios.ssh.rekey.XXXXXX.tar.gz")" EXIST_DIR="$(nx_mktemp_dir "nxios.ssh.rekey.dir.XXXXXX")" if nx_curl "$EXIST_URL" -o "$EXIST_TAR"; then if tar -xzf "$EXIST_TAR" -C "$EXIST_DIR"; then local EXIST_CONF="${EXIST_DIR}/connect-data.conf" if [[ -f "$EXIST_CONF" ]]; then while IFS='=' read -r rawK rawV; do local k v k="${rawK%%#*}" v="${rawV%%#*}" k="${k#"${k%%[![:space:]]*}"}" k="${k%"${k##*[![:space:]]}"}" v="${v#"${v%%[![:space:]]*}"}" v="${v%"${v##*[![:space:]]}"}" [[ -z "$k" ]] && continue case "$k" in host) EXIST_HOST="$v" ;; fqdn) EXIST_FQDN="$v" ;; port) EXIST_PORT="$v" ;; key) ;; # ignore old key name opts) EXIST_OPTS="$v" ;; esac done < "$EXIST_CONF" fi fi fi rm -f "$EXIST_TAR" rm -rf "$EXIST_DIR" fi # Ensure ssh bits are present + listening (non-destructive). # OVERRIDE_PORT, if set, is a *hint* for bootstrapping a Port line when none exists. nx_ensure_ssh_server_ready "$OVERRIDE_PORT" || { echo "ERROR: SSH server initialization failed; aborting ssh --init." >&2 return 1 } # Determine the *actual* port from sshd_config (authoritative for connect-data). local PORT PORT="${OVERRIDE_PORT:-${EXIST_PORT:-$(nx_detect_ssh_port)}}" echo "Initializing SSH profile for '${LABEL}':" echo " Path code: ${PATH_CODE}" echo " Login user: ${LOGIN_USER}" echo " SSH port: ${PORT}" echo " Host (init): ${OVERRIDE_HOST:-}" echo # Temp working dir for key + profile local TMP_DIR TMP_DIR="$(nx_mktemp_dir "nxios.sshinit.XXXXXX")" # Generate ed25519 keypair (no passphrase) local KEY_BASENAME="id_ed25519" local KEY_PATH="${TMP_DIR}/${KEY_BASENAME}" echo "Generating keypair..." if ! ssh-keygen -t ed25519 -N "" -C "nxios@${LABEL}" -f "$KEY_PATH" >/dev/null 2>&1; then echo "ERROR: ssh-keygen failed." >&2 rm -rf "$TMP_DIR" return 1 fi # Authorize the public key on THIS host for the current user. local SSH_HOME="${HOME:-/home/${LOGIN_USER}}" local AUTH_DIR="${SSH_HOME}/.ssh" local AUTH_FILE="${AUTH_DIR}/authorized_keys" mkdir -p "$AUTH_DIR" chmod 700 "$AUTH_DIR" 2>/dev/null || true touch "$AUTH_FILE" chmod 600 "$AUTH_FILE" 2>/dev/null || true cat "${KEY_PATH}.pub" >> "$AUTH_FILE" echo "Appended public key to ${AUTH_FILE} for user ${LOGIN_USER}" # Write connect-data.conf local HOSTNAME_FQDN HOSTNAME_FQDN="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo "${LABEL}")" # If --host: was provided, prefer that over the local hostname local CONNECT_HOST if [[ -n "$OVERRIDE_HOST" ]]; then local hlow="${OVERRIDE_HOST,,}" case "$hlow" in "@@fqdn"|"__fqdn__"|"@fqdn@"|"%fqdn%"|"<>"|"[[fqdn]]") CONNECT_HOST="$HOSTNAME_FQDN" ;; *) CONNECT_HOST="$OVERRIDE_HOST" ;; esac else if [[ -n "$EXIST_HOST" ]]; then CONNECT_HOST="$EXIST_HOST" elif [[ -n "$EXIST_FQDN" ]]; then CONNECT_HOST="$EXIST_FQDN" else CONNECT_HOST="$HOSTNAME_FQDN" fi fi # Decide ssh -o options (override existing if provided) local EFFECTIVE_OPTS="${SSH_OPTS_RAW:-$EXIST_OPTS}" cat > "${TMP_DIR}/connect-data.conf" <&2 rm -rf "$TMP_DIR" return "$rc" fi # Upload via self-reference (cmd_push) echo "Uploading bundle to path '${PATH_CODE}' as '${ARCHIVE_NAME}'..." cmd_push "--path:${PATH_CODE}" "--file:${ARCHIVE}" "--name=${ARCHIVE_NAME}" -o local rc=$? rm -rf "$TMP_DIR" rm -f "$ARCHIVE" if [[ $rc -ne 0 ]]; then echo "ERROR: upload failed (cmd_push exit ${rc})." >&2 return "$rc" fi echo "SSH profile initialization complete for ${LABEL}." return 0 fi # ---------------- CONNECT MODE ---------------- if [[ -z "$SERVER" ]]; then echo "ERROR: --server:NAME is required for ssh (non-init mode)." >&2 return 1 fi # Fetch @.tar.gz from PATH_CODE load_roots local WEB_PATH WEB_PATH="$(decode_codeword_to_webpath "$PATH_CODE")" local BUNDLE_USER="${OVERRIDE_USER:-${USER:-brett}}" local ARCHIVE_NAME="${BUNDLE_USER}@${SERVER}.tar.gz" local URL URL="$(nx_get_file_url "$PATH_CODE" "$ARCHIVE_NAME")" local TMP_BASE="${TMPDIR:-/tmp}" local TMP_TAR="" TMP_DIR="" TMP_TAR="$(mktemp -p "$TMP_BASE" "nxios.ssh.bundle.XXXXXX.tar.gz")" TMP_DIR="$(mktemp -p "$TMP_BASE" -d "nxios.ssh.bundle.XXXXXX.dir")" echo "Fetching SSH bundle for '${SERVER}':" echo " Path code: ${PATH_CODE}" echo " Resolved: ${WEB_PATH}" echo " URL: ${URL}" echo if nx_curl "$URL" -o "$TMP_TAR"; then : else local rc=$? echo "ERROR: failed to download bundle (exit ${rc})." >&2 rm -f "$TMP_TAR" rm -rf "$TMP_DIR" return "$rc" fi if ! tar -xzf "$TMP_TAR" -C "$TMP_DIR"; then local rc=$? echo "ERROR: failed to extract bundle (tar exit ${rc})." >&2 rm -f "$TMP_TAR" rm -rf "$TMP_DIR" return "$rc" fi # Clean up tar now; we only need the extracted files rm -f "$TMP_TAR" # Parse connect-data.conf local CONF="${TMP_DIR}/connect-data.conf" if [[ ! -f "$CONF" ]]; then echo "ERROR: connect-data.conf not found in bundle." >&2 rm -rf "$TMP_DIR" return 1 fi local host="" fqdn="" user="" port="" keyfile="" opts="" while IFS='=' read -r rawK rawV; do local k v k="${rawK%%#*}" v="${rawV%%#*}" k="${k#"${k%%[![:space:]]*}"}" k="${k%"${k##*[![:space:]]}"}" v="${v#"${v%%[![:space:]]*}"}" v="${v%"${v##*[![:space:]]}"}" [[ -z "$k" ]] && continue case "$k" in host) host="$v" ;; fqdn) fqdn="$v" ;; user) user="$v" ;; port) port="$v" ;; key) keyfile="$v" ;; opts) opts="$v" ;; esac done < "$CONF" [[ -z "$host" ]] && host="$SERVER" if [[ -n "$OVERRIDE_HOST" ]]; then local hlow="${OVERRIDE_HOST,,}" case "$hlow" in "@@fqdn"|"__fqdn__"|"@fqdn@"|"%fqdn%"|"<>"|"[[fqdn]]") [[ -n "$fqdn" ]] && host="$fqdn" ;; *) host="$OVERRIDE_HOST" ;; esac fi [[ -z "$user" ]] && user="${USER:-brett}" [[ -z "$port" ]] && port="22" [[ -z "$keyfile" ]] && keyfile="id_ed25519" if [[ -n "$OVERRIDE_PORT" ]]; then port="$OVERRIDE_PORT" fi local KEY_PATH="${TMP_DIR}/${keyfile}" if [[ ! -f "$KEY_PATH" ]]; then echo "ERROR: key file '${keyfile}' not found in bundle." >&2 rm -rf "$TMP_DIR" return 1 fi nx_ensure_local_ssh_client_permissions || { rm -rf "$TMP_DIR" return 1 } chmod 600 "$KEY_PATH" 2>/dev/null || true # Cleanup handler: nuke temp assets on exit (even if ssh fails) _nxios_ssh_cleanup() { [[ -n "${TMP_TAR:-}" ]] && rm -f "$TMP_TAR" [[ -n "${TMP_DIR:-}" ]] && rm -rf "$TMP_DIR" } trap _nxios_ssh_cleanup EXIT INT TERM local SSH_HOST SSH_HOST="$(nx_format_ssh_host "$host")" local SSH_TARGET="${user}@${SSH_HOST}" local -a CMD=(ssh -i "$KEY_PATH" -p "$port") case "$TTY_MODE" in t) CMD+=(-t) ;; tt) CMD+=(-tt) ;; esac # Apply ssh options: bundle opts first, then CLI overrides (later wins) local -a ALL_OPTS=() if [[ -n "$opts" ]]; then IFS=';' read -r -a OPT_ARRAY <<<"$opts" ALL_OPTS+=("${OPT_ARRAY[@]}") fi if [[ -n "$SSH_OPTS_RAW" ]]; then IFS=';' read -r -a OPT_CLI_ARRAY <<<"$SSH_OPTS_RAW" ALL_OPTS+=("${OPT_CLI_ARRAY[@]}") fi for opt in "${ALL_OPTS[@]}"; do [[ -z "$opt" ]] && continue if [[ "$opt" == BindAddress=* ]]; then CMD+=(-b "${opt#*=}") else CMD+=(-o "$opt") fi done CMD+=("$SSH_TARGET") if [[ -n "$RUN_CMD" ]]; then CMD+=("$RUN_CMD") fi CMD+=("${SSH_ARGS[@]}") echo "Starting ssh session:" echo " Target: ${SSH_TARGET}" echo " Port: ${port}" echo # Allow ssh to fail without tripping 'set -e', so cleanup still runs set +e "${CMD[@]}" local rc=$? set -e _nxios_ssh_cleanup trap - EXIT INT TERM return "$rc" } # -------------------- refresh (update) nxios.sh ---------------------- cmd_refresh_nxios() { local target="${HOME_FOLDER:-$HOME/scripts}/nxios.sh" local url url="$(nx_get_file_url "scripts" "nxios.sh")" echo "Refreshing nxios.sh from:" echo " URL: $url" echo " Target: $target" mkdir -p "$(dirname "$target")" # Download to temp then move into place, to avoid half-written scripts. local tmp tmp="$(nx_mktemp "nxios.refresh.XXXXXX")" if ! nx_curl "$url" -o "$tmp"; then echo "Update failed (download error)." >&2 rm -f "$tmp" return 1 fi chmod +x "$tmp" 2>/dev/null || true if ! mv "$tmp" "$target"; then echo "Update failed (cannot move into place)." >&2 rm -f "$tmp" return 1 fi echo "nxios.sh updated successfully." } # -------------------- stubs for future server-side API --------------- cmd_stub_rmdir() { echo "rmdir is not implemented yet." >&2 echo "Plan: add rmdir.php on the server (using decode_codeword_dir())" >&2 echo "to remove a directory tree safely, then wire nxios rmdir to it." >&2 exit 2 } cmd_stub_mkdir() { echo "mkdir is not implemented yet." >&2 echo "Plan: add mkdir.php on the server (using decode_codeword_dir())" >&2 echo "to create a directory for a codeword, then wire nxios mkdir to it." >&2 exit 2 } cmd_stub_mvdir() { echo "mvdir is not implemented yet." >&2 echo "Plan: add mvdir.php on the server (using decode_codeword_dir())" >&2 echo "to create a directory for a codeword, then wire nxios mkdir to it." >&2 exit 2 } cmd_stub_update() { echo "update is not implemented yet." >&2 echo "Plan: add list.php returning JSON of files+mtimes for comparison." >&2 exit 2 } # -------------------- main dispatch --------------------------------- if [[ $# -lt 1 ]]; then usage fi # Global flag parsing (before COMMAND) while [[ $# -gt 0 ]]; do case "$1" in -s|--silent) NXIOS_SILENT=1 shift ;; -4) NXIOS_IP_FAMILY=4 shift ;; -6) NXIOS_IP_FAMILY=6 shift ;; --) shift break ;; *) break ;; esac done if [[ $# -lt 1 ]]; then usage fi COMMAND="$1"; shift || true case "$COMMAND" in refresh) cmd_refresh_nxios "$@" ;& # runs init as well... init) cmd_init "$@" ;; push|put|save) cmd_push "$@" ;; load|get|pull) cmd_get_like "get" "$@" ;; show) cmd_show "$@" ;; exec|run) cmd_get_like "exec" "$@" ;; smb) cmd_smb "$@" ;; ssh) cmd_ssh "$@" ;; ls|dir) cmd_ls "$@" ;; rm|del|erase) cmd_rm "$@" ;; mkdir) cmd_stub_mkdir "$@" ;; rmdir) cmd_stub_rmdir "$@" ;; mvdir) cmd_stub_mvdir "$@" ;; ren|rename) cmd_ren "$@" ;; update) cmd_stub_update "$@" ;; roots) cmd_show_roots "$@" ;; --help|-h|help) usage ;; vyos-save|vyos-get) cmd_vyos_get "$@" ;; # vyos-load|vyos-set) # cmd_vyos_set "$@" ;; tls) cmd_tls "$@" ;; install|inst|ins) cmd_get_like "install" "$@" ;; *) echo "Unknown command: $COMMAND" >&2; usage ;; esac