#!/bin/sh # shellcheck shell=sh # # 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 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 . "$GB_SHARE/lib.sh" # shellcheck disable=SC1091 . "$GB_SHARE/schedule.sh" # shellcheck disable=SC1091 . "$GB_SHARE/paths.sh" # shellcheck disable=SC1091 . "$GB_SHARE/card.sh" _gb_rpc_param() { printf '%s' "$1" | jsonfilter -e "$2" 2>/dev/null } _gb_rpc_error() { printf '{ "reason": %s }\n' "$(gb_json_str "$1")" } _gb_rpc_text() { printf '{ "%s": %s }\n' "$1" "$(gb_json_str "$2")" } # _gb_rpc_relay -- "-" relays JSON as is, else # wraps as {"": ...}. 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) 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_relay pubkey pubkey } gbrpc_list_paths() { _gb_rpc_relay - paths list --json } gbrpc_audit_paths() { _gb_rpc_relay - paths audit } gbrpc_validate_cron() { _gb_rpc_expr=$(_gb_rpc_param "$1" '@.expr') 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() { _gb_rpc_limit=$(_gb_rpc_param "$1" '@.limit') case "$_gb_rpc_limit" in '' | *[!0-9]*) _gb_rpc_limit=20 ;; esac _gb_rpc_relay - history --limit "$_gb_rpc_limit" } # 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') 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_relay diff diff "$_gb_rpc_from" "$_gb_rpc_to" } # Live config vs last commit, by manifest -- sees a chmod `git diff` cannot. gbrpc_config_diff() { if ! _gb_rpc_out=$("$GB_BIN" diff 2>&1); then _gb_rpc_error "$_gb_rpc_out" 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_run() { "$GB_BIN" run --trigger luci /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 2>&1 & printf '{ "started": true }\n' } # 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_relay - hostkey accept "$_gb_rpc_fp" else _gb_rpc_relay - hostkey show fi } # keygen over an existing key needs --force --confirm ; 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') 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")" 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")" else printf '{ "ok": false, "reason": %s }\n' "$(gb_json_str "$_gb_rpc_out")" fi } # 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) 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" } # Writes accepted entries verbatim, never the expanded effective set: files # created later under a directory would silently stop being backed up. gbrpc_set_paths() { mkdir -p "$(dirname "$GB_SYSUPGRADE_CONF")" 2>/dev/null _gb_rpc_tmp="${GB_SYSUPGRADE_CONF}.newtmp.$$" : >"$_gb_rpc_tmp" : >"$_gb_rpc_tmp.kept" : >"$_gb_rpc_tmp.rej" _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_tmp.rej" else printf '%s\n' "$_gb_rpc_p" >>"$_gb_rpc_tmp" 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)" 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') _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 2>&1 & printf '{ "started": true }\n' } gbrpc_card() { _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" } 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) # 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 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