mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-28 16:22:51 +08:00
🍉 Sync 2026-09-25 18:00:12
This commit is contained in:
1 parent
7c17dda4d0
commit
2ee65a261e
135 files changed
+5726
-7799
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user