🍉 Sync 2026-09-25 18:00:12

This commit is contained in:
github-actions[bot] committed 2026-09-25 18:00:12 +08:00
1 parent 7c17dda4d0
commit 2ee65a261e
135 files changed
+5726 -7799

No files matched your search

+75 -437
View File
@@ -1,53 +1,10 @@
#!/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.
# rpcd plugin for ubus object "luci.gitbackup": translates calls into
# `gitbackup` CLI runs. Always prints JSON and exits 0: a non-zero exit would
# become a bodyless ubus error. Failures answer {"reason": ...}.
# The method list is mirrored in acl.d/luci-app-gitbackup.json (tested).
set -u
@@ -56,73 +13,49 @@ 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
# shellcheck disable=SC1091 # GB_SHARE is a runtime path
. "$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
# shellcheck disable=SC1091
. "$GB_SHARE/card.sh"
# ---------------------------------------------------------------------------
# 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 ---------------------------------------------------------
# _gb_rpc_relay <key|-> <gitbackup args...> -- "-" relays JSON as is, else
# wraps as {"<key>": ...}. stderr stays out of a success answer: one gb_log
# notice in front of the JSON breaks the whole reply.
_gb_rpc_relay() {
_gb_rpc_key="$1"
shift
_gb_rpc_errf=$(mktemp "${TMPDIR:-/tmp}/gbrpc-err.XXXXXX") || {
_gb_rpc_error 'could not create a scratch file under /tmp'
return 0
}
_gb_rpc_out=$("$GB_BIN" "$@" 2>"$_gb_rpc_errf")
_gb_rpc_rc=$?
_gb_rpc_err=$(cat "$_gb_rpc_errf")
rm -f "$_gb_rpc_errf"
if [ "$_gb_rpc_rc" -ne 0 ]; then
_gb_rpc_error "$(printf '%s\n%s' "$_gb_rpc_err" "$_gb_rpc_out" | grep .)"
elif [ "$_gb_rpc_key" = - ]; then
printf '%s\n' "$_gb_rpc_out"
else
_gb_rpc_text "$_gb_rpc_key" "$_gb_rpc_out"
fi
}
gbrpc_status() {
_gb_rpc_out=$("$GB_BIN" status 2>/dev/null)
@@ -140,132 +73,34 @@ gbrpc_log() {
}
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
_gb_rpc_relay pubkey pubkey
}
# 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
_gb_rpc_relay - paths list --json
}
gbrpc_audit_paths() {
_gb_rpc_relay - paths audit
}
# 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")"
if _gb_rpc_why=$(gb_cron_valid "$_gb_rpc_expr" 2>&1 >/dev/null); then
printf '{ "valid": true }\n'
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
_gb_rpc_relay - history --limit "$_gb_rpc_limit"
}
# 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.
# In the ACL's WRITE tier: it returns backed-up content (shadow hash, host
# keys, PSKs); tiers split on "may see secrets", not "changes something".
gbrpc_diff() {
_gb_rpc_from=$(_gb_rpc_param "$1" '@.from')
_gb_rpc_to=$(_gb_rpc_param "$1" '@.to')
@@ -273,184 +108,65 @@ gbrpc_diff() {
_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"
_gb_rpc_relay diff diff "$_gb_rpc_from" "$_gb_rpc_to"
}
# 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".
# Live config vs last commit, by manifest -- sees a chmod `git diff` cannot.
gbrpc_config_diff() {
_gb_rpc_out=$("$GB_BIN" diff 2>&1)
_gb_rpc_rc=$?
if [ "$_gb_rpc_rc" -ne 0 ]; then
if ! _gb_rpc_out=$("$GB_BIN" diff 2>&1); then
_gb_rpc_error "$_gb_rpc_out"
return 0
fi
if [ -n "$_gb_rpc_out" ]; then
elif [ -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'
}
# stdin is /dev/null, so an unknown host key ends as "needs confirmation";
# the web flow then goes through `hostkey`.
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".
# No fingerprint: show the remote's host key. With one: accept exactly it.
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)
_gb_rpc_relay - hostkey accept "$_gb_rpc_fp"
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"
_gb_rpc_relay - hostkey show
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.
# keygen over an existing key needs --force --confirm <fingerprint>; exit 4
# means "confirm first" and carries the fingerprint on its first line.
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
set -- keygen
case "$_gb_rpc_force" in true | 1) set -- "$@" --force ;; esac
[ -z "$_gb_rpc_confirm" ] || set -- "$@" --confirm "$_gb_rpc_confirm"
_gb_rpc_out=$("$GB_BIN" "$@" 2>&1)
_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
elif [ "$_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
else
printf '{ "ok": false, "reason": %s }\n' "$(gb_json_str "$_gb_rpc_out")"
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.
# Writes the token to token_file (0600 in a 0700 dir). The value never goes
# to argv, UCI or the log; there is no method to read it back.
gbrpc_set_secret() {
_gb_rpc_value=$(_gb_rpc_param "$1" '@.value')
_gb_rpc_file=$(gb_uci_get gitbackup.origin.token_file /etc/gitbackup/token)
@@ -479,95 +195,37 @@ gbrpc_set_secret() {
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.
# Writes accepted entries verbatim, never the expanded effective set: files
# created later under a directory would silently stop being backed up.
gbrpc_set_paths() {
_gb_rpc_dir=$(dirname "$GB_SYSUPGRADE_CONF")
mkdir -p "$_gb_rpc_dir" 2>/dev/null
mkdir -p "$(dirname "$GB_SYSUPGRADE_CONF")" 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"
: >"$_gb_rpc_tmp.kept"
: >"$_gb_rpc_tmp.rej"
# 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
_gb_rpc_param "$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"
"$(gb_json_str "$_gb_rpc_p")" "$(gb_json_str "$_gb_rpc_reason")" >>"$_gb_rpc_tmp.rej"
else
printf '%s\n' "$_gb_rpc_p" >>"$_gb_rpc_tmp"
printf '%s\n' "$(gb_json_str "$_gb_rpc_p")" >>"$_gb_rpc_kept_f"
gb_json_str "$_gb_rpc_p" >>"$_gb_rpc_tmp.kept"
echo >>"$_gb_rpc_tmp.kept"
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"
printf '{ "ok": true, "written": [%s], "rejected": [%s] }\n' \
"$(gb_join_lines , <"$_gb_rpc_tmp.kept")" "$(gb_join_lines , <"$_gb_rpc_tmp.rej")"
rm -f "$_gb_rpc_tmp" "$_gb_rpc_tmp.kept" "$_gb_rpc_tmp.rej"
}
# Backgrounded like run/test; --yes because the view already asked twice.
gbrpc_restore() {
_gb_rpc_device=$(_gb_rpc_param "$1" '@.device')
_gb_rpc_commit=$(_gb_rpc_param "$1" '@.commit')
@@ -590,14 +248,7 @@ gbrpc_restore() {
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
@@ -610,10 +261,6 @@ gbrpc_card() {
rm -f "$_gb_rpc_file"
}
# ---------------------------------------------------------------------------
# rpcd entry point
# ---------------------------------------------------------------------------
case "${1:-}" in
list)
cat <<'EOF'
@@ -639,17 +286,8 @@ case "${1:-}" in
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.
# rpcd sends params with NO trailing newline: `read` fills the
# variable but returns 1, so `|| X='{}'` would drop every parameter.
IFS= read -r _gb_rpc_input || true
[ -n "$_gb_rpc_input" ] || _gb_rpc_input='{}'
case "${2:-}" in