Files
op-packages/gitbackup/files/usr/libexec/rpcd/luci.gitbackup
T

679 lines
30 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/bin/sh
# shellcheck shell=sh
#
# luci.gitbackup -- rpcd plugin, ubus object "luci.gitbackup" (spec "rpcd и
# ACL"). Everything the CLI (usr/sbin/gitbackup) can do becomes reachable
# from the web here, and NOTHING ELSE ever runs: this file's own job is
# translating between the rpcd "list"/"call" protocol (TechRef:
# https://openwrt.org/docs/techref/rpcd) and a `gitbackup` CLI invocation --
# never `git`, `sysupgrade`, or any other router command directly.
#
# Ticket 21 (R110/R111/R109, D07 -- the operator's own words: "вызывать под
# капотом может любые команды роутера. желательно только gitbackup"). Before
# this ticket, gbrpc_history/gbrpc_diff/gbrpc_list_paths/gbrpc_audit_paths
# below ran `git ls-remote`/`git fetch`/`git log`/`git diff`/`git diff-tree`/
# `sysupgrade -l` themselves, straight against gitbackup.origin.url out of
# UCI -- WITHOUT ever running it through remoteurl.sh's own gb_parse_url
# first, which is the one validator this package already had for exactly
# this string (ticket 03). `gitbackup` itself always has validated its own
# GB_URL before spawning git (cmd_test/cmd_run already did; cmd_diff/
# cmd_restore now do too, ticket 21) -- so routing every one of these
# through the CLI closes that gap for free, rather than teaching this file
# a second copy of the same check. Where the CLI had no equivalent yet
# (the branch's commit list, a diff between two arbitrary commits), ticket
# 21 added `gitbackup history`/`gitbackup diff <from> <to>` instead of
# leaving a second git call here. What is left below with logic of its own
# is exactly the methods that touch no external command at all: set_paths/
# set_secret write plain files, validate_cron calls schedule.sh's own pure
# shell functions.
#
# Protocol, confirmed against this checkout's own stock examples
# (luci-app-https-dns-proxy, luci-app-squid) rather than invented:
# luci.gitbackup list -> one JSON object, method name -> example
# params object (types only, ubus does not
# enforce this against real calls).
# luci.gitbackup call <method> -> reads exactly one line of compact JSON
# (the call's params) from stdin, prints
# exactly one JSON object on stdout, and
# always exits 0 -- ubus turns a nonzero
# exit into an RPC-level error with no
# body, which would break this ticket's
# own "ошибка тоже JSON, а не пустой
# ответ" requirement.
#
# `get_secret` does not exist anywhere below. `status` prints only
# token_set (bool), never the token itself.
#
# GB_SHARE/GB_STATE_DIR/GB_BIN are the same override seam
# usr/sbin/gitbackup and etc/init.d/gitbackup already use, so tests/run.sh
# can point this at a tree, state directory and CLI binary that are not the
# router's real ones; the router never sets any of them.
set -u
GB_SHARE="${GB_SHARE:-/usr/share/gitbackup}"
GB_STATE_DIR="${GB_STATE_DIR:-/var/run/gitbackup}"
GB_BIN="${GB_BIN:-/usr/sbin/gitbackup}"
GB_SYSUPGRADE_CONF="${GB_SYSUPGRADE_CONF:-/etc/sysupgrade.conf}"
# shellcheck disable=SC1091 # GB_SHARE is a runtime path, not resolvable statically
. "$GB_SHARE/lib.sh"
# device.sh (gb_device_id/gb_expand) and auth.sh (gb_git_env, the one `eval
# "$(...)"` this package's whole codebase has ever needed) are deliberately
# NOT sourced here any more (ticket 21): both were only ever used by the
# git-calling helpers this file no longer has. `gitbackup` itself still
# sources both to do that work -- this file just never touches either
# again, which is the strongest available answer to "can this eval be
# removed": it is gone from this process entirely, not merely justified.
# shellcheck disable=SC1091
. "$GB_SHARE/schedule.sh"
# shellcheck disable=SC1091
. "$GB_SHARE/paths.sh"
# card.sh (ticket 09) may not exist on a system built before it landed --
# same "command -v gb_card" tolerance usr/sbin/gitbackup's own cmd_run
# already uses, not a new convention invented here.
if [ -r "$GB_SHARE/card.sh" ]; then
# shellcheck disable=SC1091
. "$GB_SHARE/card.sh"
fi
# ---------------------------------------------------------------------------
# JSON param helpers -- jsonfilter, not jshn.sh: interfaces.md only promises
# "jsonfilter есть, ubus есть", jshn.sh is a libubox implementation detail
# this package's own DEPENDS never asked for, and the rest of gitbackup
# already hand-rolls JSON output (lib.sh's gb_json_str/gb_json_esc) instead
# of pulling in a second writer -- reusing that here keeps one JSON style
# in the whole package instead of two.
# ---------------------------------------------------------------------------
# _gb_rpc_param <json-line> <jsonfilter-expr> -- empty string (not an error)
# for a missing/wrong-type field: jsonfilter itself exits 1 and prints
# nothing when the path does not resolve (confirmed live against this
# project's own owlab image), which every caller below treats the same as
# "field omitted".
_gb_rpc_param() {
printf '%s' "$1" | jsonfilter -e "$2" 2>/dev/null
}
# _gb_rpc_param_list <json-line> <jsonfilter-array-expr> -- one element per
# line (jsonfilter's own "@.x[*]" behavior, confirmed live), empty output
# for a missing array.
_gb_rpc_param_list() {
printf '%s' "$1" | jsonfilter -e "$2" 2>/dev/null
}
# _gb_rpc_error <reason> -- the one shape every failed method below prints:
# a JSON object with a "reason" field, never an empty body and never a bare
# exit code (ticket 10 acceptance criterion).
_gb_rpc_error() {
printf '{ "reason": %s }\n' "$(gb_json_str "$1")"
}
# _gb_rpc_ok_msg <field> <text> -- "{ "<field>": "<text>" }", the shared
# shape for the several methods that just wrap one piece of CLI text.
_gb_rpc_text() {
printf '{ "%s": %s }\n' "$1" "$(gb_json_str "$2")"
}
# ---------------------------------------------------------------------------
# Method implementations, one gbrpc_<method> per ACL entry -- the case in
# main() at the bottom names every one of these and no others, and
# t_rpcd_acl_matches_plugin (tests/run.sh) checks the reverse direction, so
# the two can never quietly drift apart.
# ---------------------------------------------------------------------------
# --- read tier ---------------------------------------------------------
gbrpc_status() {
_gb_rpc_out=$("$GB_BIN" status 2>/dev/null)
if [ -n "$_gb_rpc_out" ]; then
printf '%s\n' "$_gb_rpc_out"
else
_gb_rpc_error 'gitbackup status produced no output'
fi
}
gbrpc_log() {
_gb_rpc_lines=$(_gb_rpc_param "$1" '@.lines')
case "$_gb_rpc_lines" in '' | *[!0-9]*) _gb_rpc_lines=200 ;; esac
_gb_rpc_text text "$("$GB_BIN" log "$_gb_rpc_lines" 2>/dev/null)"
}
gbrpc_pubkey() {
_gb_rpc_out=$("$GB_BIN" pubkey 2>&1)
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -eq 0 ]; then
_gb_rpc_text pubkey "$_gb_rpc_out"
else
_gb_rpc_error "$_gb_rpc_out"
fi
}
# gbrpc_list_paths -- three distinct sets, on purpose (the read/write
# asymmetry this ticket exists to close): "paths" is the RAW, unexpanded
# content of GB_SYSUPGRADE_CONF (paths.sh's own gb_paths_list) -- exactly
# what the file contains, comments and blank lines included. "entries"
# (ticket 26) is gb_paths_entries -- the same content with every comment
# and blank line dropped, i.e. only genuine path entries, a directory line
# still included verbatim -- what the LuCI view lists and edits, and the
# only shape gbrpc_set_paths below may ever write back; a stock
# /etc/sysupgrade.conf's own header comments and commented-out example
# lines are not entries and must never render as a removable row. "effective"
# is plain `sysupgrade -l`, the full union spec §sysupgrade names
# (GB_SYSUPGRADE_CONF ∪ /lib/upgrade/keep.d/* ∪ changed conffiles) already
# recursively expanded to individual files/symlinks. Read-only: it exists
# for sizing (gb_paths_size_kb's own walk) and so a caller can show what is
# already covered without the operator having added it. Collapsing "paths"
# and "effective" into one field (the previous shape of this method) is
# exactly what let a directory a human added survive only until the next
# `set_paths` call, which would write the already-expanded file list
# straight back to disk and permanently lose the "anything added to this
# directory later is covered too" property a real sysupgrade gives a
# directory line.
#
# Ticket 21: used to build both halves itself, calling `sysupgrade -l`
# directly -- the one external command this method ever needed, and the
# one thing "только gitbackup" says this file should never do again. The
# CLI already builds exactly this shape (usr/sbin/gitbackup's own
# `paths list --json`, ticket 21) for the same reason `config_diff` below
# has always wrapped `gitbackup diff` instead of re-deriving it here.
gbrpc_list_paths() {
_gb_rpc_out=$("$GB_BIN" paths list --json 2>&1)
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -eq 0 ]; then
printf '%s\n' "$_gb_rpc_out"
else
_gb_rpc_error "$_gb_rpc_out"
fi
}
# gbrpc_validate_cron -- live validation for the Settings raw-cron field
# (spec "живая валидацию в Settings"): reuses schedule.sh's own
# gb_cron_valid/gb_cron_next rather than reimplementing either, so the
# rpcd-facing answer and the crontab that gb_cron_apply actually writes can
# never disagree about what is valid.
gbrpc_validate_cron() {
_gb_rpc_expr=$(_gb_rpc_param "$1" '@.expr')
# gb_cron_valid prints its reason on stderr only on failure -- captured
# unconditionally here (empty when the expression is fine) so both
# branches below read from the one variable instead of calling it a
# third time.
_gb_rpc_why=$(gb_cron_valid "$_gb_rpc_expr" 2>&1 >/dev/null)
if gb_cron_valid "$_gb_rpc_expr" >/dev/null 2>&1; then
_gb_rpc_next=$(gb_cron_next "$_gb_rpc_expr" 2>/dev/null)
printf '{ "valid": true, "next": %s }\n' "$(gb_json_str "$_gb_rpc_next")"
else
printf '{ "valid": false, "reason": %s }\n' "$(gb_json_str "$_gb_rpc_why")"
fi
}
# gbrpc_history -- last N commits of this device's own branch: sha,
# subject (which already starts with the backup's own date, spec G03.1, so
# there is no separate "date" field to keep in sync with it) and the list
# of /etc/config/*-and-everything-else paths that commit actually touched.
#
# Ticket 21: this used to resolve GB_URL out of UCI and fetch/log/diff-tree
# it itself, WITHOUT ever validating its shape through gb_parse_url first
# -- exactly the bypass the ticket exists to close (the operator's own
# words: "вызывать под капотом может любые команды роутера. желательно
# только gitbackup"). `gitbackup history` (usr/sbin/gitbackup, ticket 21)
# now does the exact same fetch/log/diff-tree work, but only after running
# GB_URL through gb_parse_url the same way cmd_test/cmd_run always have --
# this wrapper only ever forwards --limit and relays whatever the CLI
# printed (success: its own valid JSON body; failure: its stderr, wrapped
# into the shared { "reason": ... } shape every other method here uses).
gbrpc_history() {
_gb_rpc_limit=$(_gb_rpc_param "$1" '@.limit')
case "$_gb_rpc_limit" in '' | *[!0-9]*) _gb_rpc_limit=20 ;; esac
_gb_rpc_out=$("$GB_BIN" history --limit "$_gb_rpc_limit" 2>&1)
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -eq 0 ]; then
printf '%s\n' "$_gb_rpc_out"
else
_gb_rpc_error "$_gb_rpc_out"
fi
}
# gbrpc_diff -- plain `git diff <from> <to>` text, restricted implicitly to
# this device's own branch (its tree only ever holds GB_PREFIX's own
# content to begin with, per gb_build_tree) -- no separate path filter
# needed on top of that.
#
# Ticket 21: `from`/`to` used to be checked only for non-emptiness, with no
# separator in front of them in the `git diff` call that followed -- a
# value shaped like an option (leading "-") would have reached git's own
# argument parser as one. This still checks for the missing-field case
# itself (a fast, obvious refusal that need not spend a process on the
# CLI), but the actual FORM check -- "a full 40-character commit sha, not
# just any non-empty string" -- now lives once, in `gitbackup diff`
# (usr/sbin/gitbackup, ticket 21): the same "одна точка входа -- одна точка
# валидации" this whole ticket asks for. A malformed value is refused by
# the CLI (exit 2) and relayed here as the same { "reason": ... } shape,
# never reaching a `git` argv at all.
#
# In the ACL's WRITE tier, not read, unlike every other method here that
# only reads. Not because it writes anything -- it does not -- but because
# of what it returns: `git diff` between two commits of the backup branch
# is the CONTENT of the backed-up files, and at the shipped default
# (visibility=private, security.scrub='0') that content is unscrubbed --
# /etc/shadow's root hash, the dropbear host keys, the WPA PSKs. A read
# grant is meant to answer "is the backup healthy", which status/log/
# history/config_diff all do without handing over a single file's bytes
# (config_diff compares manifests: paths, modes, hashes, never content).
# Splitting the tiers on "may see the router's secrets" rather than on
# "changes something" is what makes the read tier actually safe to hand
# out. The LuCI History view's own "View diff" button therefore needs the
# write grant, which is the same grant its neighbouring "Restore" button
# has always needed.
gbrpc_diff() {
_gb_rpc_from=$(_gb_rpc_param "$1" '@.from')
_gb_rpc_to=$(_gb_rpc_param "$1" '@.to')
if [ -z "$_gb_rpc_from" ] || [ -z "$_gb_rpc_to" ]; then
_gb_rpc_error 'both "from" and "to" commit shas are required'
return 0
fi
_gb_rpc_out=$("$GB_BIN" diff "$_gb_rpc_from" "$_gb_rpc_to" 2>&1)
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -ne 0 ]; then
_gb_rpc_error "$_gb_rpc_out"
return 0
fi
_gb_rpc_text diff "$_gb_rpc_out"
}
# gbrpc_config_diff -- "has the LIVE config drifted from the last commit",
# which the two-sha `diff` above cannot answer (that one compares two
# already-committed shas, ticket 14/R124 History). This wraps `gitbackup
# diff` (usr/sbin/gitbackup's cmd_diff, ticket 17) instead: it re-collects
# the current backup set and compares its manifest against the manifest of
# the last commit pushed to this device's own branch, field by field --
# catching a chmod/chown that `git diff`/`git status` never sees at all
# (same blob, same tree entry mode class). No params; `gitbackup diff`
# itself resolves the branch/prefix from UCI exactly like `run` does.
#
# `gitbackup diff` prints nothing at all when there is no difference (its
# own documented contract) and returns 0 either way once the comparison
# ran; a nonzero exit only ever means it could not even get as far as
# comparing (gb_die: bad config, exit 2; unreachable remote, exit 3) --
# turned into the same { "reason": ... } shape every other method here
# uses, never a bare exit code, so the overview indicator can tell "no
# drift" apart from "could not check" instead of defaulting either one to
# "all saved".
gbrpc_config_diff() {
_gb_rpc_out=$("$GB_BIN" diff 2>&1)
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -ne 0 ]; then
_gb_rpc_error "$_gb_rpc_out"
return 0
fi
if [ -n "$_gb_rpc_out" ]; then
printf '{ "differs": true, "text": %s }\n' "$(gb_json_str "$_gb_rpc_out")"
else
printf '{ "differs": false, "text": "" }\n'
fi
}
# gbrpc_audit_paths -- G02: what changed on this router since the base
# image but is NOT in the backup set. /overlay/upper is the standard
# OpenWrt overlayfs upper layer (everything the running system has written
# relative to the read-only squashfs lower layer); anything under it that
# is neither hard-excluded (/etc/gitbackup/**, this package's own secrets)
# nor already named by `sysupgrade -l` is exactly "changed and not backed
# up".
#
# Ticket 21: this used to walk /overlay/upper and call `sysupgrade -l`
# itself -- no user-controlled input reaches it (no params at all), so it
# was never the injection risk history/diff/list_paths were, but it is
# still an external command run directly by this file instead of by
# `gitbackup`, which "только gitbackup" does not carve out an exception
# for. `gitbackup paths audit` (usr/sbin/gitbackup, ticket 21) is the exact
# same walk, moved rather than rewritten -- including the VERIFY below,
# which is about the walk itself, not about which process performs it.
#
# VERIFY: measured only that /overlay/upper does not exist inside the
# owlab package-test container used to develop this file (a plain
# extracted rootfs, never booted through OpenWrt's own fstab/mount_root) --
# not confirmed against a real overlay mount on the owrt2512 stand. Falls
# back to an empty list rather than erroring when the directory is absent,
# so a router (or dev container) without a live overlay mount still gets a
# valid, honestly-empty answer instead of a broken one.
gbrpc_audit_paths() {
_gb_rpc_out=$("$GB_BIN" paths audit 2>&1)
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -eq 0 ]; then
printf '%s\n' "$_gb_rpc_out"
else
_gb_rpc_error "$_gb_rpc_out"
fi
}
# --- write tier ----------------------------------------------------------
# gbrpc_run/gbrpc_test/gbrpc_restore all share one shape: the operation can
# run past any sane ubus call timeout (network waits, `test`'s own
# ConnectTimeout=15 twice over, `run`'s full collect/push cycle), so none
# of them wait for the CLI to finish. Each backgrounds it, detached from
# this script's own stdin/stdout/stderr (`</dev/null >/dev/null 2>&1 &`,
# the same detach shape etc/init.d/gitbackup's own config_change debounce
# timer already uses and that this project's tests have already proven
# survives its parent exiting), and answers immediately with
# `{ "started": true }`. The operator watches progress through `log`
# (ticket 10 acceptance criterion: "результат забирается через log") --
# every one of these already logs via gb_log under the shared "gitbackup"
# syslog tag, so no new log format is needed here.
gbrpc_run() {
"$GB_BIN" run --trigger luci </dev/null >/dev/null 2>&1 &
printf '{ "started": true }\n'
}
gbrpc_test() {
"$GB_BIN" test </dev/null >/dev/null 2>&1 &
printf '{ "started": true }\n'
}
# gbrpc_hostkey -- ticket 20. gbrpc_test above backgrounds `gitbackup test`
# and answers immediately, so it can never hand the operator an SSH host
# key's fingerprint to confirm -- and `gitbackup test`'s own stdin is
# `</dev/null` by construction here, so its interactive accept-or-decline
# prompt (auth.sh's gb_accept_hostkey) can never be answered from the web
# at all. This is the one new rpcd method that closes that gap, wrapping
# `gitbackup hostkey` (usr/sbin/gitbackup's cmd_hostkey) -- itself
# synchronous and fast (a single SSH host-key dial, ConnectTimeout=15;
# `accept` makes no network call at all), unlike run/test/restore above.
#
# One method serves both steps of the flow, picked by whether "fingerprint"
# was given: absent/empty -> `hostkey show` (dial, report, write nothing);
# present -> `hostkey accept <fingerprint>` (commit ONLY if it matches what
# is actually cached from the last `show` -- gb_hostkey_accept's own job,
# not this file's). Never accepts a host key with no fingerprint given: an
# empty "fingerprint" here can only mean "show", not "accept anything".
gbrpc_hostkey() {
_gb_rpc_fp=$(_gb_rpc_param "$1" '@.fingerprint')
if [ -n "$_gb_rpc_fp" ]; then
_gb_rpc_out=$("$GB_BIN" hostkey accept "$_gb_rpc_fp" 2>&1)
else
_gb_rpc_out=$("$GB_BIN" hostkey show 2>&1)
fi
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -eq 0 ]; then
printf '%s\n' "$_gb_rpc_out"
else
_gb_rpc_error "$_gb_rpc_out"
fi
}
# gbrpc_keygen -- ticket 25. "force" with no "confirm" (or the wrong one)
# no longer regenerates anything over an existing key: `gitbackup keygen
# --force` now only prints the current key's fingerprint and refuses (exit
# 4, cmd_keygen's own gb_die message) -- exactly the same show-then-name-
# it-back shape gbrpc_hostkey above already uses for the host-key prompt.
# The fingerprint is pulled back out of cmd_keygen's own first stdout line
# ("confirm-required <fingerprint>", relayed verbatim ahead of its
# stderr explanation) rather than out of the human-readable message text,
# so a future wording change to that message cannot silently break this
# parse.
gbrpc_keygen() {
_gb_rpc_force=$(_gb_rpc_param "$1" '@.force')
_gb_rpc_confirm=$(_gb_rpc_param "$1" '@.confirm')
_gb_rpc_args=''
case "$_gb_rpc_force" in true | 1) _gb_rpc_args='--force' ;; esac
if [ -n "$_gb_rpc_confirm" ]; then
# shellcheck disable=SC2086 # $_gb_rpc_args is empty or exactly one flag token
_gb_rpc_out=$("$GB_BIN" keygen $_gb_rpc_args --confirm "$_gb_rpc_confirm" 2>&1)
else
# shellcheck disable=SC2086 # $_gb_rpc_args is empty or exactly one flag token
_gb_rpc_out=$("$GB_BIN" keygen $_gb_rpc_args 2>&1)
fi
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -eq 0 ]; then
printf '{ "ok": true, "message": %s }\n' "$(gb_json_str "$_gb_rpc_out")"
return
fi
if [ "$_gb_rpc_rc" -eq 4 ]; then
_gb_rpc_fp=$(printf '%s\n' "$_gb_rpc_out" | sed -n '1s/^confirm-required //p')
printf '{ "ok": false, "confirm_required": true, "fingerprint": %s, "reason": %s }\n' \
"$(gb_json_str "$_gb_rpc_fp")" "$(gb_json_str "$_gb_rpc_out")"
return
fi
printf '{ "ok": false, "reason": %s }\n' "$(gb_json_str "$_gb_rpc_out")"
}
# gbrpc_set_secret -- writes gitbackup.origin.token_file directly (there is
# no CLI subcommand for this: the CLI never accepts a secret as an
# argument, spec "Токен... никогда в argv", and a web form field is not
# argv, but it is still not going anywhere near UCI -- R109, "/etc/config/*
# входит в sysupgrade -l"). Never logs the value: gb_log calls below name
# only the file path, exactly like gb_keygen's own logging does for the
# key it writes.
#
# An empty value clears the secret rather than writing a lone newline: a
# 1-byte file would still pass cmd_status's own `[ -s "$token_file" ]`
# check and misreport token_set=true for a field the operator just cleared.
gbrpc_set_secret() {
_gb_rpc_value=$(_gb_rpc_param "$1" '@.value')
_gb_rpc_file=$(gb_uci_get gitbackup.origin.token_file /etc/gitbackup/token)
if [ -z "$_gb_rpc_file" ]; then
_gb_rpc_error 'gitbackup.origin.token_file is empty'
return 0
fi
_gb_rpc_dir=$(dirname "$_gb_rpc_file")
mkdir -p "$_gb_rpc_dir" 2>/dev/null
chmod 0700 "$_gb_rpc_dir" 2>/dev/null
if [ -n "$_gb_rpc_value" ]; then
printf '%s\n' "$_gb_rpc_value" >"$_gb_rpc_file"
else
: >"$_gb_rpc_file"
fi
if ! chmod 0600 "$_gb_rpc_file" 2>/dev/null; then
_gb_rpc_error "could not set permissions on $_gb_rpc_file"
return 0
fi
gb_log notice "luci.gitbackup set_secret: wrote $_gb_rpc_file"
_gb_rpc_token_set=false
[ -s "$_gb_rpc_file" ] && _gb_rpc_token_set=true
printf '{ "ok": true, "token_set": %s }\n' "$_gb_rpc_token_set"
}
# gbrpc_set_paths -- writes /etc/sysupgrade.conf, one RAW path per line,
# exactly as given (a directory line included, never expanded -- that
# expansion is sysupgrade's own job at upgrade time, not this method's).
#
# Every entry is validated through paths.sh's own gb_paths_validate --
# the same function the CLI's `gitbackup paths add` already calls -- not a
# second, hand-rolled copy of its rules. Previously only two of its four
# checks (absolute, the fixed blacklist) were duplicated here, leaving a
# space in the path and a nonexistent path to the LuCI view's own JS to
# catch; that was never a safe backstop, because a write straight to flash
# is not something to trust a client's own validation for, and this
# method is not the LuCI view's exclusive caller. Routing through the one
# shared validator also fixes the JS view's own long-flagged gap: it could
# never reliably tell a dangling symlink (a legitimate entry, e.g.
# /etc/resolv.conf) from a genuinely missing path, because no rpcd ACL
# anywhere grants `fs.lstat`. This function runs ON the router, so `-e ||
# -L` (gb_paths_validate's own check) sees the real filesystem and gets it
# right without needing that ACL grant at all.
#
# Rejected entries are reported, not silently dropped (spec: "отклоняются
# с объяснением, а не молча"), but do not abort the whole write -- the
# remaining, valid paths are still saved.
#
# Ticket 26: this used to write the validated entries as the ENTIRE new
# file (`mv "$_gb_rpc_tmp" "$GB_SYSUPGRADE_CONF"`, nothing else). The LuCI
# Paths view only ever sends back gb_paths_entries' own list (comments
# excluded by design, see gbrpc_list_paths's own header above), so that
# used to mean the first `set_paths` call against a stock
# /etc/sysupgrade.conf -- one Add or Remove click anywhere -- silently
# erased its two header-comment lines and its two commented-out example
# lines, because this method never looked at them at all before
# overwriting the file. paths.sh's own gb_paths_replace_entries is the
# fix: it re-reads GB_SYSUPGRADE_CONF's comment/blank lines off disk right
# before the write and keeps them, unconditionally, regardless of what
# this call's "paths" param contained -- so a save from the view can never
# lose a line it was never shown. What still writes back verbatim,
# unexpanded, is exactly the caller's own entries -- ticket 13's decision.
gbrpc_set_paths() {
_gb_rpc_dir=$(dirname "$GB_SYSUPGRADE_CONF")
mkdir -p "$_gb_rpc_dir" 2>/dev/null
_gb_rpc_tmp="${GB_SYSUPGRADE_CONF}.newtmp.$$"
_gb_rpc_kept_f="${_gb_rpc_tmp}.kept"
_gb_rpc_rej_f="${_gb_rpc_tmp}.rej"
: >"$_gb_rpc_tmp"
: >"$_gb_rpc_kept_f"
: >"$_gb_rpc_rej_f"
# One ready-made JSON fragment per line in each scratch file --
# gb_json_str already escapes any real newline in its input (lib.sh),
# so a line-at-a-time read below can never split a value in half. This
# validation loop runs in its own subshell (the pipe from
# _gb_rpc_param_list), which is fine: it only ever writes to these two
# files, never to a variable this function reads back afterward.
_gb_rpc_param_list "$1" '@.paths[*]' | while IFS= read -r _gb_rpc_p || [ -n "$_gb_rpc_p" ]; do
[ -n "$_gb_rpc_p" ] || continue
_gb_rpc_reason=$(gb_paths_validate "$_gb_rpc_p" 2>&1 >/dev/null)
if [ -n "$_gb_rpc_reason" ]; then
printf '{ "path": %s, "reason": %s }\n' \
"$(gb_json_str "$_gb_rpc_p")" "$(gb_json_str "$_gb_rpc_reason")" >>"$_gb_rpc_rej_f"
else
printf '%s\n' "$_gb_rpc_p" >>"$_gb_rpc_tmp"
printf '%s\n' "$(gb_json_str "$_gb_rpc_p")" >>"$_gb_rpc_kept_f"
fi
done
gb_paths_replace_entries "$(cat "$_gb_rpc_tmp" 2>/dev/null)"
rm -f "$_gb_rpc_tmp"
printf '{ "ok": true, "written": ['
_gb_rpc_first=1
while IFS= read -r _gb_rpc_j || [ -n "$_gb_rpc_j" ]; do
[ -n "$_gb_rpc_j" ] || continue
[ "$_gb_rpc_first" -eq 1 ] || printf ','
_gb_rpc_first=0
printf '%s' "$_gb_rpc_j"
done <"$_gb_rpc_kept_f"
printf '], "rejected": ['
_gb_rpc_first=1
while IFS= read -r _gb_rpc_j || [ -n "$_gb_rpc_j" ]; do
[ -n "$_gb_rpc_j" ] || continue
[ "$_gb_rpc_first" -eq 1 ] || printf ','
_gb_rpc_first=0
printf '%s' "$_gb_rpc_j"
done <"$_gb_rpc_rej_f"
printf '] }\n'
rm -f "$_gb_rpc_kept_f" "$_gb_rpc_rej_f"
}
gbrpc_restore() {
_gb_rpc_device=$(_gb_rpc_param "$1" '@.device')
_gb_rpc_commit=$(_gb_rpc_param "$1" '@.commit')
_gb_rpc_dry=$(_gb_rpc_param "$1" '@.dry_run')
_gb_rpc_force=$(_gb_rpc_param "$1" '@.force')
_gb_rpc_pkgs=$(_gb_rpc_param "$1" '@.with_packages')
if [ -z "$_gb_rpc_device" ]; then
_gb_rpc_error 'device is required'
return 0
fi
set -- --device "$_gb_rpc_device" --yes
[ -n "$_gb_rpc_commit" ] && set -- "$@" --commit "$_gb_rpc_commit"
case "$_gb_rpc_dry" in true | 1) set -- "$@" --dry-run ;; esac
case "$_gb_rpc_force" in true | 1) set -- "$@" --force ;; esac
case "$_gb_rpc_pkgs" in true | 1) set -- "$@" --with-packages ;; esac
"$GB_BIN" restore "$@" </dev/null >/dev/null 2>&1 &
printf '{ "started": true }\n'
}
# gbrpc_card -- card.sh (ticket 09) may not exist yet on a system built
# ahead of it landing; the same tolerance usr/sbin/gitbackup's own cmd_run
# already applies, not a stub invented for this file only.
gbrpc_card() {
if ! command -v gb_card >/dev/null 2>&1; then
_gb_rpc_error 'the recovery card generator is not installed on this system yet'
return 0
fi
_gb_rpc_file=$(mktemp "${TMPDIR:-/tmp}/gbrpc-card.XXXXXX") || {
_gb_rpc_error 'could not create a scratch file under /tmp'
return 0
}
if gb_card "$_gb_rpc_file" 2>/dev/null; then
_gb_rpc_text card "$(cat "$_gb_rpc_file")"
else
_gb_rpc_error 'the recovery card could not be generated, see the log'
fi
rm -f "$_gb_rpc_file"
}
# ---------------------------------------------------------------------------
# rpcd entry point
# ---------------------------------------------------------------------------
case "${1:-}" in
list)
cat <<'EOF'
{
"status": {},
"log": { "lines": 0 },
"diff": { "from": "", "to": "" },
"config_diff": {},
"pubkey": {},
"list_paths": {},
"history": { "limit": 0 },
"audit_paths": {},
"validate_cron": { "expr": "" },
"run": {},
"keygen": { "force": true, "confirm": "" },
"test": {},
"hostkey": { "fingerprint": "" },
"set_secret": { "value": "" },
"set_paths": { "paths": [] },
"restore": { "device": "", "commit": "", "dry_run": true, "force": true, "with_packages": true },
"card": {}
}
EOF
;;
call)
# `|| _gb_rpc_input='{}'` here (rather than `|| true`) would silently
# DISCARD a real, fully-read line: confirmed live against the actual
# rpcd daemon (not just a manual `echo ... | plugin call` test,
# which never reproduced this) -- rpcd's own call params arrive on
# stdin with NO trailing newline, so `read -r` reads the whole line
# correctly but still returns nonzero for hitting EOF instead of a
# newline. Every parameterized method (log/diff/history/
# validate_cron/set_secret/set_paths/restore) would otherwise always
# see "{}" and never its real arguments, while a hand-typed
# `printf '...\n' | ... call ...` test -- which DOES add a trailing
# newline -- would keep looking correct throughout development.
IFS= read -r _gb_rpc_input || true
[ -n "$_gb_rpc_input" ] || _gb_rpc_input='{}'
case "${2:-}" in
status) gbrpc_status ;;
log) gbrpc_log "$_gb_rpc_input" ;;
diff) gbrpc_diff "$_gb_rpc_input" ;;
config_diff) gbrpc_config_diff ;;
pubkey) gbrpc_pubkey ;;
list_paths) gbrpc_list_paths ;;
history) gbrpc_history "$_gb_rpc_input" ;;
audit_paths) gbrpc_audit_paths ;;
validate_cron) gbrpc_validate_cron "$_gb_rpc_input" ;;
run) gbrpc_run ;;
keygen) gbrpc_keygen "$_gb_rpc_input" ;;
test) gbrpc_test ;;
hostkey) gbrpc_hostkey "$_gb_rpc_input" ;;
set_secret) gbrpc_set_secret "$_gb_rpc_input" ;;
set_paths) gbrpc_set_paths "$_gb_rpc_input" ;;
restore) gbrpc_restore "$_gb_rpc_input" ;;
card) gbrpc_card ;;
*) _gb_rpc_error "unknown method '${2:-}'" ;;
esac
;;
esac
exit 0