mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 05:25:13 +08:00
🍉 Sync 2026-09-25 18:00:12
This commit is contained in:
1 parent
1469c77f47
commit
3eae539433
135 files changed
+5726
-7799
No files matched your search
+1
-1
@@ -7,7 +7,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=airconnect
|
||||
PKG_VERSION:=1.12.2
|
||||
PKG_VERSION:=1.12.3
|
||||
PKG_RELEASE=1
|
||||
|
||||
PKG_SOURCE:=AirConnect-$(PKG_VERSION).zip
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=cloudflared-next
|
||||
PKG_VERSION:=2026.9.1
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=2026.9.3
|
||||
PKG_RELEASE:=2
|
||||
|
||||
# Architecture mapping for pre-built binaries
|
||||
ifeq ($(ARCH),x86_64)
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.09.23
|
||||
PKG_RELEASE:=47
|
||||
PKG_VERSION:=2026.09.24
|
||||
PKG_RELEASE:=48
|
||||
|
||||
PKG_SOURCE:=dae-src-2026.09.23-c9f580bcaa8a.tar.gz
|
||||
PKG_SOURCE:=dae-src-2026.09.24-17135da7c881.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
+1
-1
@@ -20,7 +20,7 @@ start_service() {
|
||||
local config_file
|
||||
config_get config_file "config" "config_file" "/etc/dae/config.dae"
|
||||
|
||||
"$PROG" validate -c "$config_file" || return 1
|
||||
DAE_LOCATION_ASSET="/usr/share/v2ray" "$PROG" validate -c "$config_file" || return 1
|
||||
|
||||
local log_maxbackups log_maxsize
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION:=2026.09.23
|
||||
PKG_RELEASE:=58
|
||||
PKG_VERSION:=2026.09.24
|
||||
PKG_RELEASE:=59
|
||||
|
||||
PKG_SOURCE:=daed-src-2026.09.23-fcc80bac4542.tar.gz
|
||||
PKG_SOURCE:=daed-src-2026.09.24-527bb166b1ae.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-09-23
|
||||
Subject: [PATCH] daed: expose config marshaller bytes
|
||||
|
||||
The current wing generator calls Marshaller.Bytes when updating global config.
|
||||
---
|
||||
diff --git a/dae-core/config/marshal.go b/dae-core/config/marshal.go
|
||||
index f68006a..dfd6777 100644
|
||||
--- a/dae-core/config/marshal.go
|
||||
+++ b/dae-core/config/marshal.go
|
||||
@@ -42,6 +42,10 @@ type Marshaller struct {
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
+func (m *Marshaller) Bytes() []byte {
|
||||
+ return m.buf.Bytes()
|
||||
+}
|
||||
+
|
||||
func (m *Marshaller) writeLine(depth int, line string) {
|
||||
if depth < 0 {
|
||||
depth = 0
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dnsproxy
|
||||
PKG_VERSION:=0.84.2
|
||||
PKG_RELEASE:=18
|
||||
PKG_VERSION:=0.85.0
|
||||
PKG_RELEASE:=19
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/AdguardTeam/dnsproxy/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2026 VizzleTF
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gitbackup
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
PKG_MAINTAINER:=VizzleTF <vizzlef@gmail.com>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/gitbackup
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
TITLE:=Automatic configuration backup to a Git repository
|
||||
# Every entry earns its place with a measured 25.12.4 failure, not habit
|
||||
# (spec, "Проверенные факты"): stat is entirely absent from the base image
|
||||
# and the manifest depends on it (coreutils-stat); dbclient cannot read
|
||||
# OpenSSH-format keys or honor UserKnownHostsFile, so ssh needs the real
|
||||
# openssh client and keygen; git-http is required for `git fetch https://`
|
||||
# (fails with "remote-https is not a git command" otherwise). flock is a
|
||||
# busybox applet and needs nothing here.
|
||||
DEPENDS:=+git +git-http +ca-bundle +jsonfilter +coreutils-stat +openssh-client +openssh-keygen
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/gitbackup/description
|
||||
gitbackup commits the router's configuration to a private git repository on
|
||||
a schedule, without keeping a working copy of that repository on flash, and
|
||||
restores it back onto a freshly flashed router.
|
||||
endef
|
||||
|
||||
define Package/gitbackup/conffiles
|
||||
/etc/config/gitbackup
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/gitbackup/install
|
||||
$(INSTALL_DIR) $(1)
|
||||
$(CP) ./files/* $(1)/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,gitbackup))
|
||||
@@ -2,18 +2,8 @@
|
||||
# shellcheck shell=sh
|
||||
# shellcheck disable=SC2034 # START/USE_PROCD/EXTRA_COMMANDS are read by rc.common, not this file
|
||||
#
|
||||
# gitbackup runs from cron, not as a daemon, so there is no process for
|
||||
# start_service to supervise. This file's job is: (1) apply
|
||||
# gitbackup.main.schedule to /etc/crontabs/root and make crond pick it up
|
||||
# (reload_service, ticket 06), and (2) watch the packages actually in the
|
||||
# backup set for config.change and turn a burst of edits into one debounced
|
||||
# `gitbackup run` (service_triggers/config_change, spec R106/R107).
|
||||
#
|
||||
# GB_SHARE/GB_STATE_DIR/GB_BIN are overridable only so tests/run.sh can
|
||||
# source this file directly (rc.common's own dispatch is not exercised by
|
||||
# the unit tests -- see this file's own functions being called straight
|
||||
# from a test body) against a tree, state directory and gitbackup binary
|
||||
# that are not the router's real ones; the router never sets any of them.
|
||||
# Not a daemon: applies the cron schedule and debounces config.change events
|
||||
# of backed-up packages into one `gitbackup run`.
|
||||
|
||||
GB_SHARE="${GB_SHARE:-/usr/share/gitbackup}"
|
||||
GB_STATE_DIR="${GB_STATE_DIR:-/var/run/gitbackup}"
|
||||
@@ -29,11 +19,7 @@ GB_BIN="${GB_BIN:-/usr/sbin/gitbackup}"
|
||||
START=60
|
||||
USE_PROCD=1
|
||||
|
||||
# config_change is not one of rc.common's built-in actions (start/stop/
|
||||
# reload/...); EXTRA_COMMANDS is rc.common's own mechanism for adding one
|
||||
# (it appends this to ALL_COMMANDS right after sourcing this file -- see
|
||||
# /etc/rc.common on the stand), which is what lets procd's run_script
|
||||
# target `/etc/init.d/gitbackup config_change` actually dispatch anywhere.
|
||||
# Lets rc.common dispatch the procd trigger target `config_change`.
|
||||
EXTRA_COMMANDS="config_change"
|
||||
EXTRA_HELP=" config_change Internal: debounced trigger used by service_triggers, not for interactive use"
|
||||
|
||||
@@ -41,52 +27,17 @@ start_service() {
|
||||
reload_service
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
:
|
||||
}
|
||||
|
||||
# reload_service -- (re)applies gitbackup.main.schedule/cron_expr to the
|
||||
# crontab and makes crond pick it up. Runs on `/etc/init.d/gitbackup
|
||||
# reload|restart|start` and, through procd_add_reload_trigger below,
|
||||
# whenever gitbackup's own UCI config changes (e.g. a Settings save in
|
||||
# LuCI) -- so flipping schedule from daily to weekly takes effect without a
|
||||
# manual reload.
|
||||
reload_service() {
|
||||
gb_cron_apply
|
||||
}
|
||||
|
||||
# _gb_backed_up_packages -- private. The set of UCI packages the config-
|
||||
# change trigger below watches: every /etc/config/<pkg> that is actually
|
||||
# part of the backup set, per `sysupgrade -l` (spec: "список берётся из
|
||||
# sysupgrade -l, фильтруется до /etc/config/*"), minus gitbackup's own
|
||||
# config -- that one already gets procd_add_reload_trigger's ordinary
|
||||
# `reload` below, which re-applies the schedule, not a debounced backup
|
||||
# run; watching it a second time here would just fire both for the same
|
||||
# edit.
|
||||
# UCI packages in the backup set, minus gitbackup (has the reload trigger).
|
||||
_gb_backed_up_packages() {
|
||||
sysupgrade -l 2>/dev/null | sed -n 's#^/etc/config/##p' | sort -u | grep -v '^gitbackup$'
|
||||
}
|
||||
|
||||
# config_change -- procd's run_script target for a backed-up package's
|
||||
# config.change event, registered once per package in service_triggers
|
||||
# below. Does not run a backup on the spot: a save in LuCI can touch
|
||||
# several packages (e.g. network + firewall together) or the same one
|
||||
# repeatedly, and each touch fires this again -- so this only (re)arms a
|
||||
# single delayed `gitbackup run` after $debounce seconds, cancelling
|
||||
# whatever the previous call armed first. A burst of edits therefore
|
||||
# collapses into exactly one run (spec R106/R107), not one per edit.
|
||||
#
|
||||
# busybox ash's arithmetic (and `sleep`) only does whole seconds --
|
||||
# `sleep 0.3` is "invalid number", measured on the stand -- hence the
|
||||
# integer-only guard on $debounce below rather than trusting it unchecked.
|
||||
#
|
||||
# Killing the previous timer's PID only stops that subshell; if it had
|
||||
# already progressed past `sleep` (a race that needs the previous debounce
|
||||
# window to have fully elapsed a moment before this call, and is why a
|
||||
# closer-together burst is exactly what this exists to collapse), the
|
||||
# short-lived orphaned `sleep` left behind exits on its own and does
|
||||
# nothing further -- rm and run only happen after it, in the same
|
||||
# subshell, and that subshell is what got killed.
|
||||
# (Re)arm one delayed run, cancelling the previous timer.
|
||||
# busybox `sleep` takes whole seconds only, hence the integer guard.
|
||||
config_change() {
|
||||
_gb_debounce=$(gb_uci_get gitbackup.main.debounce 60)
|
||||
case "$_gb_debounce" in ''|*[!0-9]*) _gb_debounce=60 ;; esac
|
||||
@@ -94,18 +45,6 @@ config_change() {
|
||||
mkdir -p "$GB_STATE_DIR"
|
||||
_gb_pidfile="$GB_STATE_DIR/debounce.pid"
|
||||
|
||||
# Review finding, ticket 06: killing the PID from the pidfile blind is
|
||||
# fine right after this same router wrote it, but a long-uptime router
|
||||
# can wrap PIDs, and by the time the NEXT config.change fires that same
|
||||
# number may belong to an unrelated process this has no business
|
||||
# signaling. `kill -0` (send no signal, just ask whether the PID
|
||||
# exists) is the cheap half of that check -- it does not prove the PID
|
||||
# is still OUR sleep, only that it is not free for the kernel to have
|
||||
# reused by something else; reused PIDs are strictly the OS's own
|
||||
# concern here. Deliberately `kill -0`, not a `/proc/<pid>` test: this
|
||||
# file's own tests/run.sh unit test runs it on macOS, which has no
|
||||
# /proc at all, while `kill -0` is the one portable primitive both that
|
||||
# host and busybox ash on the router actually implement.
|
||||
if [ -f "$_gb_pidfile" ]; then
|
||||
_gb_prev_pid=$(cat "$_gb_pidfile")
|
||||
kill -0 "$_gb_prev_pid" 2>/dev/null && kill "$_gb_prev_pid" 2>/dev/null
|
||||
@@ -118,21 +57,9 @@ config_change() {
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "gitbackup"
|
||||
|
||||
# R106/R107 both live under one on/off switch: a router that wants
|
||||
# gitbackup to run strictly on its own schedule can turn the
|
||||
# config-change trigger off without touching the watched packages.
|
||||
[ "$(gb_uci_get gitbackup.main.on_config_change 1)" = 1 ] || return 0
|
||||
|
||||
# Same path resolution procd.sh's own _procd_add_reload_trigger uses
|
||||
# (readlink, falling back to $initscript itself when it is not a
|
||||
# symlink) -- $initscript is rc.common's own variable (set to "$1"
|
||||
# before this file is sourced, see /etc/rc.common on the stand), not
|
||||
# invented here; shellcheck cannot see that external assignment.
|
||||
# shellcheck disable=SC2154
|
||||
_gb_script=$(readlink "$initscript")
|
||||
_gb_name=$(basename "${_gb_script:-$initscript}")
|
||||
|
||||
for _gb_pkg in $(_gb_backed_up_packages); do
|
||||
procd_add_config_trigger "config.change" "$_gb_pkg" /etc/init.d/"$_gb_name" config_change
|
||||
procd_add_config_trigger "config.change" "$_gb_pkg" /etc/init.d/gitbackup config_change
|
||||
done
|
||||
}
|
||||
@@ -1,15 +1,7 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup: create the secrets directory once, at first boot after install.
|
||||
#
|
||||
# Not done in postinst -- postinst runs while an image is being built, where
|
||||
# there may be neither entropy nor a real, writable /etc yet, and the spec is
|
||||
# explicit that installing the package must not run a backup or generate keys.
|
||||
#
|
||||
# /etc/init.d/boot runs every /etc/uci-defaults/* script as
|
||||
# `( . "./$(basename $file)" )` and deletes it afterwards only if that exited
|
||||
# 0, so this file has no shebang and is not executable -- same convention as
|
||||
# base-files' own uci-defaults scripts (verified in the local checkout).
|
||||
# First boot, not postinst (postinst also runs at image build time).
|
||||
# Sourced by /etc/init.d/boot and deleted on exit 0: no shebang needed.
|
||||
|
||||
[ -d /etc/gitbackup ] || mkdir -m 0700 /etc/gitbackup
|
||||
chown root:root /etc/gitbackup
|
||||
|
||||
@@ -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
|
||||
|
||||
+168
-734
File diff suppressed because it is too large.
Load diff
@@ -1,46 +1,17 @@
|
||||
#!/bin/sh
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- GIT_ASKPASS helper (spec "Аутентификация").
|
||||
# GIT_ASKPASS helper, executed by git with the prompt in $1. The only reader
|
||||
# of the token file: the token reaches git through this pipe, never argv, a
|
||||
# URL or a log.
|
||||
#
|
||||
# Not sourced like the rest of usr/share/gitbackup/*.sh -- git execs this
|
||||
# directly (GIT_ASKPASS=/usr/share/gitbackup/askpass.sh, set by
|
||||
# auth.sh's gb_git_env) once per missing credential component, with the
|
||||
# prompt text as $1 ("Username for '...': ", "Password for '...': ", the
|
||||
# exact wording is git's own and varies by version), and reads exactly one
|
||||
# line back from this script's stdout.
|
||||
# Only the password prompt gets the token. git embeds the username in the
|
||||
# URL of its next prompt and passes that as argv here, so a token answered
|
||||
# as username would leak into argv (seen with GIT_TRACE=1).
|
||||
#
|
||||
# This is the only place gitbackup.origin.token_file is ever read for a
|
||||
# push: the token reaches git through this pipe, never through argv, a URL,
|
||||
# or a log line.
|
||||
#
|
||||
# The username prompt is answered with a fixed, non-secret placeholder --
|
||||
# NEVER the token -- and only the password prompt gets the token. This is
|
||||
# not the obvious design: an earlier version answered both prompts with the
|
||||
# token (a fine-grained PAT works as either field on GitHub, per its own
|
||||
# docs), on the reasoning that a value read from a file and only ever
|
||||
# piped to git's stdin cannot show up in `ps` because it was never a
|
||||
# command-line argument. That reasoning was wrong, caught only by actually
|
||||
# tracing a live request on the owlab stand (GIT_TRACE=1): git treats
|
||||
# whatever the username prompt returns as the credential's username, and
|
||||
# builds the URL for its *next* prompt -- "Password for
|
||||
# 'http://<username>@host:port': " -- with that value inline, then execs
|
||||
# this script again with that whole string as $1. A token answered as the
|
||||
# username therefore reappears as a literal substring of argv on the very
|
||||
# next invocation, which is precisely what this package must never do
|
||||
# (spec: "Токен... никогда в argv"). Answering the username with a fixed
|
||||
# placeholder instead means the value that could ever leak into an argv or
|
||||
# a URL is never the secret. Re-verified after the fix, same live trace:
|
||||
# the second prompt now reads "Password for
|
||||
# 'http://gitbackup@host:port': " -- the token appears nowhere in argv, at
|
||||
# any point, confirmed by sampling `ps` throughout the whole exchange.
|
||||
#
|
||||
# VERIFY: the placeholder username plus token-as-password shape is
|
||||
# confirmed against GitHub's own docs (a fine-grained PAT as the password
|
||||
# with any non-empty username); not confirmed live against GitLab/
|
||||
# Bitbucket/self-hosted Gitea, which needs a real hosted remote and real
|
||||
# credentials neither of which exist on this dev stand (spec.md, "Открытые
|
||||
# места": "push на живой GitHub/GitLab -- нет учётных данных на стенде").
|
||||
# VERIFY: placeholder-user + token-as-password is documented for GitHub PATs
|
||||
# only; not tested against GitLab/Bitbucket/Gitea.
|
||||
|
||||
set -u
|
||||
|
||||
GB_SHARE="${GB_SHARE:-/usr/share/gitbackup}"
|
||||
@@ -49,10 +20,7 @@ GB_SHARE="${GB_SHARE:-/usr/share/gitbackup}"
|
||||
|
||||
case "${1:-}" in
|
||||
*sername*)
|
||||
# Never the token: see this file's header. The exact string is
|
||||
# arbitrary -- git only requires it to be non-empty -- "gitbackup"
|
||||
# just makes a stray log line or trace recognizable as this
|
||||
# package's own request rather than a real account name.
|
||||
# Any non-empty value works; never the token.
|
||||
printf 'gitbackup\n'
|
||||
;;
|
||||
*)
|
||||
@@ -61,9 +29,7 @@ case "${1:-}" in
|
||||
gb_log err "askpass: no readable token at $_gb_token_file (gitbackup.origin.token_file)"
|
||||
exit 1
|
||||
fi
|
||||
# head -n 1, not cat: a trailing blank line or a second line in the
|
||||
# token file (a stray newline from how it was pasted in) must not
|
||||
# become part of what git sends as the credential.
|
||||
# First line only: a stray trailing newline must not reach git.
|
||||
head -n 1 "$_gb_token_file"
|
||||
;;
|
||||
esac
|
||||
@@ -1,86 +1,18 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- access to the remote (spec "Аутентификация").
|
||||
#
|
||||
# Exposes gb_git_env, gb_keygen, gb_pubkey and gb_accept_hostkey; every
|
||||
# _gb_auth_-prefixed helper below is private and namespaced by module, same
|
||||
# convention collect.sh uses (multiple *.sh here end up sourced into the
|
||||
# same process by usr/sbin/gitbackup, so a bare _gb_ helper name could
|
||||
# collide with a sibling module's). Sourced, never executed: nothing here
|
||||
# runs at load time -- GB_ETC_DIR below is a variable assignment from an
|
||||
# environment default, not a filesystem access.
|
||||
#
|
||||
# Depends on lib.sh (gb_log, gb_uci_get) already being sourced by the
|
||||
# caller, same convention every other module here uses.
|
||||
#
|
||||
# GB_ETC_DIR overrides gitbackup's own secrets directory (default
|
||||
# /etc/gitbackup). It is deliberately NOT a UCI option: gitbackup.origin.
|
||||
# key_file and .token_file already give the operator a path for those two,
|
||||
# but known_hosts has no such option in the shipped schema (spec:
|
||||
# "known_hosts лежит в /etc/gitbackup/", a fixed location, not configurable)
|
||||
# -- this exists only so tests/run.sh can point it at a tmp directory
|
||||
# instead of the real /etc/gitbackup, the same seam GB_ROOT/GB_STATE_DIR/
|
||||
# GB_SYSFS_NET give the other modules.
|
||||
# gitbackup -- remote access: gb_git_env, gb_keygen, gb_keygen_forget_old,
|
||||
# gb_pubkey, gb_accept_hostkey, gb_hostkey_show, gb_hostkey_accept. Needs lib.sh.
|
||||
GB_ETC_DIR="${GB_ETC_DIR:-/etc/gitbackup}"
|
||||
|
||||
# gb_git_env
|
||||
# gb_git_env -- "export KEY='VALUE'" lines for `eval "$(gb_git_env)"`.
|
||||
# ConnectTimeout: BatchMode does not cover the TCP handshake, and a host that
|
||||
# drops packets would hang ssh for minutes.
|
||||
#
|
||||
# Prints "export KEY='VALUE'" lines, one per line, quoted so a caller can
|
||||
# safely `eval "$(gb_git_env)"` even though GIT_SSH_COMMAND's value itself
|
||||
# contains spaces -- and so the assignments are exported into the calling
|
||||
# shell's environment, where the `git` child process this whole thing
|
||||
# exists for can actually see them.
|
||||
# Must be OpenSSH `ssh`, not dbclient: git's ssh-variant probe fails on it and
|
||||
# falls back to "simple", dropping every -o option, host-key checking included.
|
||||
#
|
||||
# GIT_SSH_COMMAND and GIT_ASKPASS are both always printed, regardless of
|
||||
# gitbackup.origin.auth: it is the git remote URL's own scheme (ssh:// vs
|
||||
# https://), not this option, that decides which one a given `git`
|
||||
# invocation actually uses -- ssh transport never calls GIT_ASKPASS for
|
||||
# anything, and GIT_SSH_COMMAND is simply inert for an https transport. Not
|
||||
# branching on .auth here means there is exactly one thing to keep in sync
|
||||
# with the remote URL parser (remoteurl.sh) instead of two.
|
||||
#
|
||||
# GIT_SSH_COMMAND carries the four options the spec names by name: -i (the
|
||||
# deploy key), UserKnownHostsFile (this package's own known_hosts, never
|
||||
# the invoking user's ~/.ssh/known_hosts -- a stale entry there must not be
|
||||
# able to either accept or block this package's own host-key decision),
|
||||
# StrictHostKeyChecking=yes (an unknown or changed host key is refused,
|
||||
# full stop -- gitbackup test is the only place that ever adds one) and
|
||||
# BatchMode=yes (never fall back to an interactive passphrase/password
|
||||
# prompt that would hang a cron job forever) -- plus ConnectTimeout=15, not
|
||||
# named in the spec but added after a live failure on the owlab stand: a
|
||||
# host that never answers at all (as opposed to actively refusing the
|
||||
# connection) leaves `ssh` hanging with neither of those two safeguards to
|
||||
# stop it, well past two minutes with no output. BatchMode only rules out
|
||||
# an interactive *prompt*; it does nothing about the TCP handshake itself
|
||||
# never completing, which is exactly what a firewalled or simply
|
||||
# nonexistent host looks like.
|
||||
#
|
||||
# GIT_SSL_CAINFO is printed only when gitbackup.origin.ca_file is set, and
|
||||
# is the only sanctioned way to talk to a self-signed/private-CA remote:
|
||||
# GIT_SSL_NO_VERIFY and http.sslVerify=false are refused even as an option
|
||||
# by this package, spec "Аутентификация" and "Границы и швы" -- there is no
|
||||
# code path anywhere in this package that can turn certificate verification
|
||||
# off, only one that can hand it an extra trusted certificate.
|
||||
#
|
||||
# _gb_ssh_cmd below is built as a plain `ssh ...` command, not busybox's
|
||||
# `dbclient` -- and this StrictHostKeyChecking=yes option is exactly why.
|
||||
# Measured on the 25.12.4 stand (dropbear 2025.89, git 2.50.1): with
|
||||
# GIT_SSH_COMMAND set to a dbclient invocation, `GIT_TRACE=1 git fetch`
|
||||
# shows git's ssh-variant probe (`dbclient -G ...`, used to detect whether
|
||||
# the ssh command understands OpenSSH's own option syntax) failing, after
|
||||
# which git silently falls back to its "simple" transport form and stops
|
||||
# passing any `-o NAME=VALUE` pair at all -- not just this one, every
|
||||
# option this function sets, including UserKnownHostsFile and BatchMode.
|
||||
# The failure is invisible: git does not warn that it dropped them, fetch
|
||||
# still succeeds against a reachable host, and the option only turns out
|
||||
# to have been a no-op the day host-key checking was supposed to catch
|
||||
# something. That silent downgrade, not merely dbclient's separate inability
|
||||
# to read OpenSSH-format keys (see package/gitbackup/Makefile), is why this
|
||||
# package depends on the real openssh-client instead.
|
||||
#
|
||||
# Never prints the token itself: GIT_ASKPASS names askpass.sh's path, and
|
||||
# the token is read off disk by askpass.sh's own process, later, only once
|
||||
# git has actually decided it needs credentials.
|
||||
# TLS verification can never be turned off; ca_file is the only way to trust
|
||||
# a private CA. The token is never printed: askpass.sh reads it on demand.
|
||||
gb_git_env() {
|
||||
_gb_key=$(gb_uci_get gitbackup.origin.key_file /etc/gitbackup/id_ed25519)
|
||||
_gb_known="$GB_ETC_DIR/known_hosts"
|
||||
@@ -88,8 +20,6 @@ gb_git_env() {
|
||||
|
||||
printf 'export GIT_SSH_COMMAND=%s\n' "$(_gb_auth_shquote "$_gb_ssh_cmd")"
|
||||
printf 'export GIT_ASKPASS=%s\n' "$(_gb_auth_shquote "${GB_SHARE:-/usr/share/gitbackup}/askpass.sh")"
|
||||
# Never let git fall back to its own interactive prompt on a real tty --
|
||||
# askpass.sh is the only credential source this package allows.
|
||||
printf 'export GIT_TERMINAL_PROMPT=%s\n' "$(_gb_auth_shquote 0)"
|
||||
|
||||
_gb_ca=$(gb_uci_get gitbackup.origin.ca_file)
|
||||
@@ -97,23 +27,10 @@ gb_git_env() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# _gb_auth_shquote <string> -- <string> wrapped in single quotes, with any
|
||||
# single quote inside it escaped, so the result can be eval'd back to the
|
||||
# original value regardless of what it contains (a path with a space is the
|
||||
# realistic case here, not an adversarial one, but the quoting is correct
|
||||
# either way).
|
||||
_gb_auth_shquote() {
|
||||
printf "'%s'" "$(printf '%s' "$1" | sed "s/'/'\\\\''/g")"
|
||||
}
|
||||
|
||||
# _gb_auth_key_fingerprint <key-path>
|
||||
#
|
||||
# ssh-keygen -lf's own fingerprint line for whichever half of the pair is
|
||||
# actually readable, preferring the public half (same preference order
|
||||
# gb_pubkey below already uses) -- unlike gb_pubkey, this never needs the
|
||||
# `-y` derivation fallback: `ssh-keygen -lf` accepts either a private or a
|
||||
# public key file directly and prints the identical fingerprint for
|
||||
# either, since the fingerprint is a hash of the public half alone.
|
||||
_gb_auth_key_fingerprint() {
|
||||
_gb_akf_key="$1"
|
||||
if [ -r "$_gb_akf_key.pub" ]; then
|
||||
@@ -123,84 +40,16 @@ _gb_auth_key_fingerprint() {
|
||||
fi
|
||||
}
|
||||
|
||||
# gb_keygen [force] [confirm-fingerprint]
|
||||
# gb_keygen [force] [confirm-fingerprint] -- no passphrase: BatchMode=yes
|
||||
# would make an encrypted key hang every push.
|
||||
#
|
||||
# Generates the deploy key at gitbackup.origin.key_file (default
|
||||
# /etc/gitbackup/id_ed25519): ed25519, no passphrase. No passphrase is
|
||||
# deliberate, not an oversight -- GIT_SSH_COMMAND always carries
|
||||
# BatchMode=yes (gb_git_env), which refuses to prompt for one, so an
|
||||
# encrypted key would simply make every push hang forever instead of
|
||||
# failing loudly.
|
||||
#
|
||||
# Refuses to overwrite a key that already exists unless <force> is a
|
||||
# non-empty argument: losing the only copy of a deploy key locks every
|
||||
# remote it was already added to until the operator notices and re-adds
|
||||
# the new public half everywhere (ticket 04 acceptance criterion). This
|
||||
# part of the contract is unchanged by ticket 25 below: `gitbackup keygen`
|
||||
# with no `force` at all on an existing key behaves exactly as before.
|
||||
#
|
||||
# Ticket 25: `force` alone used to be enough to destroy the working key on
|
||||
# the spot, no warning, no way back -- reported live from a real router
|
||||
# where `keygen --force`, run only to see where the operation writes,
|
||||
# silently ate the key a working deploy relied on. The private half is
|
||||
# gone the instant the `rm -f` below runs, and every remote it had been
|
||||
# added to as a deploy key stops authenticating that same instant, with
|
||||
# nothing on this router able to undo either side of that.
|
||||
#
|
||||
# gb_hostkey_show/gb_hostkey_accept above already solved the equivalent
|
||||
# problem for a different irreversible decision -- "confirm by naming
|
||||
# exactly what was shown", not a bare y/N -- and this reuses that same
|
||||
# shape instead of adding a `read` prompt here: a `read`-based prompt
|
||||
# would need a second, separate branch for "no controlling terminal at
|
||||
# all" (ticket 20's own EOF-is-neither-yes-nor-no lesson, gb_accept_hostkey
|
||||
# above), and that branch would need testing on its own. Requiring the
|
||||
# caller to name the fingerprint back sidesteps the distinction entirely --
|
||||
# an interactive operator and a non-interactive script take the identical
|
||||
# path, and there is no `read` anywhere in this function to hang or
|
||||
# misread an EOF as a decision nobody made:
|
||||
#
|
||||
# force set, no existing key -- generates immediately, exactly
|
||||
# as before (nothing to confirm)
|
||||
# force unset, existing key -- refused, exit 1, unchanged
|
||||
# force set, existing key, confirm -- prints "confirm-required
|
||||
# empty or not matching the current <fingerprint>" to stdout (nothing
|
||||
# key's own fingerprint destroyed) and returns 4
|
||||
# force set, existing key, confirm -- proceeds: the old key is kept
|
||||
# matches the current fingerprint aside (see below) and a fresh one
|
||||
# exactly is generated; returns 0
|
||||
#
|
||||
# A caller that never asks (only ever passes `force`) gets told what is
|
||||
# missing on every single call -- it can never destroy anything by
|
||||
# accident, and it is never met with a bare, unexplained refusal either.
|
||||
#
|
||||
# Decision (ticket 25 leaves this to the implementer, either answer
|
||||
# acceptable if justified): the key about to be destroyed is copied aside
|
||||
# as "<key>.old"/"<key>.old.pub" before anything is removed, left there
|
||||
# until gb_keygen_forget_old below is called. Nothing on this router ever
|
||||
# reads ".old" back automatically -- there is deliberately no "fall back
|
||||
# to the old key if the new one fails" logic, which would make a failed
|
||||
# push ambiguous about which key actually ran and silently reintroduce the
|
||||
# very key this whole flow just told the operator to stop using. The
|
||||
# reason to keep it anyway is narrower: the one step this flow cannot
|
||||
# verify from the router is whether the operator actually went to the
|
||||
# provider and added the NEW public key before the next push runs. If they
|
||||
# forgot, or pasted the wrong key, the router itself becomes unreachable
|
||||
# over the only channel this package uses, and the file this comment is
|
||||
# justifying is then the difference between "cp it back and try again" and
|
||||
# "nothing, the old private half is just gone" -- for the cost of one
|
||||
# extra ~400-byte file that a successful `gitbackup test` (cmd_test,
|
||||
# usr/sbin/gitbackup) removes on its own the first time the NEW key is
|
||||
# actually proven to work. Deleting outright instead (the spec's other
|
||||
# sanctioned answer) would not shorten the provider trip either way; it
|
||||
# would only remove this one way an already-bad day gets worse.
|
||||
#
|
||||
# Creates the key's directory at 0700 if it does not exist yet (uci-
|
||||
# defaults/99-gitbackup already does this for the default /etc/gitbackup,
|
||||
# but gitbackup.origin.key_file can point anywhere), and leaves the
|
||||
# private key at 0600 -- ssh-keygen's own default, asserted here rather
|
||||
# than trusted, since a future ssh-keygen with a different umask-derived
|
||||
# default would otherwise change this package's security posture with no
|
||||
# code change on this side to catch it.
|
||||
# Replacing a key cuts off every remote it is registered with, so it takes
|
||||
# force AND the current fingerprint named back:
|
||||
# key, no force -- refuse, 1
|
||||
# key, force, no/wrong fp -- print "confirm-required <fp>", destroy
|
||||
# nothing, 4
|
||||
# key, force, matching fp -- keep <key>.old(.pub) for manual rollback,
|
||||
# regenerate, 0
|
||||
gb_keygen() {
|
||||
_gb_force="${1:-}"
|
||||
_gb_confirm="${2:-}"
|
||||
@@ -225,11 +74,7 @@ gb_keygen() {
|
||||
return 4
|
||||
fi
|
||||
|
||||
# Confirmed: keep the outgoing key's own copy before touching
|
||||
# anything -- see this function's header comment for why. Best
|
||||
# effort on purpose (2>/dev/null): a failure to write the backup
|
||||
# copy must never block the regeneration the operator explicitly
|
||||
# confirmed, only mean there is nothing to roll back to.
|
||||
# Best effort: must not block a confirmed regeneration.
|
||||
cp -f "$_gb_key" "$_gb_key.old" 2>/dev/null && chmod 0600 "$_gb_key.old" 2>/dev/null
|
||||
[ -e "$_gb_key.pub" ] && cp -f "$_gb_key.pub" "$_gb_key.old.pub" 2>/dev/null
|
||||
fi
|
||||
@@ -249,31 +94,12 @@ gb_keygen() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_keygen_forget_old
|
||||
#
|
||||
# Ticket 25's other half of the id_ed25519.old decision above (gb_keygen's
|
||||
# own header comment): removes the pre-regeneration copy once something has
|
||||
# actually proven the CURRENT key works end to end. Called from cmd_test's
|
||||
# own success path (usr/sbin/gitbackup) -- a successful `git ls-remote`
|
||||
# against the configured remote is the one signal this package already
|
||||
# computes that means "the key at key_file is accepted by the provider
|
||||
# right now", which is exactly the event this flow is waiting for. A no-op
|
||||
# whenever there is nothing to forget (the overwhelming majority of
|
||||
# successful `test` runs never followed a keygen at all), so this is safe
|
||||
# to call unconditionally on every success rather than tracking anywhere
|
||||
# whether a keygen happened first.
|
||||
# Called once `gitbackup test` has proven the current key works.
|
||||
gb_keygen_forget_old() {
|
||||
_gb_key=$(gb_uci_get gitbackup.origin.key_file /etc/gitbackup/id_ed25519)
|
||||
rm -f "$_gb_key.old" "$_gb_key.old.pub"
|
||||
}
|
||||
|
||||
# gb_pubkey
|
||||
#
|
||||
# Prints the deploy key's public half. Reads the already-generated .pub
|
||||
# file when it is there (the common case, ssh-keygen always writes one),
|
||||
# and falls back to deriving it from the private key with `ssh-keygen -y`
|
||||
# only if that file is missing -- an operator could plausibly delete just
|
||||
# the .pub half by hand and expect this to still work.
|
||||
gb_pubkey() {
|
||||
_gb_key=$(gb_uci_get gitbackup.origin.key_file /etc/gitbackup/id_ed25519)
|
||||
if [ -r "$_gb_key.pub" ]; then
|
||||
@@ -288,38 +114,15 @@ gb_pubkey() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# _gb_auth_dial_hostkey <host> <port> <out-file>
|
||||
#
|
||||
# The dial step gb_accept_hostkey and gb_hostkey_show below both need,
|
||||
# factored out so there is exactly one place that knows how to fetch a raw
|
||||
# host key off the wire. There is no ssh-keyscan on this image without
|
||||
# pulling in the separate openssh-client-utils package (measured on the
|
||||
# 25.12.4 stand: `apk info -L openssh-client openssh-keygen` together
|
||||
# provide neither ssh-keyscan nor any equivalent) -- rather than growing
|
||||
# DEPENDS for one command, this reuses the `ssh` binary DEPENDS already
|
||||
# carries. The SSH protocol completes its host-key exchange before user
|
||||
# authentication even starts, so `ssh -o StrictHostKeyChecking=accept-new
|
||||
# -o BatchMode=yes` against a scratch, empty known_hosts file writes the
|
||||
# host's key into it regardless of whether the authentication that follows
|
||||
# succeeds -- verified live on the owlab stand: `ssh -o
|
||||
# UserKnownHostsFile=<scratch> -o StrictHostKeyChecking=accept-new -o
|
||||
# BatchMode=yes git@github.com exit` still populates <scratch> with
|
||||
# github.com's key even though the command itself exits 255 on "Permission
|
||||
# denied (publickey)" right after, because no key for github.com had
|
||||
# actually been added yet.
|
||||
#
|
||||
# Writes <out-file> and returns 0 when a key was obtained, or returns 2
|
||||
# with <out-file> removed when nothing could be obtained at all (host
|
||||
# unreachable, connection refused, DNS failure).
|
||||
# _gb_auth_dial_hostkey <host> <port> <out-file> -- no ssh-keyscan in
|
||||
# openssh-client: key exchange precedes auth, so accept-new against an empty
|
||||
# scratch known_hosts records the key even though auth fails. 2 unreachable.
|
||||
_gb_auth_dial_hostkey() {
|
||||
_gb_adh_host="$1"
|
||||
_gb_adh_port="$2"
|
||||
_gb_adh_out="$3"
|
||||
rm -f "$_gb_adh_out"
|
||||
|
||||
# ConnectTimeout=15: same reasoning as gb_git_env's own copy of it -- a
|
||||
# host that never answers at all, rather than actively refusing,
|
||||
# otherwise leaves this hanging well past two minutes (measured live).
|
||||
ssh -o UserKnownHostsFile="$_gb_adh_out" -o StrictHostKeyChecking=accept-new \
|
||||
-o BatchMode=yes -o ConnectTimeout=15 -p "$_gb_adh_port" "git@$_gb_adh_host" exit >/dev/null 2>&1
|
||||
|
||||
@@ -330,58 +133,33 @@ _gb_auth_dial_hostkey() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# _gb_hostkey_pending_file -- fixed path under GB_ETC_DIR, never part of
|
||||
# known_hosts itself. Holds the raw known_hosts-format line(s) for the
|
||||
# LAST host key gb_hostkey_show fetched, and nothing else: gb_hostkey_accept
|
||||
# below commits exactly this file's bytes, never a fresh dial of its own
|
||||
# (see gb_hostkey_accept's own header comment for why that distinction is
|
||||
# the whole point of the two-step design).
|
||||
# gb_hostkey_accept commits exactly the bytes gb_hostkey_show fetched, never
|
||||
# a fresh dial, so what the operator confirmed is what gets trusted.
|
||||
_gb_hostkey_pending_file() {
|
||||
printf '%s/hostkey_pending' "$GB_ETC_DIR"
|
||||
}
|
||||
|
||||
# gb_accept_hostkey <host> [port]
|
||||
#
|
||||
# Interactive, and only ever meant to be called from `gitbackup test`
|
||||
# (spec: "Host key принимается только внутри gitbackup test"). Fetches the
|
||||
# host's SSH key, shows its fingerprint on stderr, asks for a yes/no on
|
||||
# stdin, and only on "yes" appends it to this package's own known_hosts
|
||||
# (GB_ETC_DIR/known_hosts) -- never the invoking user's ~/.ssh/known_hosts.
|
||||
#
|
||||
# Returns 0 once the key is confirmed and recorded (or was already known --
|
||||
# see the ssh-keygen -F check below, which makes this safe to call
|
||||
# unconditionally from `gitbackup test` every time), 1 when the operator
|
||||
# explicitly declines, 2 when no host key could be obtained at all (host
|
||||
# unreachable, connection refused, DNS failure), 3 when there was no way to
|
||||
# even ASK the operator (ticket 20: `read` hit EOF -- no controlling
|
||||
# terminal at all, e.g. rpcd's own `gitbackup test </dev/null`). 3 used to
|
||||
# be indistinguishable from 1 here: the old code fed `read`'s result
|
||||
# straight into the `case` below without ever looking at `read`'s own exit
|
||||
# status, so an EOF -- which busybox ash's `read` reports as a nonzero
|
||||
# return with `_gb_ans` left empty -- fell into the same "not y" branch a
|
||||
# real, typed "no" does, logging "declined by the operator" for an operator
|
||||
# who was never actually asked. Reported live against a real GitHub deploy
|
||||
# key on a real router: `gitbackup test` run from `gbrpc_test` (stdin
|
||||
# `</dev/null` by construction, ticket 20) always produced that exact
|
||||
# false accusation, and offered no way to accept the key from the web at
|
||||
# all -- gb_hostkey_show/gb_hostkey_accept below are that way.
|
||||
gb_accept_hostkey() {
|
||||
# Sets _gb_host, _gb_port, _gb_known; true when already in known_hosts.
|
||||
_gb_hostkey_init() {
|
||||
_gb_host="$1"
|
||||
_gb_port="${2:-22}"
|
||||
case "$_gb_port" in
|
||||
''|0) _gb_port=22 ;;
|
||||
esac
|
||||
|
||||
[ "$_gb_port" != 0 ] || _gb_port=22
|
||||
mkdir -p "$GB_ETC_DIR" 2>/dev/null
|
||||
_gb_known="$GB_ETC_DIR/known_hosts"
|
||||
[ -r "$_gb_known" ] && ssh-keygen -F "$_gb_host" -f "$_gb_known" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# Already trusted: nothing to accept. Asked of ssh-keygen itself, not a
|
||||
# raw grep, so a known_hosts entry hashed with -H (never written by this
|
||||
# package, but nothing stops an operator from hand-editing the file) is
|
||||
# still recognized correctly.
|
||||
if [ -r "$_gb_known" ] && ssh-keygen -F "$_gb_host" -f "$_gb_known" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
_gb_hostkey_record() {
|
||||
cat "$1" >>"$_gb_known"
|
||||
chmod 0600 "$_gb_known"
|
||||
rm -f "$1"
|
||||
gb_log notice "$2: $_gb_host:$_gb_port accepted and recorded in $_gb_known"
|
||||
}
|
||||
|
||||
# gb_accept_hostkey <host> [port] -- terminal prompt; never trusts silently.
|
||||
# 0 trusted, 1 declined, 2 unreachable, 3 no stdin (web uses show/accept).
|
||||
gb_accept_hostkey() {
|
||||
_gb_hostkey_init "$1" "${2:-}" && return 0
|
||||
|
||||
_gb_scratch=$(mktemp "${TMPDIR:-/tmp}/gitbackup-hostkey.XXXXXX") || return 2
|
||||
if ! _gb_auth_dial_hostkey "$_gb_host" "$_gb_port" "$_gb_scratch"; then
|
||||
@@ -392,60 +170,27 @@ gb_accept_hostkey() {
|
||||
_gb_fp=$(ssh-keygen -lf "$_gb_scratch" 2>/dev/null)
|
||||
printf 'Host key for %s:%s --\n %s\nAccept and remember it? [y/N] ' "$_gb_host" "$_gb_port" "$_gb_fp" >&2
|
||||
|
||||
# See this function's own header comment: `read`'s exit status, not
|
||||
# just the string it captured, is what tells a real "no" (read
|
||||
# succeeded, the operator typed something other than y/yes) apart from
|
||||
# "could not ask at all" (read failed -- EOF, no stdin to read from).
|
||||
if IFS= read -r _gb_ans; then
|
||||
case "$_gb_ans" in
|
||||
y|Y|yes|YES) ;;
|
||||
*)
|
||||
rm -f "$_gb_scratch"
|
||||
gb_log notice "gb_accept_hostkey: $_gb_host:$_gb_port declined by the operator"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
else
|
||||
if ! IFS= read -r _gb_ans; then
|
||||
rm -f "$_gb_scratch"
|
||||
gb_log err "gb_accept_hostkey: $_gb_host:$_gb_port could not ask for confirmation -- no interactive input is available here. Run 'gitbackup test' from a terminal with a real stdin, or accept the host key from the LuCI web UI (Settings -> Connection test), which uses 'gitbackup hostkey show'/'hostkey accept' for exactly this case."
|
||||
return 3
|
||||
fi
|
||||
|
||||
cat "$_gb_scratch" >>"$_gb_known"
|
||||
chmod 0600 "$_gb_known"
|
||||
rm -f "$_gb_scratch"
|
||||
gb_log notice "gb_accept_hostkey: $_gb_host:$_gb_port accepted and recorded in $_gb_known"
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_hostkey_show <host> [port]
|
||||
#
|
||||
# The non-interactive half of ticket 20's web flow: dials <host>, same as
|
||||
# gb_accept_hostkey above, but never asks anything and never writes
|
||||
# known_hosts. Instead it caches the exact bytes it fetched in
|
||||
# _gb_hostkey_pending_file, so a later gb_hostkey_accept call can commit
|
||||
# precisely THAT material -- not whatever a second, independent dial might
|
||||
# return -- which is what keeps the show/confirm round trip from opening a
|
||||
# window for a key swap in between (see gb_hostkey_accept's own comment).
|
||||
#
|
||||
# Prints exactly one line to stdout:
|
||||
# trusted <host> <port> -- nothing to show, already known
|
||||
# pending <host> <port> <fingerprint...> -- ssh-keygen -lf's own text
|
||||
# Deliberately plain, not JSON: usr/sbin/gitbackup's cmd_hostkey is the one
|
||||
# JSON boundary here, same split gb_pubkey/`gitbackup pubkey` already uses.
|
||||
#
|
||||
# Returns 0 in both printed cases above, 2 when the host could not be
|
||||
# reached at all (nothing printed, nothing cached).
|
||||
gb_hostkey_show() {
|
||||
_gb_host="$1"
|
||||
_gb_port="${2:-22}"
|
||||
case "$_gb_port" in
|
||||
''|0) _gb_port=22 ;;
|
||||
case "$_gb_ans" in
|
||||
y|Y|yes|YES) ;;
|
||||
*)
|
||||
rm -f "$_gb_scratch"
|
||||
gb_log notice "gb_accept_hostkey: $_gb_host:$_gb_port declined by the operator"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
mkdir -p "$GB_ETC_DIR" 2>/dev/null
|
||||
_gb_known="$GB_ETC_DIR/known_hosts"
|
||||
if [ -r "$_gb_known" ] && ssh-keygen -F "$_gb_host" -f "$_gb_known" >/dev/null 2>&1; then
|
||||
_gb_hostkey_record "$_gb_scratch" gb_accept_hostkey
|
||||
}
|
||||
|
||||
# gb_hostkey_show <host> [port] -- "trusted <host> <port>" or
|
||||
# "pending <host> <port> <fp>". Records nothing. 2 when unreachable.
|
||||
gb_hostkey_show() {
|
||||
if _gb_hostkey_init "$1" "${2:-}"; then
|
||||
printf 'trusted %s %s\n' "$_gb_host" "$_gb_port"
|
||||
return 0
|
||||
fi
|
||||
@@ -462,33 +207,11 @@ gb_hostkey_show() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_hostkey_accept <host> [port] <fingerprint>
|
||||
#
|
||||
# Commits the key gb_hostkey_show cached, but ONLY when <fingerprint>
|
||||
# matches ssh-keygen's own fingerprint of exactly that cached material.
|
||||
# <fingerprint> is the text the operator was shown and clicked "accept"
|
||||
# on, so this check is what stops a second, possibly different dial from
|
||||
# silently substituting a different key between the moment the fingerprint
|
||||
# was displayed and the moment it was confirmed (an active MITM that
|
||||
# changed which key answers the connection the second time around, or a
|
||||
# second concurrent gb_hostkey_show call from another tab overwriting the
|
||||
# pending file first). This is exactly why this function never dials the
|
||||
# network itself: re-fetching "to double check" would defeat the point --
|
||||
# it would be confirming a NEW dial, not the one the operator actually
|
||||
# saw.
|
||||
#
|
||||
# Returns 0 once accepted and recorded, 1 when there is nothing pending
|
||||
# (gb_hostkey_show was never called, or its result already got
|
||||
# consumed/replaced -- call it again), 4 when <fingerprint> does not match
|
||||
# what is actually cached (exit-code contract: "4 отказ по безопасности" --
|
||||
# this is refused, never silently accepted with a mismatch logged instead).
|
||||
# gb_hostkey_accept <host> <port> <fp> -- only on an exact fingerprint match.
|
||||
# 1 nothing pending, 4 mismatch.
|
||||
gb_hostkey_accept() {
|
||||
_gb_host="$1"
|
||||
_gb_port="${2:-22}"
|
||||
_gb_hostkey_init "$1" "${2:-}"
|
||||
_gb_want_fp="$3"
|
||||
case "$_gb_port" in
|
||||
''|0) _gb_port=22 ;;
|
||||
esac
|
||||
|
||||
_gb_pending=$(_gb_hostkey_pending_file)
|
||||
if [ ! -s "$_gb_pending" ]; then
|
||||
@@ -502,11 +225,5 @@ gb_hostkey_accept() {
|
||||
return 4
|
||||
fi
|
||||
|
||||
mkdir -p "$GB_ETC_DIR" 2>/dev/null
|
||||
_gb_known="$GB_ETC_DIR/known_hosts"
|
||||
cat "$_gb_pending" >>"$_gb_known"
|
||||
chmod 0600 "$_gb_known"
|
||||
rm -f "$_gb_pending"
|
||||
gb_log notice "gb_hostkey_accept: $_gb_host:$_gb_port accepted and recorded in $_gb_known"
|
||||
return 0
|
||||
_gb_hostkey_record "$_gb_pending" gb_hostkey_accept
|
||||
}
|
||||
@@ -1,42 +1,10 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- recovery card (spec "Recovery card и RECOVERY.md", ticket 09).
|
||||
# gitbackup -- recovery card: gb_card. Needs lib.sh and remoteurl.sh; reads
|
||||
# GB_URL/GB_DEVICE/GB_SCRUB as resolved by the CLI.
|
||||
#
|
||||
# Exposes gb_card; every _gb_card_-prefixed helper below is private, same
|
||||
# per-module namespacing convention every sibling file here uses. Sourced,
|
||||
# never executed: nothing here runs at load time.
|
||||
#
|
||||
# Depends on lib.sh (gb_log, gb_uci_get) and remoteurl.sh (gb_parse_url,
|
||||
# gb_provider) already being sourced by the caller -- same convention
|
||||
# visibility.sh/restore.sh use. Reads GB_URL and GB_DEVICE from the
|
||||
# environment rather than taking them as arguments: usr/sbin/gitbackup's
|
||||
# _gb_run_backup already has both resolved (by gb_validate_config, before
|
||||
# _gb_run_backup even starts) at the one call site this module is actually
|
||||
# reached from today (step 11b, "the recovery card"), same convention
|
||||
# gitio.sh/restore.sh already use for config context the CLI resolved once.
|
||||
# A future LuCI "Download recovery card" button (ticket 11) goes through
|
||||
# `gitbackup card`, which resolves the same two through gb_validate_config
|
||||
# first, so this module never has to.
|
||||
#
|
||||
# gb_card is deliberately built to never die and never fail loudly: it is
|
||||
# called from the middle of a real backup run (usr/sbin/gitbackup's
|
||||
# `gb_card "$_gb_outdir/RECOVERY.md" 2>/dev/null`), and a bug in a recovery
|
||||
# document must not be able to abort the backup it rides along with. Every
|
||||
# helper below degrades to a shorter card instead of calling gb_die, and
|
||||
# gb_card itself always returns 0.
|
||||
|
||||
# gb_card <outfile>
|
||||
#
|
||||
# Writes one recovery document to <outfile> -- the same content LuCI's
|
||||
# "Download recovery card" button (ticket 11) offers for offline storage and
|
||||
# `gitbackup run` commits to devices/<id>/RECOVERY.md (spec: "Оба генерирует
|
||||
# gb_card; различаются только назначением и местом"). Contains a ready-made
|
||||
# bootstrap one-liner with --repo/--device already filled in, a link to the
|
||||
# repository's own web UI, a reminder that a token or deploy key still has
|
||||
# to be supplied by hand, and a short Path 0 fallback for when there is no
|
||||
# network to run bootstrap.sh over at all. Never the secret itself: neither
|
||||
# the token nor the deploy key's private half ever passes through this
|
||||
# module (interfaces.md: "Секрета в карточке нет").
|
||||
# gb_card <outfile> -- writes RECOVERY.md. Never contains a secret. Runs
|
||||
# mid-backup, so it never dies and always returns 0.
|
||||
gb_card() {
|
||||
_gb_c_out="$1"
|
||||
_gb_c_dev="${GB_DEVICE:-}"
|
||||
@@ -47,13 +15,8 @@ gb_card() {
|
||||
_gb_c_authflag='--token <TOKEN>'
|
||||
_gb_c_weblink=''
|
||||
|
||||
# GB_SCRUB is this run's EFFECTIVE scrub decision, already resolved
|
||||
# once by gb_validate_config (usr/sbin/gitbackup) and exported for
|
||||
# every subcommand -- the same seam collect.sh reads GB_DEVICE
|
||||
# through, and the reason this file does not re-derive it from
|
||||
# gitbackup.security.scrub itself: visibility=public forces scrub on
|
||||
# regardless of what that option says, and a card that missed the
|
||||
# override would promise a Path 0 that does not exist.
|
||||
# GB_SCRUB, not gitbackup.security.scrub: visibility=public forces scrub
|
||||
# on, and missing that would promise a Path 0 that does not exist.
|
||||
_gb_c_scrub=0
|
||||
[ "$(gb_json_bool "${GB_SCRUB:-0}")" = true ] && _gb_c_scrub=1
|
||||
_gb_c_archive=0
|
||||
@@ -71,11 +34,7 @@ gb_card() {
|
||||
_gb_c_owner="$4"
|
||||
_gb_c_repo="$5"
|
||||
[ "$_gb_c_scheme" = ssh ] && _gb_c_authflag='--ssh-key <PATH-TO-DEPLOY-KEY>'
|
||||
# No port here, same known limitation as gb_deeplink
|
||||
# (remoteurl.sh): every provider this package recognizes serves
|
||||
# its web UI on the default https port, so this has never
|
||||
# needed one -- a self-hosted instance on a nonstandard port
|
||||
# would need it added here too, at the same time.
|
||||
# No port: known provider web UIs are on the default https port.
|
||||
_gb_c_weblink="https://$_gb_c_host/$_gb_c_owner/$_gb_c_repo"
|
||||
fi
|
||||
fi
|
||||
@@ -100,15 +59,8 @@ gb_card() {
|
||||
if [ -n "$_gb_c_weblink" ]; then
|
||||
printf 'Repository: %s\n\n' "$_gb_c_weblink"
|
||||
fi
|
||||
# Path 0 is printed only when there is actually an archive in the
|
||||
# repository to open it with. `gitbackup run` writes
|
||||
# backup.tar.gz when main.archive is on AND this run does not
|
||||
# scrub (usr/sbin/gitbackup's own step 11a -- sysupgrade -b reads
|
||||
# the live filesystem, so pushing its output would undo the scrub
|
||||
# the same run was just asked to perform). A recovery card is
|
||||
# read exactly once, by someone whose router is already broken;
|
||||
# sending them after a file that was never pushed is worse than
|
||||
# telling them this path is unavailable, and why.
|
||||
# Path 0 only when an archive is actually pushed (archive on, no
|
||||
# scrub); otherwise say why it is unavailable.
|
||||
if [ "$_gb_c_archive" = 1 ]; then
|
||||
printf '## Path 0 -- no network, or bootstrap.sh will not run\n\n'
|
||||
printf '1. Open the repository above from any other device and download\n'
|
||||
|
||||
@@ -1,64 +1,24 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- backup-set collection and manifest.json (spec "Сбор и
|
||||
# manifest.json").
|
||||
#
|
||||
# Exposes gb_collect, gb_manifest_path and gb_manifest_equal; every
|
||||
# _gb_collect_-prefixed helper below is private and namespaced by module
|
||||
# (not just "_gb_") because every *.sh here ends up sourced into the same
|
||||
# process by usr/sbin/gitbackup -- a same-named helper in a sibling module
|
||||
# would silently shadow this one otherwise. Sourced, never executed:
|
||||
# nothing here runs at load time.
|
||||
#
|
||||
# Depends on lib.sh (gb_log, gb_json_esc/str, ...) already being sourced by
|
||||
# the caller, same convention device.sh uses. It does NOT depend on
|
||||
# device.sh: the device name is read from $GB_DEVICE, which
|
||||
# usr/sbin/gitbackup's gb_validate_config already resolves once via
|
||||
# gb_device_id and exports for every subcommand (same contract as GB_URL
|
||||
# and GB_SCRUB) -- collecting would otherwise resolve the device a second
|
||||
# time and could disagree with the CLI's own answer.
|
||||
#
|
||||
# GB_ROOT overrides the filesystem root every real-path read goes through
|
||||
# (default '', meaning the actual '/'). GB_EXCLUDE_LIST overrides where the
|
||||
# hard-exclude patterns live (default GB_SHARE/exclude.list, GB_SHARE
|
||||
# itself defaulting like the CLI's own). Neither is set by the router --
|
||||
# both exist so tests/run.sh can point collect.sh at a fixture tree instead
|
||||
# of the host filesystem, the same seam GB_SYSFS_NET gives device.sh.
|
||||
# gitbackup -- backup-set collection: gb_collect, gb_manifest_path,
|
||||
# gb_manifest_equal. Needs lib.sh; reads GB_DEVICE and GB_SCRUB set by the CLI.
|
||||
|
||||
GB_ROOT="${GB_ROOT:-}"
|
||||
GB_EXCLUDE_LIST="${GB_EXCLUDE_LIST:-${GB_SHARE:-/usr/share/gitbackup}/exclude.list}"
|
||||
|
||||
# gb_manifest_path <outdir>
|
||||
gb_manifest_path() {
|
||||
printf '%s/manifest.json\n' "$1"
|
||||
}
|
||||
|
||||
# gb_manifest_equal <manifest-a> <manifest-b>
|
||||
#
|
||||
# True when two manifest.json files describe the same backup set. Compares
|
||||
# only entries[] and scrubbed[] (spec: "generated не участвует в сравнении
|
||||
# манифестов, иначе изменения были бы каждый прогон") -- and, by the same
|
||||
# reasoning, neither do version/hostname/device/openwrt/board: none of them
|
||||
# describe what restore actually writes back, and comparing them would turn
|
||||
# an unrelated hostname change into a spurious commit on the next run.
|
||||
#
|
||||
# gb_manifest_tail (lib.sh) is the shared reader now: this used to be its
|
||||
# own private sed range here, one of three near-identical readers of the
|
||||
# same flat manifest.json format that scrub.sh and restore.sh had each
|
||||
# grown independently (ticket 18) -- one reader, not three.
|
||||
# Only entries[] and scrubbed[]: "generated" and host metadata would
|
||||
# otherwise produce a commit every run.
|
||||
gb_manifest_equal() {
|
||||
[ -r "$1" ] && [ -r "$2" ] || return 1
|
||||
[ "$(gb_manifest_tail "$1")" = "$(gb_manifest_tail "$2")" ]
|
||||
}
|
||||
|
||||
# gb_collect <outdir>
|
||||
#
|
||||
# Builds outdir/files (backup-set tree, paths preserved), outdir/meta (five
|
||||
# files) and outdir/manifest.json. The path list is `sysupgrade -l` and
|
||||
# nothing else (R29) -- collect.sh never walks /etc on its own to decide
|
||||
# what belongs in the set, only to find the empty directories `sysupgrade
|
||||
# -l` can never report (see _gb_collect_empty_dirs) and to concatenate
|
||||
# /etc/apk/repositories.d/* for meta/repositories.txt.
|
||||
# gb_collect <outdir> -- outdir/files, meta/, manifest.json. The path list is
|
||||
# `sysupgrade -l` only; sources are walked just for empty dirs.
|
||||
gb_collect() {
|
||||
_gb_outdir="$1"
|
||||
[ -n "$_gb_outdir" ] || { gb_log err 'gb_collect: outdir is required'; return 1; }
|
||||
@@ -77,31 +37,18 @@ gb_collect() {
|
||||
rm -rf "$_gb_scratch"
|
||||
}
|
||||
|
||||
# _gb_collect_is_excluded <path> -- true when <path> matches a pattern in
|
||||
# GB_EXCLUDE_LIST. A pattern ending in "/**" matches the path itself and
|
||||
# everything under it; anything else is matched with plain `case` glob
|
||||
# rules. Re-reads the file on every call rather than caching it in a
|
||||
# variable: the list is a handful of lines, and this keeps the match logic
|
||||
# free of the quoting/IFS games a cached multi-line variable would need.
|
||||
#
|
||||
# Matched on the canonical spelling of <path>, never the raw one: `case`
|
||||
# globbing is textual, so "//etc/gitbackup/token" and
|
||||
# "/etc/./gitbackup/token" match no pattern that names /etc/gitbackup even
|
||||
# though find(1), cp(1) and the kernel all agree they are that file. This
|
||||
# is the second half of the same fix gb_paths_validate carries -- that one
|
||||
# stops such an entry being accepted into sysupgrade.conf at all, this one
|
||||
# holds even for a path that never went through it, since `sysupgrade -l`
|
||||
# unions in /lib/upgrade/keep.d/* and changed conffiles that this package
|
||||
# does not own. exclude.list's own R76 ("must never end up inside its own
|
||||
# backup, no matter how the router is configured") is only true with both.
|
||||
_gb_collect_is_excluded() {
|
||||
# Own variable name, deliberately not the _gb_path every caller loop
|
||||
# here already uses: POSIX sh has no locals, and canonicalizing into
|
||||
# that name would silently rewrite the caller's loop variable too.
|
||||
# Canonical: case globs are textual. Own variable name: callers loop over
|
||||
# _gb_path and sh has no locals.
|
||||
_gb_ie_path=$(gb_path_canon "$1")
|
||||
[ -r "$GB_EXCLUDE_LIST" ] || return 1
|
||||
while IFS= read -r _gb_pat || [ -n "$_gb_pat" ]; do
|
||||
case "$_gb_pat" in
|
||||
# Whole-file secrets: excluded only when this run scrubs.
|
||||
'#public: '*)
|
||||
[ "$(gb_json_bool "${GB_SCRUB:-0}")" = true ] || continue
|
||||
_gb_pat="${_gb_pat#'#public: '}"
|
||||
;;
|
||||
''|'#'*) continue ;;
|
||||
esac
|
||||
case "$_gb_pat" in
|
||||
@@ -122,25 +69,12 @@ _gb_collect_is_excluded() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# _gb_collect_files <outdir> <entries-file>
|
||||
#
|
||||
# Copies every file and symlink `sysupgrade -l` reports, minus hard-exclude,
|
||||
# into outdir/files with its path preserved, and appends one manifest entry
|
||||
# line per item to <entries-file>. Runs the loop body on the read side of a
|
||||
# pipe (a subshell in POSIX sh), so it only ever accumulates state by
|
||||
# appending to <entries-file> -- a real file survives the subshell boundary,
|
||||
# a shell variable set in there would not.
|
||||
# Pipe subshell: state goes to <entries-file>, not variables.
|
||||
_gb_collect_files() {
|
||||
_gb_outdir="$1"
|
||||
_gb_entries="$2"
|
||||
sysupgrade -l 2>/dev/null | while IFS= read -r _gb_path || [ -n "$_gb_path" ]; do
|
||||
[ -n "$_gb_path" ] || continue
|
||||
# One spelling per file from here down, so the staged tree, the
|
||||
# manifest entry and the exclude decision cannot disagree about
|
||||
# what "this path" is: "/etc/./config/network" and
|
||||
# "/etc/config/network" are one file to the kernel but two
|
||||
# distinct manifest entries (and two distinct restore targets)
|
||||
# if the raw string is carried through.
|
||||
_gb_path=$(gb_path_canon "$_gb_path")
|
||||
_gb_collect_is_excluded "$_gb_path" && continue
|
||||
|
||||
@@ -149,40 +83,28 @@ _gb_collect_files() {
|
||||
mkdir -p "$(dirname "$_gb_dest")" || continue
|
||||
|
||||
if [ -L "$_gb_src" ]; then
|
||||
# Never dereferenced (R24.1): the target string is copied as-is,
|
||||
# and mode/uid/gid come from `stat` without -L, i.e. the link
|
||||
# itself, not what it points to.
|
||||
# Never dereferenced: stat without -L reports the link itself.
|
||||
_gb_target=$(readlink "$_gb_src") || continue
|
||||
ln -s "$_gb_target" "$_gb_dest" || continue
|
||||
_gb_mug=$(stat -c '%a %u %g' "$_gb_src" 2>/dev/null) || _gb_mug='0 0 0'
|
||||
# shellcheck disable=SC2086 # word-splitting is the point: unpacking "mode uid gid"
|
||||
set -- $_gb_mug
|
||||
_gb_collect_entry_symlink "$_gb_path" "$1" "$2" "$3" "$_gb_target" >>"$_gb_entries"
|
||||
_gb_collect_entry symlink "$_gb_path" "$1" "$2" "$3" target "$_gb_target" >>"$_gb_entries"
|
||||
elif [ -f "$_gb_src" ]; then
|
||||
cp "$_gb_src" "$_gb_dest" || continue
|
||||
_gb_mug=$(stat -c '%a %u %g' "$_gb_src" 2>/dev/null) || _gb_mug='0 0 0'
|
||||
# shellcheck disable=SC2086
|
||||
set -- $_gb_mug
|
||||
_gb_sha=$(sha256sum "$_gb_dest" 2>/dev/null | awk '{print $1}')
|
||||
_gb_collect_entry_file "$_gb_path" "$1" "$2" "$3" "$_gb_sha" >>"$_gb_entries"
|
||||
_gb_collect_entry file "$_gb_path" "$1" "$2" "$3" sha256 "$_gb_sha" >>"$_gb_entries"
|
||||
else
|
||||
gb_log warning "gb_collect: sysupgrade -l listed '$_gb_path' but it is not there anymore"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
# _gb_collect_empty_dirs <outdir> <entries-file>
|
||||
#
|
||||
# `sysupgrade -l` never reports a directory (spec: it flattens every source
|
||||
# through `find ... -type f -o -type l`), so an empty directory anywhere in
|
||||
# the backup set would silently vanish from the tree and from git, which
|
||||
# does not store empty directories either. This walks the same sources
|
||||
# sysupgrade.conf feeds that `find` -- /etc/sysupgrade.conf and every file
|
||||
# under /lib/upgrade/keep.d/ -- and records, as a `dir` entry, every
|
||||
# directory under a directory-type line that turns out to have zero
|
||||
# children. A directory that has children (even only other empty
|
||||
# directories) is not itself empty and gets no entry of its own: restore
|
||||
# recreates it implicitly by recreating what it contains.
|
||||
# `sysupgrade -l` never lists directories and git stores none, so empty dirs
|
||||
# get explicit `dir` entries.
|
||||
_gb_collect_empty_dirs() {
|
||||
_gb_outdir="$1"
|
||||
_gb_entries="$2"
|
||||
@@ -196,9 +118,7 @@ _gb_collect_empty_dirs() {
|
||||
/*) ;;
|
||||
*) continue ;; # blank, comment, or a relative line -- not a source we can resolve
|
||||
esac
|
||||
# keep.d lines end directories in "/" (measured on the 25.12.4 stand,
|
||||
# e.g. "/etc/config/"); stripped here so `find`'s own top-level match
|
||||
# does not echo it back into the manifest path.
|
||||
# keep.d lines end directories in "/"; strip it.
|
||||
_gb_line="${_gb_line%/}"
|
||||
[ -n "$_gb_line" ] && [ -d "$GB_ROOT$_gb_line" ] || continue
|
||||
|
||||
@@ -209,43 +129,30 @@ _gb_collect_empty_dirs() {
|
||||
_gb_mug=$(stat -c '%a %u %g' "$_gb_realdir" 2>/dev/null) || _gb_mug='0 0 0'
|
||||
# shellcheck disable=SC2086
|
||||
set -- $_gb_mug
|
||||
_gb_collect_entry_dir "$_gb_relpath" "$1" "$2" "$3" >>"$_gb_entries"
|
||||
_gb_collect_entry dir "$_gb_relpath" "$1" "$2" "$3" >>"$_gb_entries"
|
||||
done
|
||||
done
|
||||
}
|
||||
|
||||
# _gb_collect_meta <outdir> -- the five meta/ files R17-R21 ask for.
|
||||
_gb_collect_meta() {
|
||||
_gb_outdir="$1"
|
||||
|
||||
# board.json: `ubus call system board` (R17), read back by
|
||||
# _gb_collect_write_manifest instead of calling ubus a second time.
|
||||
ubus call system board 2>/dev/null >"$_gb_outdir/meta/board.json"
|
||||
|
||||
# installed_packages.txt: full "name-version" lines, not bare names --
|
||||
# --with-packages (a later ticket) reads names from `apk info` instead
|
||||
# (spec, "apk"). R18.
|
||||
apk list --installed 2>/dev/null >"$_gb_outdir/meta/installed_packages.txt"
|
||||
|
||||
# repositories.txt: /etc/apk/repositories.d/*, concatenated -- 25.12 has
|
||||
# no /etc/apk/repositories at all (measured on stand), so that path is
|
||||
# never read. R19.
|
||||
# 25.12 has no /etc/apk/repositories, only repositories.d/*.
|
||||
: >"$_gb_outdir/meta/repositories.txt"
|
||||
for _gb_f in "$GB_ROOT/etc/apk/repositories.d/"*; do
|
||||
[ -r "$_gb_f" ] && cat "$_gb_f" >>"$_gb_outdir/meta/repositories.txt"
|
||||
done
|
||||
|
||||
# sysupgrade.conf: a copy of the extra-paths file as it stood at
|
||||
# collection time (R20).
|
||||
if [ -r "$GB_ROOT/etc/sysupgrade.conf" ]; then
|
||||
cp "$GB_ROOT/etc/sysupgrade.conf" "$_gb_outdir/meta/sysupgrade.conf"
|
||||
else
|
||||
: >"$_gb_outdir/meta/sysupgrade.conf"
|
||||
fi
|
||||
|
||||
# os-release.txt: a copy of /etc/os-release (R21). Confirmed on the
|
||||
# owlab 25.12.4 stand: the file exists and VERSION_ID="25.12.4" is
|
||||
# exactly the field manifest.openwrt below reads out of it.
|
||||
if [ -r "$GB_ROOT/etc/os-release" ]; then
|
||||
cp "$GB_ROOT/etc/os-release" "$_gb_outdir/meta/os-release.txt"
|
||||
else
|
||||
@@ -253,7 +160,6 @@ _gb_collect_meta() {
|
||||
fi
|
||||
}
|
||||
|
||||
# _gb_collect_write_manifest <outdir> <entries-file>
|
||||
_gb_collect_write_manifest() {
|
||||
_gb_outdir="$1"
|
||||
_gb_entries="$2"
|
||||
@@ -266,11 +172,7 @@ _gb_collect_write_manifest() {
|
||||
"$_gb_outdir/meta/os-release.txt" 2>/dev/null | head -n 1)
|
||||
_gb_now=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
# sort -u both dedupes (an empty directory reachable from two sources in
|
||||
# sysupgrade.conf would otherwise get two identical entries) and makes
|
||||
# two collections of an unchanged router byte-identical apart from
|
||||
# "generated" -- gb_manifest_equal does not depend on this, but it means
|
||||
# `diff` on two manifests is actually readable.
|
||||
# Stable order across runs.
|
||||
_gb_sorted=$(sort -u "$_gb_entries")
|
||||
|
||||
{
|
||||
@@ -282,40 +184,16 @@ _gb_collect_write_manifest() {
|
||||
printf ' "openwrt": %s,\n' "$(gb_json_str "$_gb_openwrt")"
|
||||
printf ' "board": %s,\n' "$(gb_json_str "$_gb_model")"
|
||||
printf ' "entries": [\n'
|
||||
_gb_collect_join "$_gb_sorted"
|
||||
printf '%s\n' "$_gb_sorted" | gb_manifest_join
|
||||
printf ' ],\n'
|
||||
printf ' "scrubbed": [\n'
|
||||
# Always empty here: scrub only runs at visibility=public and edits
|
||||
# outdir/files in place after gb_collect has already run (ticket 05
|
||||
# owns that step and, with it, the entries/scrubbed this manifest
|
||||
# needs once scrubbing has happened -- not this function's concern,
|
||||
# see collect.sh's own header comment on ownership).
|
||||
# Filled later by gb_scrub.
|
||||
printf ' ]\n'
|
||||
printf '}\n'
|
||||
} >"$_gb_manifest"
|
||||
}
|
||||
|
||||
# _gb_collect_join <text> -- <text>'s non-empty lines, each indented 4
|
||||
# spaces and comma-joined, with no trailing comma on the last one -- the
|
||||
# shape a JSON array's body needs. <text> is a whole variable, not a file:
|
||||
# the values it holds already went through sort -u in the one caller that
|
||||
# exists today.
|
||||
_gb_collect_join() {
|
||||
_gb_first=1
|
||||
printf '%s\n' "$1" | while IFS= read -r _gb_line || [ -n "$_gb_line" ]; do
|
||||
[ -n "$_gb_line" ] || continue
|
||||
if [ "$_gb_first" -eq 1 ]; then _gb_first=0; else printf ',\n'; fi
|
||||
printf ' %s' "$_gb_line"
|
||||
done
|
||||
[ -z "$1" ] || printf '\n'
|
||||
}
|
||||
|
||||
# _gb_collect_json_num <value> -- <value> if it is all digits, else "0".
|
||||
#
|
||||
# Guards mode/uid/gid against a failed `stat` leaving them empty: an
|
||||
# unquoted empty field would make the manifest invalid JSON, and "0" is a
|
||||
# safe, obviously-wrong value a human reviewing the manifest will notice
|
||||
# rather than a parser choking on it.
|
||||
# A failed stat must not produce invalid JSON.
|
||||
_gb_collect_json_num() {
|
||||
case "${1:-}" in
|
||||
''|*[!0-9]*) printf '0' ;;
|
||||
@@ -323,23 +201,10 @@ _gb_collect_json_num() {
|
||||
esac
|
||||
}
|
||||
|
||||
# _gb_collect_entry_file <path> <mode> <uid> <gid> <sha256>
|
||||
_gb_collect_entry_file() {
|
||||
printf '{"path":%s,"type":"file","mode":%s,"uid":%s,"gid":%s,"sha256":%s}\n' \
|
||||
"$(gb_json_str "$1")" "$(_gb_collect_json_num "$2")" \
|
||||
"$(_gb_collect_json_num "$3")" "$(_gb_collect_json_num "$4")" "$(gb_json_str "$5")"
|
||||
}
|
||||
|
||||
# _gb_collect_entry_symlink <path> <mode> <uid> <gid> <target>
|
||||
_gb_collect_entry_symlink() {
|
||||
printf '{"path":%s,"type":"symlink","mode":%s,"uid":%s,"gid":%s,"target":%s}\n' \
|
||||
"$(gb_json_str "$1")" "$(_gb_collect_json_num "$2")" \
|
||||
"$(_gb_collect_json_num "$3")" "$(_gb_collect_json_num "$4")" "$(gb_json_str "$5")"
|
||||
}
|
||||
|
||||
# _gb_collect_entry_dir <path> <mode> <uid> <gid>
|
||||
_gb_collect_entry_dir() {
|
||||
printf '{"path":%s,"type":"dir","mode":%s,"uid":%s,"gid":%s}\n' \
|
||||
"$(gb_json_str "$1")" "$(_gb_collect_json_num "$2")" \
|
||||
"$(_gb_collect_json_num "$3")" "$(_gb_collect_json_num "$4")"
|
||||
# _gb_collect_entry <type> <path> <mode> <uid> <gid> [<key> <value>]
|
||||
_gb_collect_entry() {
|
||||
printf '{"path":%s,"type":"%s","mode":%s,"uid":%s,"gid":%s%s}\n' \
|
||||
"$(gb_json_str "$2")" "$1" "$(_gb_collect_json_num "$3")" \
|
||||
"$(_gb_collect_json_num "$4")" "$(_gb_collect_json_num "$5")" \
|
||||
"${6:+,\"$6\":$(gb_json_str "$7")}"
|
||||
}
|
||||
@@ -1,16 +1,7 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- device identity (spec G01).
|
||||
#
|
||||
# Exposes gb_device_id and gb_expand; every _gb_-prefixed helper below is
|
||||
# private and may change without notice to callers, same convention as
|
||||
# lib.sh. Sourced, never executed: nothing here runs at load time.
|
||||
# gitbackup -- device identity: gb_device_id, gb_expand.
|
||||
|
||||
# gb_device_id -- resolves gitbackup.main.device_id to a device name.
|
||||
#
|
||||
# One of three strategies. Called on every subcommand (through the CLI's
|
||||
# config validation), so the strategies and their failure modes live here
|
||||
# once instead of being reimplemented at every call site.
|
||||
gb_device_id() {
|
||||
_gb_strategy=$(gb_uci_get gitbackup.main.device_id hostname)
|
||||
case "$_gb_strategy" in
|
||||
@@ -21,42 +12,18 @@ gb_device_id() {
|
||||
esac
|
||||
}
|
||||
|
||||
# gb_expand <template> -- substitutes every {device} in <template> with
|
||||
# gb_device_id's answer.
|
||||
#
|
||||
# Takes only the template on purpose (see interfaces.md): the device name is
|
||||
# always gb_device_id's own resolution, never a caller-supplied override, so
|
||||
# there is exactly one place that can disagree with it. gb_device_id is only
|
||||
# called when the template actually has a placeholder, so a plain template
|
||||
# (e.g. a literal path_prefix) never pays for device resolution it does not
|
||||
# need and cannot fail because of it.
|
||||
# gb_expand <template> -- substitutes {device}; resolves the device only when
|
||||
# the placeholder is present, so a plain template never fails on it.
|
||||
gb_expand() {
|
||||
_gb_tpl="$1"
|
||||
case "$_gb_tpl" in
|
||||
_gb_dev=''
|
||||
case "$1" in
|
||||
*'{device}'*) _gb_dev=$(gb_device_id) || exit $? ;;
|
||||
esac
|
||||
_gb_out=''
|
||||
_gb_rest="$_gb_tpl"
|
||||
while true; do
|
||||
case "$_gb_rest" in
|
||||
*'{device}'*)
|
||||
_gb_out="$_gb_out${_gb_rest%%\{device\}*}$_gb_dev"
|
||||
_gb_rest="${_gb_rest#*\{device\}}"
|
||||
;;
|
||||
*)
|
||||
_gb_out="$_gb_out$_gb_rest"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
printf '%s\n' "$_gb_out"
|
||||
gb_subst_device "$1" "$_gb_dev"
|
||||
}
|
||||
|
||||
# _gb_device_by_hostname -- gitbackup.main.device_id='hostname'.
|
||||
#
|
||||
# Refuses the stock 'OpenWrt' hostname: two routers left at the default would
|
||||
# resolve to the same device name, push to the same branch, and overwrite
|
||||
# each other's backups.
|
||||
# Refuses the stock 'OpenWrt' hostname: two routers left at the default
|
||||
# would push to the same branch and overwrite each other.
|
||||
_gb_device_by_hostname() {
|
||||
_gb_board=$(ubus call system board 2>/dev/null)
|
||||
_gb_h=$(printf '%s' "$_gb_board" | jsonfilter -e '@.hostname')
|
||||
@@ -67,7 +34,6 @@ _gb_device_by_hostname() {
|
||||
printf '%s\n' "$_gb_h"
|
||||
}
|
||||
|
||||
# _gb_device_by_custom -- gitbackup.main.device_id='custom'.
|
||||
_gb_device_by_custom() {
|
||||
_gb_d=$(gb_uci_get gitbackup.main.device)
|
||||
[ -n "$_gb_d" ] ||
|
||||
@@ -81,12 +47,8 @@ _gb_device_by_custom() {
|
||||
printf '%s\n' "$_gb_d"
|
||||
}
|
||||
|
||||
# _gb_device_by_board -- gitbackup.main.device_id='board'.
|
||||
#
|
||||
# <model-slug>-<last 6 hex digits of the first NIC's MAC>, deterministic
|
||||
# because it depends only on hardware, never on config. GB_SYSFS_NET
|
||||
# overrides the sysfs root so tests/run.sh can feed a fake set of interfaces;
|
||||
# the router never sets it and gets the real /sys/class/net.
|
||||
# <model-slug>-<last 6 hex of first NIC MAC>: hardware-only, deterministic.
|
||||
# GB_SYSFS_NET is a test seam.
|
||||
_gb_device_by_board() {
|
||||
_gb_board=$(ubus call system board 2>/dev/null)
|
||||
_gb_model=$(printf '%s' "$_gb_board" | jsonfilter -e '@.model')
|
||||
@@ -113,7 +75,6 @@ _gb_device_by_board() {
|
||||
printf '%s-%s\n' "$(_gb_slugify "$_gb_model")" "$_gb_tail"
|
||||
}
|
||||
|
||||
# _gb_slugify <string> -- lowercase, every byte outside [a-z0-9] becomes '-'.
|
||||
_gb_slugify() {
|
||||
# shellcheck disable=SC2018,SC2019 # ASCII on purpose: device names must match [A-Za-z0-9._-]
|
||||
printf '%s' "$1" | tr 'A-Z' 'a-z' | sed 's/[^a-z0-9]/-/g'
|
||||
|
||||
@@ -1,50 +1,18 @@
|
||||
# gitbackup hard-exclude list.
|
||||
#
|
||||
# One glob pattern per line, matched against an absolute path -- either one
|
||||
# `sysupgrade -l` reported or a directory found while looking for empty
|
||||
# directories (collect.sh, _gb_collect_is_excluded). "#" starts a comment,
|
||||
# blank lines are ignored. A pattern ending in "/**" matches the path
|
||||
# itself and everything under it; anything else is matched with plain
|
||||
# shell `case` glob rules.
|
||||
#
|
||||
# This block is unconditional: it is enforced at every visibility setting.
|
||||
# The extra secret-file excludes visibility=public adds (/etc/shadow,
|
||||
# dropbear host keys, uhttpd.key, /etc/ssl/private/**, WireGuard private
|
||||
# keys -- spec "Scrub") are not here: they depend on a UCI setting this
|
||||
# file has no way to read, and belong to scrub.sh instead (ticket 05).
|
||||
# gitbackup hard-exclude list: one glob per line, matched against absolute
|
||||
# paths (collect.sh). "/**" matches the path and everything under it;
|
||||
# otherwise plain shell `case` glob rules. Enforced at every visibility.
|
||||
|
||||
# The package's own credentials must never end up inside its own backup,
|
||||
# no matter how the router is configured (R76).
|
||||
# The package's own credentials never go into its own backup.
|
||||
/etc/gitbackup/**
|
||||
|
||||
# --- public-only hard-exclude (ticket 05, spec "Scrub") ---
|
||||
#
|
||||
# Every line below starting "#public:" is, to collect.sh, just another
|
||||
# comment -- '#'* is always skipped by _gb_collect_is_excluded, so
|
||||
# collect.sh's own unconditional read of this file (used at every
|
||||
# visibility, private included) never sees these as patterns and its
|
||||
# behavior needs no visibility awareness added to it. scrub.sh is the only
|
||||
# reader that treats "#public:" specially: it strips the prefix and applies
|
||||
# what remains as a hard-exclude pattern against the already-collected tree,
|
||||
# and scrub.sh itself only ever runs when visibility=public -- so the
|
||||
# distinction lives in who reads the line, not in a flag either reader has
|
||||
# to check. These are files no redaction can make safe to publish, because
|
||||
# the secret IS the file, not a value inside it: removed from the tree
|
||||
# entirely and dropped from manifest.json's entries[] (never scrubbed[] --
|
||||
# that array is for redacted option values, not removed files). Same glob
|
||||
# rules as above, applied to the pattern text after "#public:".
|
||||
#
|
||||
# Named verbatim in the brief and spec "Scrub":
|
||||
# --- excluded only while scrubbing ---
|
||||
# "#public: <pattern>" lines are DATA: collect.sh excludes them when the run
|
||||
# scrubs (forced on by visibility=public). Whole-file secrets; no redaction
|
||||
# makes them safe to publish.
|
||||
#public: /etc/shadow
|
||||
#public: /etc/dropbear/*_host_key*
|
||||
#public: /etc/uhttpd.key
|
||||
#public: /etc/ssl/private/**
|
||||
# VERIFY: no OpenWrt-specific source confirms this path is ever populated --
|
||||
# the wireguard package itself keeps keys inline in UCI (network.*.private_key,
|
||||
# already in scrub.list) and has no file-based key storage of its own. This
|
||||
# is the upstream wg-quick convention (a *.conf/key pair under
|
||||
# /etc/wireguard/), kept as defense-in-depth for a router where someone
|
||||
# generated keys by hand (`wg genkey`) and left the file there instead of
|
||||
# only pasting it into UCI -- not verified against an OpenWrt package that
|
||||
# actually writes there.
|
||||
# VERIFY: no OpenWrt package is known to write here (UCI wireguard keeps keys
|
||||
# inline, see scrub.list); wg-quick convention, kept for hand-made keys.
|
||||
#public: /etc/wireguard/**
|
||||
@@ -1,46 +1,14 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- push without a clone (spec "Как коммит попадает в
|
||||
# репозиторий без клона (G05, G06)"). This is the trick the whole package
|
||||
# exists to prove: `git commit-tree` only ever needs the PARENT COMMIT
|
||||
# object to exist in the local object database -- never its tree, never
|
||||
# its blobs.
|
||||
# gitbackup -- push without a clone: gb_remote_head, gb_fetch_meta,
|
||||
# gb_build_tree, gb_commit_push.
|
||||
#
|
||||
# Ticket 07's own V10 check (done before any of this was written, both
|
||||
# against this repo's git 2.51 on the dev host and consistent with git
|
||||
# 2.50.1 on the owlab 25.12.4 stand): a parent SHA that is not in the local
|
||||
# object database AT ALL makes `commit-tree -p <sha>` hard-fail with
|
||||
# "fatal: <sha> is not a valid object" -- so fetching that one commit
|
||||
# object (gb_fetch_meta below) is mandatory, not an optional optimization.
|
||||
# Once just the commit object exists locally, `commit-tree` happily builds
|
||||
# a new tree/commit on top of it, and `git push` negotiates and sends only
|
||||
# the objects the far end is actually missing -- proven with a single
|
||||
# hand-copied commit object and zero blobs, `git fsck --full` clean on the
|
||||
# receiving bare repository afterward.
|
||||
#
|
||||
# Every function here reads GB_URL from the environment rather than taking
|
||||
# it as an argument -- the same convention visibility.sh/auth.sh use for
|
||||
# config context the CLI already resolved once (gb_validate_config) -- so a
|
||||
# caller only ever names what actually varies per call: a branch, a tree, a
|
||||
# directory. gb_build_tree additionally reads GB_PREFIX and GB_PARENT this
|
||||
# same way (see its own comment) for the same reason.
|
||||
#
|
||||
# Sourced, never executed: nothing here runs at load time. Depends on
|
||||
# lib.sh (gb_log) already being sourced by the caller, same convention as
|
||||
# every other module in this directory.
|
||||
# `git commit-tree -p <parent>` needs the parent commit object locally or
|
||||
# fails "not a valid object" -- hence gb_fetch_meta's blobless shallow fetch.
|
||||
# Reads GB_URL, GB_PREFIX, GB_PARENT as resolved by the CLI. Needs lib.sh.
|
||||
|
||||
# gb_remote_head <branch>
|
||||
#
|
||||
# Prints <branch>'s current tip SHA on GB_URL and returns 0; prints nothing
|
||||
# and returns 0 when the branch does not exist yet (a device's first ever
|
||||
# backup) -- verified live: `git ls-remote <a-reachable-repo>
|
||||
# refs/heads/<no-such-branch>` exits 0 with empty output, not an error, as
|
||||
# long as the repository itself was reachable. Prints nothing and returns 1
|
||||
# when `git ls-remote` itself failed (network unreachable, host key
|
||||
# refused, authentication rejected -- git folds all of these into one
|
||||
# nonzero exit and a "fatal: ..." message); turning that into the shared
|
||||
# exit-3 (network/auth) contract is the caller's job, not this module's,
|
||||
# same division of responsibility gb_visibility_ok's 0/3/4 already uses.
|
||||
# gb_remote_head <branch> -- tip SHA, or nothing (rc 0) for a new branch.
|
||||
# Returns 1 when ls-remote fails; mapping to exit 3 is the caller's.
|
||||
gb_remote_head() {
|
||||
_gb_branch="$1"
|
||||
_gb_out=$(git ls-remote "${GB_URL:?gb_remote_head: GB_URL is not set}" \
|
||||
@@ -54,63 +22,10 @@ gb_remote_head() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_fetch_meta <branch> <repodir> [depth]
|
||||
#
|
||||
# Prepares <repodir> as a scratch, non-bare git repository (created empty
|
||||
# the first time; reused as-is on a retry, spec step 14) and fetches only
|
||||
# <branch>'s tip -- commit and trees, never blobs, when the remote
|
||||
# understands partial clone (`--filter=blob:none`).
|
||||
#
|
||||
# <depth> defaults to 1 -- this function's original, and still by far its
|
||||
# most common, shape: `run`'s own "just the tip" need, every call site in
|
||||
# usr/sbin/gitbackup. An explicit empty string ("") skips `--depth`
|
||||
# entirely and fetches the branch's whole history instead, which is what
|
||||
# restore.sh needs to reach an arbitrary past commit (ticket 18: this
|
||||
# parameter is what let restore.sh's own near-identical
|
||||
# init/remote-add/fetch sequence for exactly that one case go away, instead
|
||||
# of staying a second, hand-maintained copy of this same function that
|
||||
# differed from it only in this one flag).
|
||||
#
|
||||
# Not `--filter=tree:0`, even though that measures smaller in the spec's
|
||||
# own table (12 KiB flat vs. 644 KiB for openwrt/luci -- "Проверенные
|
||||
# факты 25.12.4"): the very next thing usr/sbin/gitbackup's run does with
|
||||
# this fetch is read one specific blob out of it (the old manifest.json,
|
||||
# for the unchanged-since-last-time comparison), and `blob:none` leaves
|
||||
# every tree in place so that blob can be fetched lazily by path -- a
|
||||
# `git cat-file -p <parent>:<prefix>/manifest.json` -- instead of a second,
|
||||
# bespoke transfer. Confirmed live (this repo's own git, promisor remote
|
||||
# behavior): after a `--filter=blob:none` fetch against a repository with
|
||||
# `uploadpack.allowFilter` on, the fetched pack carries zero blobs, and the
|
||||
# very next `cat-file -p <path>` transparently pulls in exactly the one
|
||||
# blob asked for and no more (object count went from 8 with blobs already
|
||||
# present to a lazy on-demand fetch of 1 when they were not).
|
||||
#
|
||||
# A remote that does not support the filter extension at all -- confirmed
|
||||
# live against a plain local-path remote, which never speaks the partial-
|
||||
# clone protocol extension regardless of git version -- degrades on its
|
||||
# own to an ordinary `--depth=1` (git's own message: "warning: filtering
|
||||
# not recognized by server, ignoring"). Either way this is still only
|
||||
# <branch>'s single tip commit, on this device's own branch -- never
|
||||
# history, never another device's branch -- so "no clone, ever" holds
|
||||
# regardless of what the remote can do.
|
||||
#
|
||||
# Fetched through a NAMED remote ("origin"), configured once and reused on
|
||||
# the one retry spec step 14 allows, rather than the anonymous-URL form
|
||||
# (`git fetch <url> <branch>`) gb_remote_head/gb_commit_push use -- an
|
||||
# anonymous partial-clone fetch turned out to leave the resulting promisor
|
||||
# pack in a state where a LATER operation needing one of its still-missing
|
||||
# objects could not resolve where to lazily fetch it from again (confirmed
|
||||
# live: an anonymous fetch's own "remote" bookkeeping is the URL string
|
||||
# itself, which git's shorthand-remote-name rule rejects outright for a
|
||||
# plain filesystem path -- "warning: promisor remote name cannot begin
|
||||
# with '/'" -- and the lazy re-fetch a later `write-tree` needs then fails
|
||||
# outright, not just warns). A named remote sidesteps this by construction
|
||||
# and is not a behavior change for a real https/ssh URL, which was never
|
||||
# affected by the rejected-shorthand rule to begin with.
|
||||
#
|
||||
# Returns 0 on success; 1 if the repository could not be initialized or the
|
||||
# fetch itself failed (network/auth -- again the caller's exit-3 to assign,
|
||||
# same convention as gb_remote_head).
|
||||
# gb_fetch_meta <branch> <repodir> [depth] -- depth "" = full history.
|
||||
# blob:none, not tree:0: trees stay so one blob can be lazily fetched by path.
|
||||
# Named remote: an anonymous-URL partial fetch breaks later lazy fetches
|
||||
# ("promisor remote name cannot begin with '/'").
|
||||
gb_fetch_meta() {
|
||||
_gb_branch="$1"
|
||||
_gb_repodir="$2"
|
||||
@@ -138,55 +53,11 @@ gb_fetch_meta() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_build_tree <repodir> <treedir> [extra_file] [extra_gitpath]
|
||||
#
|
||||
# Hashes and stages every regular file and symlink under <treedir> into a
|
||||
# scratch index, rooted at GB_PREFIX (environment, not an argument -- see
|
||||
# this file's header), writes a tree object from that index into <repodir>
|
||||
# and prints its SHA on stdout. <treedir> holds this run's own collected
|
||||
# backup set (collect.sh's outdir) with no prefix baked into it on disk --
|
||||
# the prefix only ever exists inside the git tree this function builds, not
|
||||
# on the filesystem gb_collect wrote to.
|
||||
#
|
||||
# [extra_file]/[extra_gitpath] (ticket 22) stage exactly one additional
|
||||
# regular file at a git path given VERBATIM, bypassing GB_PREFIX entirely --
|
||||
# the one thing usr/sbin/gitbackup needs this for is a device branch's own
|
||||
# README.md, which has to sit at the BRANCH ROOT (a sibling of
|
||||
# "devices/<id>") for GitHub to render it when a human opens that branch
|
||||
# from a phone; a copy staged under GB_PREFIX would never be seen there.
|
||||
# Both empty (the two-argument form every other call site, including every
|
||||
# existing test, still uses) skips this step entirely -- behavior is
|
||||
# unchanged. Mode is always 100644: nothing that calls this today needs an
|
||||
# executable or a symlink at the extra path.
|
||||
#
|
||||
# When GB_PARENT (also environment: the SHA gb_remote_head returned, empty
|
||||
# for a brand-new branch) is set, the index is first seeded from that
|
||||
# commit's FULL tree (`git read-tree`), and GB_PREFIX's own old subtree is
|
||||
# then cleared (`git rm --cached`) before <treedir>'s fresh content is
|
||||
# staged over it. This is what spec step 11 calls out for a branch shared
|
||||
# by several devices (gitbackup.origin.branch with no `{device}`):
|
||||
# everything outside GB_PREFIX survives untouched, and its blobs are never
|
||||
# fetched -- gb_fetch_meta only ever asked for trees, and read-tree alone
|
||||
# needs no blobs either, confirmed live. Clearing GB_PREFIX before restaging
|
||||
# it (rather than only adding paths <treedir> happens to still have) is
|
||||
# what makes a path this device stopped backing up actually disappear from
|
||||
# the pushed tree instead of lingering forever; on the shipped default (a
|
||||
# branch of "device/{device}" per device) GB_PARENT's tree only ever
|
||||
# contained this same GB_PREFIX to begin with, so the clear+restage is a
|
||||
# no-op in effect there, not a special case this function has to detect.
|
||||
#
|
||||
# Only three modes ever reach the index -- git stores no others: 100644,
|
||||
# 100755 (an executable regular file, `[ -x ]`), 120000 (symlink). A
|
||||
# symlink's blob is hashed from the raw target STRING via --stdin, never
|
||||
# from the path: `git hash-object <symlink-path>` opens the path and so
|
||||
# follows the link, and fails outright ("could not open ... No such file or
|
||||
# directory") for the dangling symlinks this backup set is full of
|
||||
# (/etc/resolv.conf and friends) -- confirmed live, and confirmed live that
|
||||
# `printf '%s' <target> | git hash-object --stdin` reproduces exactly the
|
||||
# blob SHA `git add` itself would have stored for that same symlink.
|
||||
#
|
||||
# Returns 0 and prints the tree SHA on success; 1 (nothing printed) if any
|
||||
# git plumbing step failed.
|
||||
# gb_build_tree <repodir> <treedir> [extra_file] [extra_gitpath] -- prints the
|
||||
# tree SHA. With GB_PARENT, the parent tree is seeded and GB_PREFIX cleared:
|
||||
# other devices on a shared branch survive, dropped paths disappear.
|
||||
# Symlinks are hashed from the target string; hashing the path would follow
|
||||
# the link and fail on dangling ones.
|
||||
gb_build_tree() {
|
||||
_gb_repodir="$1"
|
||||
_gb_treedir="$2"
|
||||
@@ -207,11 +78,6 @@ gb_build_tree() {
|
||||
git rm -r --cached --ignore-unmatch -q -- "$_gb_prefix" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# The loop body runs on the read side of a pipe (a subshell, POSIX sh) --
|
||||
# same technique collect.sh's own _gb_collect_files relies on -- but
|
||||
# every git call inside it addresses $_gb_idx as a real file via
|
||||
# GIT_INDEX_FILE, which survives the subshell boundary; only a shell
|
||||
# variable set in there would not.
|
||||
( cd "$_gb_treedir" && find . \( -type f -o -type l \) ) | while IFS= read -r _gb_rel; do
|
||||
_gb_rel="${_gb_rel#./}"
|
||||
_gb_full="$_gb_treedir/$_gb_rel"
|
||||
@@ -249,33 +115,9 @@ gb_build_tree() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_commit_push <repodir> <tree> <parent> <msgfile> <branch>
|
||||
#
|
||||
# Creates one commit from <tree> and <msgfile> -- parented on <parent>, or
|
||||
# a root commit when <parent> is empty (a brand-new branch) -- and pushes
|
||||
# it straight to <branch> on GB_URL: `git push <sha>:refs/heads/<branch>`,
|
||||
# which is what lets git figure out and send only the objects the remote
|
||||
# does not already have, without this side ever holding a checkout of
|
||||
# anything (spec step 12/13).
|
||||
#
|
||||
# Prints the new commit's SHA on stdout and returns 0 on success. On
|
||||
# failure prints nothing and returns:
|
||||
# 1 `commit-tree` itself failed (a bad tree/parent SHA -- a bug on this
|
||||
# side, not a remote problem)
|
||||
# 2 the push was rejected because the branch moved (spec step 14:
|
||||
# "Отказ non-fast-forward -> повтор с шага 8 один раз, дальше exit 1").
|
||||
# Matched against git's own literal wording for BOTH of the two ways
|
||||
# it phrases this same situation -- confirmed live, forcing the race
|
||||
# two different ways: "(non-fast-forward)" when the branch diverged
|
||||
# from a history this side never fetched, "(fetch first)" when this
|
||||
# side's own remote-tracking view is simply stale (the far more
|
||||
# common shape in practice: this side fetched the parent once, then
|
||||
# someone else pushed before this side's own push landed). Both are
|
||||
# "the branch moved under us, re-fetch and retry" to this package.
|
||||
# The caller owns the one retry; this function only reports which
|
||||
# kind of failure happened.
|
||||
# 3 any other push failure (auth rejected, network gone mid-run) -- the
|
||||
# caller's exit 3, same convention as gb_remote_head/gb_fetch_meta.
|
||||
# gb_commit_push <repodir> <tree> <parent> <msgfile> <branch> -- prints the
|
||||
# new SHA. Returns 1 commit-tree failed, 2 branch moved (caller retries once),
|
||||
# 3 other push failure.
|
||||
gb_commit_push() {
|
||||
_gb_repodir="$1"
|
||||
_gb_tree="$2"
|
||||
@@ -284,25 +126,10 @@ gb_commit_push() {
|
||||
_gb_branch="$5"
|
||||
_gb_gitdir="$_gb_repodir/.git"
|
||||
|
||||
# GIT_AUTHOR_*/GIT_COMMITTER_* set explicitly, not left to git's own
|
||||
# fallback chain (user.name/user.email, then a guessed
|
||||
# username@hostname): found live on the owlab stand -- a freshly
|
||||
# booted router has no ~/.gitconfig and no [user] section anywhere,
|
||||
# and commit-tree refuses outright ("Author identity unknown ...
|
||||
# Please tell me who you are") rather than guess. A dev machine's own
|
||||
# global gitconfig had been masking this in every host-side test up to
|
||||
# that point. One fixed bot identity for every device's every commit --
|
||||
# the commit body's own Device: field is what actually identifies
|
||||
# which router pushed it, this is not trying to be a person.
|
||||
if [ -n "$_gb_parent" ]; then
|
||||
_gb_commit=$(GIT_AUTHOR_NAME=gitbackup GIT_AUTHOR_EMAIL=gitbackup@localhost \
|
||||
GIT_COMMITTER_NAME=gitbackup GIT_COMMITTER_EMAIL=gitbackup@localhost \
|
||||
git --git-dir="$_gb_gitdir" commit-tree "$_gb_tree" -p "$_gb_parent" -F "$_gb_msgfile" 2>&1)
|
||||
else
|
||||
_gb_commit=$(GIT_AUTHOR_NAME=gitbackup GIT_AUTHOR_EMAIL=gitbackup@localhost \
|
||||
GIT_COMMITTER_NAME=gitbackup GIT_COMMITTER_EMAIL=gitbackup@localhost \
|
||||
git --git-dir="$_gb_gitdir" commit-tree "$_gb_tree" -F "$_gb_msgfile" 2>&1)
|
||||
fi
|
||||
# Routers have no user.name/email and commit-tree refuses to guess.
|
||||
_gb_commit=$(GIT_AUTHOR_NAME=gitbackup GIT_AUTHOR_EMAIL=gitbackup@localhost \
|
||||
GIT_COMMITTER_NAME=gitbackup GIT_COMMITTER_EMAIL=gitbackup@localhost \
|
||||
git --git-dir="$_gb_gitdir" commit-tree "$_gb_tree" ${_gb_parent:+-p "$_gb_parent"} -F "$_gb_msgfile" 2>&1)
|
||||
_gb_rc=$?
|
||||
if [ "$_gb_rc" -ne 0 ]; then
|
||||
gb_log err "gb_commit_push: commit-tree failed: $_gb_commit"
|
||||
|
||||
@@ -1,37 +1,24 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- primitives shared by every module.
|
||||
# gitbackup -- shared primitives: gb_log, gb_die, gb_uci_get, gb_path_canon,
|
||||
# gb_json_esc/str/bool, gb_manifest_field/tail/each/join, gb_free_kb,
|
||||
# gb_have_net, gb_subst_device, gb_join_lines.
|
||||
#
|
||||
# Sourced, never executed: defining functions is all this file does, so a test
|
||||
# can pull it in and call one function without a router underneath it.
|
||||
#
|
||||
# Variables are prefixed _gb_ instead of being declared `local`, because
|
||||
# `local` is not in POSIX sh; busybox ash has it, the shells this suite also
|
||||
# runs under do not all agree, and the prefix costs nothing.
|
||||
#
|
||||
# Nothing here runs at load time: sourcing this file defines functions and
|
||||
# touches nothing else, which is what makes it testable off the router.
|
||||
# No side effects at load time. _gb_ prefix instead of `local` (not POSIX).
|
||||
|
||||
# gb_log <level> <message>...
|
||||
#
|
||||
# level is a syslog level -- debug, info, notice, warning, err -- so it can be
|
||||
# handed to logger unchanged. Syslog is the log, because `gitbackup log` reads
|
||||
# it back with logread and nothing else survives a reboot on a read-only root.
|
||||
# gb_log <syslog-level> <message>... -- syslog is the log (`gitbackup log`
|
||||
# reads it back with logread).
|
||||
gb_log() {
|
||||
_gb_level="$1"
|
||||
shift
|
||||
logger -t gitbackup -p "daemon.$_gb_level" -- "$*" 2>/dev/null
|
||||
# Anything the operator has to act on also goes to stderr: a CLI run by
|
||||
# hand that answers only into syslog looks like it did nothing.
|
||||
case "$_gb_level" in
|
||||
notice|warning|err) printf '%s: %s\n' "$_gb_level" "$*" >&2 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# gb_die <exit-code> <message>...
|
||||
#
|
||||
# Exit codes are the contract of the whole package: 0 ok, 1 general error,
|
||||
# 2 invalid configuration, 3 network or authentication, 4 refused for safety.
|
||||
# Exit codes: 0 ok, 1 general error, 2 invalid config, 3 network/auth, 4 refused for safety.
|
||||
gb_die() {
|
||||
_gb_code="$1"
|
||||
shift
|
||||
@@ -39,40 +26,18 @@ gb_die() {
|
||||
exit "$_gb_code"
|
||||
}
|
||||
|
||||
# gb_uci_get <config.section.option> [default]
|
||||
#
|
||||
# An unset option and an empty one are the same answer here on purpose: UCI
|
||||
# writes `option x ''` for a field the user cleared in LuCI, and every caller
|
||||
# wants the default in both cases.
|
||||
# gb_uci_get <config.section.option> [default] -- empty yields the default
|
||||
# too: LuCI writes `option x ''` for a cleared field.
|
||||
gb_uci_get() {
|
||||
_gb_value=$(uci -q get "$1") || _gb_value=''
|
||||
[ -n "$_gb_value" ] || _gb_value="${2-}"
|
||||
printf '%s\n' "$_gb_value"
|
||||
}
|
||||
|
||||
# gb_path_canon <path>
|
||||
#
|
||||
# Prints <path> in its one canonical spelling: repeated slashes collapsed,
|
||||
# "." components dropped, ".." components resolved lexically, no trailing
|
||||
# slash. A relative path is printed back unchanged -- there is nothing to
|
||||
# resolve it against here, and every caller checks for absoluteness itself.
|
||||
#
|
||||
# Lexical, not filesystem-resolving: no realpath(1) on the base image, and
|
||||
# `readlink -f` would additionally follow symlinks, which is exactly what
|
||||
# this package must NOT do to a backup path (collect.sh's R24.1 -- a
|
||||
# symlink is backed up as a symlink, never dereferenced).
|
||||
#
|
||||
# This exists because two separate decisions in this package are made by
|
||||
# matching a path against a fixed pattern -- gb_paths_validate's blacklist
|
||||
# and collect.sh's _gb_collect_is_excluded -- and `case` glob matching is
|
||||
# purely textual. "/etc/gitbackup", "//etc/gitbackup", "/etc/./gitbackup"
|
||||
# and "/etc/../etc/gitbackup" name the same directory to the kernel and to
|
||||
# find(1), but only the first one matches a "/etc/gitbackup*" pattern. The
|
||||
# package's own exclude.list calls its exclusion of that directory
|
||||
# unconditional ("must never end up inside its own backup, no matter how
|
||||
# the router is configured", R76); without canonicalization it was not,
|
||||
# and an entry spelled "//etc/gitbackup" pushed the deploy private key and
|
||||
# the git token into the very repository they authenticate to.
|
||||
# gb_path_canon <path> -- lexical canonical spelling (no realpath on the
|
||||
# image; readlink -f would follow symlinks, which are backed up as symlinks).
|
||||
# Exclusion checks are textual `case` matches: without this "//etc/gitbackup"
|
||||
# slips past them and the deploy key and token get pushed.
|
||||
gb_path_canon() {
|
||||
_gb_pc_rest="${1-}"
|
||||
case "$_gb_pc_rest" in
|
||||
@@ -97,16 +62,7 @@ gb_path_canon() {
|
||||
printf '%s\n' "${_gb_pc_out:-/}"
|
||||
}
|
||||
|
||||
# gb_json_esc <string>
|
||||
#
|
||||
# Escapes a string for use inside JSON double quotes. Written out rather than
|
||||
# delegated because the base image has no JSON writer, and "just interpolate
|
||||
# it" breaks on the first quote in a path -- which is exactly the input this
|
||||
# package handles.
|
||||
#
|
||||
# Only the five characters that can actually reach us are escaped: the values
|
||||
# are UCI options, filesystem paths and error messages. A NUL cannot survive a
|
||||
# shell variable in the first place.
|
||||
# The base image has no JSON writer; only ", \, newline, tab and CR can reach us.
|
||||
gb_json_esc() {
|
||||
_gb_rest="${1-}"
|
||||
_gb_out=''
|
||||
@@ -115,7 +71,6 @@ gb_json_esc() {
|
||||
_gb_tab=$(printf '\t')
|
||||
_gb_cr=$(printf '\r')
|
||||
while [ -n "$_gb_rest" ]; do
|
||||
# The first character: strip everything after it, then strip it.
|
||||
_gb_ch="${_gb_rest%"${_gb_rest#?}"}"
|
||||
_gb_rest="${_gb_rest#?}"
|
||||
# shellcheck disable=SC1003 # the '\' branch matches a literal backslash, not an escaped quote
|
||||
@@ -131,7 +86,7 @@ gb_json_esc() {
|
||||
printf '%s' "$_gb_out"
|
||||
}
|
||||
|
||||
# gb_json_str <string> -- an escaped, quoted JSON string, or null when empty.
|
||||
# Quoted JSON string, or null when empty.
|
||||
gb_json_str() {
|
||||
if [ -z "${1-}" ]; then
|
||||
printf 'null'
|
||||
@@ -140,7 +95,6 @@ gb_json_str() {
|
||||
fi
|
||||
}
|
||||
|
||||
# gb_json_bool <value> -- true when the value is a UCI-style truth, else false.
|
||||
gb_json_bool() {
|
||||
case "${1-}" in
|
||||
1|on|true|yes|enabled) printf 'true' ;;
|
||||
@@ -148,40 +102,9 @@ gb_json_bool() {
|
||||
esac
|
||||
}
|
||||
|
||||
# gb_manifest_field <object> <field>
|
||||
#
|
||||
# <object> is one manifest.json array item -- exactly one line, no nested
|
||||
# brackets, the shape gb_collect (collect.sh) writes and gb_manifest_each
|
||||
# (below) hands to its own callback. Prints <field>'s value: unescaped and
|
||||
# unquoted for a JSON string (gb_json_esc above is the only writer, and it
|
||||
# only ever emits "\"" and "\\" for a filesystem path or symlink target --
|
||||
# newlines/tabs/CR do not occur in either), or the raw text up to the next
|
||||
# "," or "}" for a bare (numeric) field. Empty when the field is absent, or
|
||||
# JSON null -- gb_json_str's own bare-word encoding of an empty string
|
||||
# (above), which no caller here ever wants to see as the literal text
|
||||
# "null".
|
||||
#
|
||||
# One reader for what used to be three (ticket 18): collect.sh compared
|
||||
# whole manifests with a hand-rolled sed tail (gb_manifest_tail, below, is
|
||||
# what that became), restore.sh had two near-identical field extractors of
|
||||
# its own (a quoted-string one and a bare-numeric one), and scrub.sh
|
||||
# reached for "path" with one more copy of the same parameter-expansion
|
||||
# pair, unescaped. Same review finding, same fix, in one place instead of
|
||||
# three that could each drift the moment the manifest format ever does.
|
||||
#
|
||||
# The quoted-string branch walks the value one character at a time instead
|
||||
# of cutting at the first literal '"' with a parameter-expansion pattern:
|
||||
# `${v%%\"*}` (the consolidation's first shape, inherited from ticket 08's
|
||||
# _gb_restore_json_str) stops at the FIRST quote character in the string
|
||||
# with no regard for whether a backslash precedes it, so a path containing
|
||||
# a literal quote -- which gb_json_esc above escapes as `\"`, and which
|
||||
# sysupgrade places no restriction against (only whitespace in a path is
|
||||
# unsupported) -- truncates there instead of at its own closing quote. The
|
||||
# writer can produce `\"`; the reader must be able to consume it. Walking
|
||||
# character by character and tracking "the previous character was an
|
||||
# unescaped backslash" finds the real closing quote and un-escapes `\"`/`\\`
|
||||
# in the same pass, so the trailing `sed` cleanup this branch used to need
|
||||
# is gone -- there is nothing left for it to fix up.
|
||||
# gb_manifest_field <object> <field> -- one single-line manifest item; empty
|
||||
# when absent or null. Walks char by char so an escaped \" inside a path does
|
||||
# not end the value early.
|
||||
gb_manifest_field() {
|
||||
_gb_mf_obj="$1"
|
||||
_gb_mf_field="$2"
|
||||
@@ -194,10 +117,6 @@ gb_manifest_field() {
|
||||
_gb_mf_ch="${_gb_mf_rest%"${_gb_mf_rest#?}"}"
|
||||
_gb_mf_rest="${_gb_mf_rest#?}"
|
||||
if [ "$_gb_mf_bs" -eq 1 ]; then
|
||||
# Whatever follows a backslash is emitted as itself:
|
||||
# gb_json_esc only ever escapes '"' and '\' into a
|
||||
# manifest string field, so this covers both cases
|
||||
# without needing to special-case which one it was.
|
||||
_gb_mf_v="$_gb_mf_v$_gb_mf_ch"
|
||||
_gb_mf_bs=0
|
||||
continue
|
||||
@@ -221,35 +140,15 @@ gb_manifest_field() {
|
||||
esac
|
||||
}
|
||||
|
||||
# gb_manifest_tail <manifest.json> [section]
|
||||
#
|
||||
# Everything from ' "<section>": [' (default "entries") onward, i.e. that
|
||||
# key's own array plus every key written after it, to end of file. Relies
|
||||
# on gb_collect's fixed layout (collect.sh): "entries" and "scrubbed" are
|
||||
# always the last two top-level keys, one object per line, so this one sed
|
||||
# range captures both without a JSON parser this image does not have.
|
||||
# gb_manifest_equal (collect.sh) is the one caller: comparing this tail
|
||||
# between two manifests is exactly comparing entries[] and scrubbed[]
|
||||
# together (spec: "generated не участвует в сравнении манифестов").
|
||||
# gb_manifest_tail <manifest.json> [section] -- section key to EOF. Relies on
|
||||
# gb_collect keeping "entries" and "scrubbed" last, so comparing tails
|
||||
# ignores "generated".
|
||||
gb_manifest_tail() {
|
||||
sed -n '/^ "'"${2:-entries}"'": \[/,$p' "$1"
|
||||
}
|
||||
|
||||
# gb_manifest_each <manifest.json> <section> <callback>
|
||||
#
|
||||
# Calls "<callback> <object>" once per <section>'s array item (e.g.
|
||||
# "entries" or "scrubbed"), <object> already unindented and with its
|
||||
# trailing comma (present on every item but the last) stripped -- the
|
||||
# shape gb_manifest_field expects. Reads the file via a plain redirect, not
|
||||
# a pipe: POSIX sh only forks a subshell for the pipe form, so a callback's
|
||||
# own global-variable accumulation (no `local` anywhere in this codebase)
|
||||
# survives the loop here.
|
||||
#
|
||||
# A section's own closing line is either ' ],' (something else follows it
|
||||
# in the object, e.g. "entries" with "scrubbed" after) or ' ]' (it is the
|
||||
# last key, e.g. "scrubbed" itself); both are accepted regardless of which
|
||||
# section was asked for, because the two sections never overlap and each
|
||||
# one's own opening line is what starts the count in the first place.
|
||||
# gb_manifest_each <manifest.json> <section> <callback> -- one call per item.
|
||||
# Redirect, not a pipe, so the callback's globals survive the loop.
|
||||
gb_manifest_each() {
|
||||
_gb_me_manifest="$1"
|
||||
_gb_me_section="$2"
|
||||
@@ -271,54 +170,14 @@ gb_manifest_each() {
|
||||
done <"$_gb_me_manifest"
|
||||
}
|
||||
|
||||
# gb_free_kb <directory> -- free kilobytes on the filesystem holding it.
|
||||
#
|
||||
# -P because the default df output wraps long device names onto their own line
|
||||
# and the awk below would then read the wrong row.
|
||||
# -P keeps long device names from wrapping onto their own line.
|
||||
gb_free_kb() {
|
||||
df -Pk "$1" 2>/dev/null | awk 'NR==2 {print $4}'
|
||||
}
|
||||
|
||||
# gb_have_net <host> [port]
|
||||
#
|
||||
# True when a TCP connection opens. There is no curl in the base image, and
|
||||
# -- D01, measured live on the owlab 25.12.4 stand -- this image's busybox
|
||||
# nc (v1.37.0) is not the usual BSD/GNU nc at all: `nc --help` prints
|
||||
# "Usage: nc [IPADDR PORT]" and nothing else. No `-w`, no `-z`, no flags of
|
||||
# any kind; `nc -w 5 host port` (this function's previous body) prints that
|
||||
# same usage banner and exits 1 for every host, reachable or not, which is
|
||||
# exactly the bug ticket 01 flagged as D01 and interfaces.md warned every
|
||||
# later module off relying on. The fix is simply never passing nc a flag it
|
||||
# does not have: `nc host port` alone is accepted, connects, and exits 0/1
|
||||
# correctly -- confirmed live for a real open port, a closed one (ECONNREFUSED)
|
||||
# and a bad hostname.
|
||||
#
|
||||
# The remaining problem a flag-free nc leaves is a *hang*: an address that
|
||||
# is filtered rather than refused (no RST, no reply at all) leaves a bare
|
||||
# `nc host port` blocked on the kernel's own TCP connect timeout, which is
|
||||
# well over a minute -- unacceptable for a check `run` is supposed to fail
|
||||
# out of quickly. There is no `timeout` applet either (confirmed: absent
|
||||
# from `busybox --list` and no separate binary on the stand), so the bound
|
||||
# is hand-rolled by POLLING, not by a second background "watchdog" job that
|
||||
# kills nc after 5s: an earlier version of this function did exactly that
|
||||
# (`( sleep 5; kill $ncpid ) & watchpid=$!; ...; kill $watchpid`), and it
|
||||
# leaked a process every time nc finished BEFORE the 5s elapsed (the common
|
||||
# case) -- found live on the owlab stand, not in any unit test: killing the
|
||||
# watchdog SUBSHELL does not kill the plain `sleep 5` still running inside
|
||||
# it, which is a separate child process with its own pid; that orphaned
|
||||
# sleep keeps running for whatever time was left, still holding every file
|
||||
# descriptor it inherited at the moment it was forked -- including, when
|
||||
# `run`'s own step 1 flock (fd 9 on /var/run/gitbackup.lock) was already
|
||||
# held at the time gb_have_net was called, the lock file itself. Confirmed
|
||||
# live: calling `run` twice back to back after a fast, successful
|
||||
# gb_have_net check left the SECOND call seeing the lock as still busy for
|
||||
# up to ~5s, purely from the first call's orphaned watchdog sleep -- despite
|
||||
# the first `run` process having already exited outright. Polling for nc's
|
||||
# own pid to disappear (`kill -0`, no signal sent, just an existence check)
|
||||
# needs no second process at all, so there is nothing left over to leak.
|
||||
# Measured live: an address that never answers (192.0.2.1, a TEST-NET-1
|
||||
# address routed nowhere) returns in ~5s instead of hanging; a real open
|
||||
# port and a real closed port both still return in well under a second.
|
||||
# gb_have_net <host> [port] -- TCP connect within ~5s. busybox nc takes no
|
||||
# -w/-z and there is no `timeout`, so nc's pid is polled. No `sleep; kill`
|
||||
# watchdog: its orphaned sleep kept run's flock fd open after exit.
|
||||
gb_have_net() {
|
||||
_gb_hn_host="$1"
|
||||
_gb_hn_port="${2:-443}"
|
||||
@@ -337,3 +196,30 @@ gb_have_net() {
|
||||
wait "$_gb_hn_pid" 2>/dev/null
|
||||
return $?
|
||||
}
|
||||
|
||||
# Device comes from the caller: restore has no device_id config yet.
|
||||
gb_subst_device() {
|
||||
_gb_sd_out=''
|
||||
_gb_sd_rest="$1"
|
||||
while true; do
|
||||
case "$_gb_sd_rest" in
|
||||
*'{device}'*)
|
||||
_gb_sd_out="$_gb_sd_out${_gb_sd_rest%%\{device\}*}$2"
|
||||
_gb_sd_rest="${_gb_sd_rest#*\{device\}}"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "$_gb_sd_out$_gb_sd_rest"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
gb_join_lines() {
|
||||
awk -v sep="$1" '$0 != "" { printf "%s%s", s, $0; s = sep }'
|
||||
}
|
||||
|
||||
# stdin lines as a manifest JSON array body.
|
||||
gb_manifest_join() {
|
||||
grep . | sed 's/^/ /; $!s/$/,/'
|
||||
}
|
||||
@@ -1,90 +1,27 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- Paths: the /etc/sysupgrade.conf editor (ticket 17, spec
|
||||
# "Paths", "Сбор и manifest.json", "Проверенные факты 25.12.4 -> sysupgrade").
|
||||
#
|
||||
# The whole point (spec, verbatim): "Расширение списка -- редактирование
|
||||
# /etc/sysupgrade.conf, а не отдельный UCI-список. То, что пользователь
|
||||
# добавил ради бэкапа, автоматически переживёт реальный sysupgrade." This
|
||||
# module owns that file, its blacklist, and the effective-set size
|
||||
# estimate; usr/sbin/gitbackup's `paths` subcommand is its only caller
|
||||
# today, but nothing here assumes that -- a future rpcd method could call
|
||||
# these same functions instead of re-deriving the blacklist a second time.
|
||||
#
|
||||
# GB_SYSUPGRADE_CONF overrides the file this module reads/writes (default
|
||||
# /etc/sysupgrade.conf) -- the same override name
|
||||
# usr/libexec/rpcd/luci.gitbackup's own gbrpc_set_paths already uses for
|
||||
# the identical file. GB_ROOT overrides the filesystem root existence
|
||||
# checks resolve against, same convention collect.sh/restore.sh already
|
||||
# use, so tests can point this at a fixture tree instead of the host
|
||||
# filesystem.
|
||||
#
|
||||
# Depends on lib.sh (gb_log) already being sourced by the caller, same
|
||||
# convention every sibling module here uses. Sourced, never executed:
|
||||
# nothing here runs at load time -- both variable defaults below are cheap
|
||||
# parameter expansions, not commands.
|
||||
# gitbackup -- editor for /etc/sysupgrade.conf, so paths added for backup also
|
||||
# survive a real sysupgrade. Exposes gb_paths_list, gb_paths_entries,
|
||||
# gb_paths_size_kb, gb_paths_validate, gb_paths_add, gb_paths_del,
|
||||
# gb_paths_replace_entries. Path validation lives only here, not in JS.
|
||||
# Needs lib.sh.
|
||||
|
||||
GB_SYSUPGRADE_CONF="${GB_SYSUPGRADE_CONF:-/etc/sysupgrade.conf}"
|
||||
GB_ROOT="${GB_ROOT:-}"
|
||||
|
||||
# gb_paths_list -- the raw content of GB_SYSUPGRADE_CONF, one line per
|
||||
# call. This is deliberately NOT sysupgrade -l's own wider effective set
|
||||
# (spec: that is GB_SYSUPGRADE_CONF union /lib/upgrade/keep.d/* union
|
||||
# changed conffiles) -- a package's own paths editor has nothing to add or
|
||||
# remove from the other two sources, so listing them here would be lines
|
||||
# `paths del` could never actually delete. gb_paths_size_kb below is the
|
||||
# one place the wider effective set matters.
|
||||
# The file verbatim, comments included -- writes rely on it. Not the effective
|
||||
# set (keep.d, conffiles): this editor cannot change those.
|
||||
gb_paths_list() {
|
||||
[ -r "$GB_SYSUPGRADE_CONF" ] && cat "$GB_SYSUPGRADE_CONF"
|
||||
return 0
|
||||
}
|
||||
|
||||
# _gb_paths_is_comment_or_blank <line> -- true (0) when <line> is not a real
|
||||
# path entry: empty, or a sysupgrade.conf comment. sysupgrade.conf's own
|
||||
# comment syntax is "first character is '#'" -- nothing past that character
|
||||
# is ever inspected, so "# /etc/openvpn/" is a commented-out EXAMPLE line,
|
||||
# not a disabled entry for /etc/openvpn/, and backs up nothing at all
|
||||
# (ticket 26). This is the one and only place that decides what counts as
|
||||
# "not an entry" -- gb_paths_entries (what gets listed) and
|
||||
# gb_paths_replace_entries (what gets preserved on a full-list write) both
|
||||
# call it, so the two can never quietly disagree about the same line.
|
||||
_gb_paths_is_comment_or_blank() {
|
||||
case "$1" in
|
||||
'' | '#'*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# gb_paths_entries -- gb_paths_list, filtered down to genuine path entries:
|
||||
# blank lines and comments (see _gb_paths_is_comment_or_blank) are dropped.
|
||||
# gb_paths_list itself is left alone and stays raw -- ticket 26 chose two
|
||||
# functions with two distinct meanings ("what the file literally contains"
|
||||
# vs. "what a human's own path list looks like") over one function whose
|
||||
# answer would depend on which caller was asking. This is what the LuCI
|
||||
# view and `paths list --json`'s new "entries" field are built on: a stock
|
||||
# /etc/sysupgrade.conf's four header/example lines are comments, contain
|
||||
# zero real entries, and must render as an empty, unremovable list.
|
||||
gb_paths_entries() {
|
||||
gb_paths_list | while IFS= read -r _gb_pe_l || [ -n "$_gb_pe_l" ]; do
|
||||
_gb_paths_is_comment_or_blank "$_gb_pe_l" && continue
|
||||
printf '%s\n' "$_gb_pe_l"
|
||||
done
|
||||
gb_paths_list | grep -v -e '^$' -e '^#'
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_paths_size_kb -- kilobytes of everything `sysupgrade -l` would
|
||||
# actually collect (the full effective set: GB_SYSUPGRADE_CONF union
|
||||
# keep.d union changed conffiles -- the union sysupgrade itself computes,
|
||||
# not just this file's own lines), read straight off the filesystem with
|
||||
# no copy made. Moved here from usr/sbin/gitbackup's own former
|
||||
# _gb_run_estimate_kb (ticket 07) so `run`'s pre-flight space check and
|
||||
# `paths`'s own 2 MB warning threshold (spec "LuCI -> Paths": "Счётчик
|
||||
# суммарного размера набора с предупреждением на 2 MB") can never quietly
|
||||
# disagree about what "the size of the backup set" means. Symlinks are
|
||||
# skipped (their own size is a few bytes and does not matter to either
|
||||
# caller's headroom math); a path sysupgrade -l names that is no longer
|
||||
# there contributes 0, same tolerance gb_collect already has for that
|
||||
# case.
|
||||
# KB of the full effective set (`sysupgrade -l`); symlinks count as 0.
|
||||
gb_paths_size_kb() {
|
||||
sysupgrade -l 2>/dev/null | while IFS= read -r _gb_pkb_p || [ -n "$_gb_pkb_p" ]; do
|
||||
[ -n "$_gb_pkb_p" ] || continue
|
||||
@@ -93,27 +30,9 @@ gb_paths_size_kb() {
|
||||
done | awk '{s += $1} END {printf "%d", (s + 1023) / 1024}'
|
||||
}
|
||||
|
||||
# gb_paths_validate <path>
|
||||
#
|
||||
# 0 when <path> may be added to GB_SYSUPGRADE_CONF; on refusal, prints one
|
||||
# human-readable reason to stderr and returns a code from this package's
|
||||
# own shared contract (interfaces.md "Коды выхода") -- 2 for a plain bad
|
||||
# argument (not absolute, contains a space, not canonically spelled, does
|
||||
# not exist), 4 for a refusal on safety grounds (the fixed blacklist) --
|
||||
# so a caller can pass the return value straight to gb_die without having
|
||||
# to reclassify it.
|
||||
#
|
||||
# Checked in the order a human would want to hear about it: absolute
|
||||
# first (everything below assumes that), then the one syntactic limit
|
||||
# `sysupgrade -l`'s own `find` invocation imposes (spec "Проверенные
|
||||
# факты 25.12.4 -> sysupgrade": "пути с пробелами не поддерживаются
|
||||
# конструктивно"), then the one spelling every textual check below (and
|
||||
# collect.sh's own hard-exclude) is able to reason about, then the fixed
|
||||
# blacklist (never configurable -- ticket 17's own brief: "запрет на
|
||||
# /etc/gitbackup/**, /proc, /sys, /tmp"), and
|
||||
# only last whether the path actually exists -- there is no point telling
|
||||
# an operator their entry is missing from disk before telling them it was
|
||||
# never going to be accepted at all.
|
||||
# gb_paths_validate <path> -- 0, or a reason on stderr and 2 (bad argument) /
|
||||
# 4 (blacklisted). Spaces are refused: sysupgrade passes lines to find(1)
|
||||
# unquoted.
|
||||
gb_paths_validate() {
|
||||
_gb_pv_path="$1"
|
||||
|
||||
@@ -132,25 +51,8 @@ gb_paths_validate() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# The blacklist below is `case` glob matching, i.e. purely textual, so
|
||||
# it only holds for one spelling of each path. "//etc/gitbackup",
|
||||
# "/etc/./gitbackup" and "/etc/../etc/gitbackup" all name the reserved
|
||||
# directory to find(1) -- which is what a sysupgrade.conf line becomes
|
||||
# -- while matching none of the patterns that reserve it, and
|
||||
# collect.sh's own hard-exclude of that directory is textual in the
|
||||
# same way. An entry spelled like that therefore used to sail past
|
||||
# both and put /etc/gitbackup/token and the deploy private key into
|
||||
# the backup repository itself.
|
||||
#
|
||||
# Refused rather than silently rewritten to the canonical spelling:
|
||||
# what lands in sysupgrade.conf is what the operator (or the LuCI
|
||||
# Paths view) asked for, verbatim, everywhere else in this file --
|
||||
# gb_paths_add's idempotence and gb_paths_del's exact-line removal
|
||||
# both depend on that -- and quietly editing an entry on the way in
|
||||
# would break the "add what I typed, remove what I see" contract for
|
||||
# the sake of one accepted keystroke. The canonical spelling is named
|
||||
# in the message instead, and reaches the Paths view as the rejection
|
||||
# reason gbrpc_set_paths already reports per entry.
|
||||
# The blacklist is textual, so only one spelling may pass; refused rather
|
||||
# than rewritten, since entries are stored exactly as given.
|
||||
_gb_pv_canon=$(gb_path_canon "$_gb_pv_path")
|
||||
if [ "$_gb_pv_canon" != "$_gb_pv_path" ]; then
|
||||
printf '%s: not a canonical path -- write it as %s\n' "$_gb_pv_path" "$_gb_pv_canon" >&2
|
||||
@@ -176,10 +78,7 @@ gb_paths_validate() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# -e alone misses a dangling symlink (lstat vs stat) -- and this
|
||||
# backup set is full of legitimate ones, /etc/resolv.conf being the
|
||||
# standard example (collect.sh's own R24.1 comment) -- so -L is
|
||||
# checked too before refusing.
|
||||
# -L too: -e misses dangling symlinks such as /etc/resolv.conf.
|
||||
if [ ! -e "$GB_ROOT$_gb_pv_path" ] && [ ! -L "$GB_ROOT$_gb_pv_path" ]; then
|
||||
printf '%s: no such file or directory\n' "$_gb_pv_path" >&2
|
||||
return 2
|
||||
@@ -188,11 +87,6 @@ gb_paths_validate() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_paths_add <path> -- appends <path> to GB_SYSUPGRADE_CONF unless
|
||||
# gb_paths_validate refuses it (its own reason already on stderr by then,
|
||||
# its return code propagated unchanged) or it is already there verbatim
|
||||
# (idempotent: spec/ticket 17 "add/del правят именно его, идемпотентно" --
|
||||
# running add twice must not duplicate the line).
|
||||
gb_paths_add() {
|
||||
_gb_pa_path="$1"
|
||||
gb_paths_validate "$_gb_pa_path" || return $?
|
||||
@@ -204,23 +98,7 @@ gb_paths_add() {
|
||||
printf '%s\n' "$_gb_pa_path" >>"$GB_SYSUPGRADE_CONF"
|
||||
}
|
||||
|
||||
# gb_paths_del <path> -- removes every line equal to <path>, verbatim.
|
||||
# Silently succeeds when it was not there to begin with -- same
|
||||
# idempotence gb_paths_add already provides, in the other direction.
|
||||
#
|
||||
# Deliberately NOT routed through gb_paths_validate (ticket 26 checked
|
||||
# this): an exact-line removal of something that is not a valid entry is
|
||||
# harmless -- there is nothing to protect against by refusing to delete a
|
||||
# line that was never going to be backed up in the first place -- and a
|
||||
# human running this subcommand by hand is trusted the way `rm` is. This
|
||||
# does mean `gitbackup paths del '## some comment'` will happily strip
|
||||
# that exact comment line the same way it would any other line; that is
|
||||
# an accepted property of a deliberately unvalidated CLI subcommand, not a
|
||||
# defect. The actual bug ticket 26 fixed lived one layer up: the LuCI view
|
||||
# never called this function at all for its own "Remove" button (it always
|
||||
# went through `set_paths`'s full-list replace, see gbrpc_set_paths in
|
||||
# usr/libexec/rpcd/luci.gitbackup), and gb_paths_entries below is what
|
||||
# stops that button from ever being offered for a comment line again.
|
||||
# Deliberately unvalidated: must be able to remove any stale entry.
|
||||
gb_paths_del() {
|
||||
_gb_pd_path="$1"
|
||||
[ -r "$GB_SYSUPGRADE_CONF" ] || return 0
|
||||
@@ -230,36 +108,16 @@ gb_paths_del() {
|
||||
mv "$_gb_pd_tmp" "$GB_SYSUPGRADE_CONF"
|
||||
}
|
||||
|
||||
# gb_paths_replace_entries <entries> -- rewrites GB_SYSUPGRADE_CONF as:
|
||||
# every comment/blank line gb_paths_list already contains, in its original
|
||||
# order and wording, completely unchanged -- followed by <entries>
|
||||
# (newline-separated, blank lines ignored), one per line, exactly as
|
||||
# given. Ticket 13's decision that a full-list write (`set_paths`, the
|
||||
# rpcd plugin's only way to persist the LuCI Paths view's edits) writes
|
||||
# back VERBATIM what it was handed stays -- a directory entry is still
|
||||
# never expanded here. What changes is what "the file" means during that
|
||||
# rewrite: it used to mean "only what the caller sent", so the very first
|
||||
# save from the Paths view against a stock /etc/sysupgrade.conf silently
|
||||
# erased its own header comments and the two commented-out example lines,
|
||||
# because the caller (the LuCI view, then `gbrpc_set_paths`) never saw
|
||||
# them in the first place -- gb_paths_entries (this same module) is what
|
||||
# the view lists and edits, and comments are excluded from it by design.
|
||||
# Losing someone else's lines on a save they never touched is not
|
||||
# acceptable (ticket 26's own brief), so this function -- the one place a
|
||||
# full-list write happens -- re-reads the comments straight from disk
|
||||
# and keeps them, unconditionally, regardless of what the caller sent.
|
||||
# Keeps the file's comment lines (a save must not erase the stock header),
|
||||
# then <entries> verbatim. Never expand directories into the effective set:
|
||||
# files created later would silently stop being backed up.
|
||||
gb_paths_replace_entries() {
|
||||
_gb_pre_tmp="${GB_SYSUPGRADE_CONF}.tmp.$$"
|
||||
mkdir -p "$(dirname "$GB_SYSUPGRADE_CONF")" 2>/dev/null
|
||||
|
||||
{
|
||||
gb_paths_list | while IFS= read -r _gb_pre_l || [ -n "$_gb_pre_l" ]; do
|
||||
_gb_paths_is_comment_or_blank "$_gb_pre_l" && printf '%s\n' "$_gb_pre_l"
|
||||
done
|
||||
printf '%s\n' "$1" | while IFS= read -r _gb_pre_e || [ -n "$_gb_pre_e" ]; do
|
||||
[ -n "$_gb_pre_e" ] || continue
|
||||
printf '%s\n' "$_gb_pre_e"
|
||||
done
|
||||
gb_paths_list | grep -e '^$' -e '^#'
|
||||
printf '%s\n' "$1" | grep .
|
||||
} >"$_gb_pre_tmp"
|
||||
|
||||
mv "$_gb_pre_tmp" "$GB_SYSUPGRADE_CONF"
|
||||
|
||||
@@ -1,30 +1,11 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- remote URL parsing, provider detection, deploy-key deep links
|
||||
# (spec "Парсер remote URL", brief §5.3).
|
||||
#
|
||||
# Sourced, never executed: nothing here runs at load time. gb_provider reads
|
||||
# gitbackup.origin.provider through gb_uci_get (lib.sh), so a caller must
|
||||
# source lib.sh first, same convention as device.sh.
|
||||
# gitbackup -- remote URL parsing: gb_parse_url, gb_provider. Needs lib.sh.
|
||||
|
||||
# gb_parse_url <url>
|
||||
#
|
||||
# Parses one of the four remote URL forms the spec requires, with or without
|
||||
# a trailing ".git":
|
||||
# git@host:owner/repo.git scp-like (git's own alias syntax)
|
||||
# ssh://[user@]host[:port]/owner/repo.git
|
||||
# https://host[:port]/owner/repo.git
|
||||
#
|
||||
# Prints "scheme host port owner repo" on one line, so a caller does
|
||||
# `set -- $(gb_parse_url "$url")`. port is "0" when the URL did not name
|
||||
# one -- the parser does not guess a scheme's default port, because what
|
||||
# that default even means differs by caller (auth.sh's ssh -p vs.
|
||||
# visibility.sh's HTTPS API host, see visibility.sh's _gb_hostport_for_api).
|
||||
#
|
||||
# Returns 1 and prints nothing on anything that is not one of the three
|
||||
# forms above, or where owner/repo fail the charset check -- a mangled URL
|
||||
# must fail outright, never be parsed halfway into a wrong-but-plausible
|
||||
# result (ticket 03 acceptance criterion).
|
||||
# gb_parse_url <url> -- accepts [user@]host:owner/repo, ssh://[user@]host[:port]/
|
||||
# owner/repo, https://host[:port]/owner/repo (".git" optional). Prints
|
||||
# "scheme host port owner repo", port 0 when absent. Anything else: return 1,
|
||||
# no output -- never half-parses.
|
||||
gb_parse_url() {
|
||||
_gb_u="$1"
|
||||
_gb_scheme=''
|
||||
@@ -48,15 +29,10 @@ gb_parse_url() {
|
||||
_gb_parse_hostport_path "$_gb_rest" || return 1
|
||||
;;
|
||||
*://*)
|
||||
# Some other scheme (git://, ftp://, ...) -- not one of the three
|
||||
# forms the spec names, refused rather than guessed at.
|
||||
return 1
|
||||
;;
|
||||
*:*)
|
||||
# scp-like alias syntax: "[user@]host:owner/repo(.git)?". Git itself
|
||||
# only accepts this when the part before the first ':' has no '/' --
|
||||
# that is what tells it apart from a local path or an already-handled
|
||||
# scheme above.
|
||||
# git treats it as scp-like only with no '/' before the first ':'.
|
||||
_gb_scheme=ssh
|
||||
_gb_port=''
|
||||
_gb_userhost="${_gb_u%%:*}"
|
||||
@@ -79,11 +55,7 @@ gb_parse_url() {
|
||||
printf '%s %s %s %s %s\n' "$_gb_scheme" "$_gb_host" "${_gb_port:-0}" "$_gb_owner" "$_gb_repo"
|
||||
}
|
||||
|
||||
# _gb_parse_hostport_path <host[:port]/owner/repo[.git]>
|
||||
#
|
||||
# Shared by the https:// and ssh:// branches once their scheme prefix (and,
|
||||
# for ssh://, an optional user@) is already stripped. Sets _gb_host, _gb_port
|
||||
# and, through _gb_parse_ownerrepo, _gb_owner/_gb_repo.
|
||||
# Sets _gb_host, _gb_port, _gb_owner, _gb_repo.
|
||||
_gb_parse_hostport_path() {
|
||||
_gb_r="$1"
|
||||
case "$_gb_r" in
|
||||
@@ -109,10 +81,7 @@ _gb_parse_hostport_path() {
|
||||
_gb_parse_ownerrepo "$_gb_path"
|
||||
}
|
||||
|
||||
# _gb_parse_ownerrepo <owner/repo[.git]> -- sets _gb_owner and _gb_repo.
|
||||
#
|
||||
# Exactly one '/' is allowed: nested groups ("group/subgroup/repo") are not
|
||||
# one of the four supported forms and are refused, not guessed at.
|
||||
# Exactly one '/': nested groups are refused, not guessed at.
|
||||
_gb_parse_ownerrepo() {
|
||||
_gb_p="$1"
|
||||
case "$_gb_p" in
|
||||
@@ -128,7 +97,6 @@ _gb_parse_ownerrepo() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# _gb_charset_ok <string> -- non-empty and only [A-Za-z0-9._-].
|
||||
_gb_charset_ok() {
|
||||
[ -n "$1" ] || return 1
|
||||
case "$1" in
|
||||
@@ -137,18 +105,8 @@ _gb_charset_ok() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_provider <host>
|
||||
#
|
||||
# github.com/gitlab.com/bitbucket.org/codeberg.org are recognized by host;
|
||||
# anything else is "generic" unless gitbackup.origin.provider names one
|
||||
# explicitly, which always wins over host detection (ticket 03 acceptance
|
||||
# criterion) -- the one escape hatch for a self-hosted GitLab/Gitea/Forgejo
|
||||
# instance, which cannot be told apart from an unknown host by name alone.
|
||||
#
|
||||
# codeberg.org maps to "gitea": Codeberg runs Forgejo, whose anonymous repo
|
||||
# API is the same shape as Gitea's (spec, "Проверенные факты 25.12.4 →
|
||||
# Провайдеры"), and gitbackup.origin.provider's enum has no separate
|
||||
# "codeberg"/"forgejo" value to return instead.
|
||||
# gb_provider <host> -- a non-auto gitbackup.origin.provider wins (self-hosted
|
||||
# escape hatch). codeberg.org is Forgejo, whose API is Gitea's.
|
||||
gb_provider() {
|
||||
_gb_host="$1"
|
||||
_gb_opt=$(gb_uci_get gitbackup.origin.provider auto)
|
||||
@@ -164,53 +122,3 @@ gb_provider() {
|
||||
*) printf 'generic\n' ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# gb_deeplink <provider> <scheme> <host> <owner> <repo>
|
||||
#
|
||||
# A URL to the provider's "add deploy key" page, or -- for generic, which has
|
||||
# no such page anywhere -- an instruction to add the key to authorized_keys
|
||||
# by hand (brief §5.3).
|
||||
#
|
||||
# GitHub and Bitbucket are hardcoded to their one cloud host: gb_provider
|
||||
# only ever returns "github"/"bitbucket" for github.com/bitbucket.org, so
|
||||
# there is no self-hosted variant to build a different host for. GitLab and
|
||||
# Gitea/Forgejo are commonly self-hosted, so their link is built from the
|
||||
# remote's own host instead -- the brief's table shows a gitlab.com example,
|
||||
# but GitLab's repository-settings page lives at the same path on every
|
||||
# instance, not just gitlab.com. <scheme> is coerced to https: a deploy-key
|
||||
# page is only ever reachable over the web, but the parsed scheme is often
|
||||
# "ssh" (the git remote's own transport, from an scp-like or ssh:// URL),
|
||||
# which is never a valid scheme for a browser link.
|
||||
gb_deeplink() {
|
||||
_gb_p="$1"
|
||||
_gb_sch="$2"
|
||||
_gb_h="$3"
|
||||
_gb_o="$4"
|
||||
_gb_r="$5"
|
||||
case "$_gb_sch" in
|
||||
http|https) ;;
|
||||
*) _gb_sch=https ;;
|
||||
esac
|
||||
case "$_gb_p" in
|
||||
github)
|
||||
# VERIFY: an anonymous request to /settings/keys/new returns 404 for
|
||||
# both a real and a garbage owner/repo (spec, "Проверенные факты"),
|
||||
# so unlike the other three this exact path could not be confirmed
|
||||
# by the same probe -- it is GitHub's documented deploy-key URL, not
|
||||
# a measured one.
|
||||
printf 'https://github.com/%s/%s/settings/keys/new\n' "$_gb_o" "$_gb_r"
|
||||
;;
|
||||
gitlab)
|
||||
printf '%s://%s/%s/%s/-/settings/repository\n' "$_gb_sch" "$_gb_h" "$_gb_o" "$_gb_r"
|
||||
;;
|
||||
gitea)
|
||||
printf '%s://%s/%s/%s/settings/keys\n' "$_gb_sch" "$_gb_h" "$_gb_o" "$_gb_r"
|
||||
;;
|
||||
bitbucket)
|
||||
printf 'https://bitbucket.org/%s/%s/admin/access-keys/\n' "$_gb_o" "$_gb_r"
|
||||
;;
|
||||
*)
|
||||
printf 'No provider API to link to; add the deploy key to ~/.ssh/authorized_keys for the git user on %s, ideally with restrict,command="git-shell".\n' "$_gb_h"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -1,79 +1,19 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- Path 2 restore, applying a backup set straight back onto the
|
||||
# filesystem (spec "Восстановление"). This is the module that makes
|
||||
# manifest.json's mode/uid/gid/target fields matter: git stores none of
|
||||
# them, so a plain checkout of files/ would hand back /etc/shadow at
|
||||
# whatever mode git happens to give a new file (world-readable) and every
|
||||
# dropbear host key owned by whoever ran the restore -- a router that
|
||||
# LOOKS restored but that dropbear and login refuse to trust.
|
||||
# gitbackup -- restore: gb_restore. Needs lib.sh and gitio.sh; GB_URL from
|
||||
# the environment. Applies manifest mode/uid/gid/symlinks, which git does not
|
||||
# store: a plain checkout would leave /etc/shadow world-readable. Not
|
||||
# `sysupgrade -r`, which is not atomic. The device comes from the argument (a
|
||||
# bare router has no device_id yet): gb_subst_device, never gb_expand.
|
||||
#
|
||||
# Exposes gb_restore; every _gb_restore_-prefixed helper below is private
|
||||
# and namespaced by module, same convention every other module here uses
|
||||
# (all of *.sh end up sourced into one process by usr/sbin/gitbackup, so a
|
||||
# bare _gb_ helper name could collide with a sibling module's). Sourced,
|
||||
# never executed: nothing here runs at load time.
|
||||
#
|
||||
# Depends on lib.sh (gb_log, gb_uci_get, gb_manifest_field, gb_manifest_each
|
||||
# -- gb_json_str is NOT used here, this module only ever reads manifest.json,
|
||||
# never writes one) and gitio.sh (gb_remote_head, gb_fetch_meta) already
|
||||
# being sourced by the caller -- restore reuses gitio.sh's own proven fetch
|
||||
# machinery (a NAMED "origin" remote, a --filter=blob:none fetch, an
|
||||
# optional depth) for both the common case and the specific-past-commit one
|
||||
# instead of re-deriving either.
|
||||
# Does NOT depend on collect.sh or device.sh: the device being restored is
|
||||
# whatever the caller passes as an argument, never gb_device_id's own
|
||||
# resolution -- a fresh/bare router calling `gitbackup restore --device D`
|
||||
# has, by construction, no working device_id config yet (bootstrap's own
|
||||
# Path 1 step 4 runs this before the restored gitbackup.main.device_id even
|
||||
# exists on disk), so gb_expand's automatic gb_device_id() call cannot be
|
||||
# reused here; _gb_restore_expand below is a deliberate, small duplicate of
|
||||
# gb_expand's substitution loop that takes the device explicitly instead.
|
||||
#
|
||||
# GB_ROOT overrides the filesystem root every write and every real-path
|
||||
# read goes through (default '', meaning the actual '/') -- the same seam
|
||||
# collect.sh's GB_ROOT already is, so a test can point this at a fixture
|
||||
# tree instead of the host filesystem and collect_fixture-shaped setups
|
||||
# apply unchanged. GB_URL is read from the environment, not taken as an
|
||||
# argument -- same convention gitio.sh/visibility.sh use for config context
|
||||
# the CLI already resolved once.
|
||||
#
|
||||
# VERIFY: Path 0 ("uploaded devices/<d>/backup.tar.gz through LuCI's own
|
||||
# stock Backup/Flash Firmware -> Restore configuration") is NOT this
|
||||
# module's own code at all -- Path 0 is the router's stock `sysupgrade -r`
|
||||
# reading a plain sysupgrade archive, gitbackup never runs in that path.
|
||||
# What was actually confirmed live on the owlab owrt2512 25.12.4 stand:
|
||||
# `sysupgrade -b` followed straight by `sysupgrade -r` on that same
|
||||
# archive genuinely restores config (a changed system.hostname reverted
|
||||
# correctly) with no error, AS LONG AS the archive excludes /etc/hosts --
|
||||
# on THIS container-based stand /etc/hosts is a Docker/OrbStack bind
|
||||
# mount, not a real file, so any tar restore that includes it hits `tar:
|
||||
# can't remove old file etc/hosts: Resource busy`. That failure is
|
||||
# confirmed to be the stand's own container artifact, not a router
|
||||
# behavior: a real router has no such bind mount over /etc/hosts. What
|
||||
# remains genuinely unverified: an actual click through LuCI's own
|
||||
# Restore configuration upload widget (no browser available here), and
|
||||
# the container limitation above itself was never worked around inside
|
||||
# gitbackup's own code -- only demonstrated by hand-filtering the tar
|
||||
# before feeding it to sysupgrade -r.
|
||||
# VERIFY: Path 0 (LuCI upload -> stock `sysupgrade -r`) is not this code and
|
||||
# was never clicked through.
|
||||
|
||||
GB_ROOT="${GB_ROOT:-}"
|
||||
|
||||
# gb_restore <device> <commit-or-empty> [--dry-run] [--force] [--yes] [--with-packages]
|
||||
#
|
||||
# <commit-or-empty> is "" or "HEAD" for the branch's current tip (the
|
||||
# common case, and the only one bootstrap's Path 1 ever uses); anything
|
||||
# else is fetched as a specific historical commit, which needs the whole
|
||||
# branch's history (not gb_fetch_meta's own --depth=1) to be reachable at
|
||||
# all -- still only THIS device's one branch, never every branch in the
|
||||
# repository, so "полного клона не появляется" holds either way.
|
||||
#
|
||||
# Returns 0 on success, 1 on a general failure (sha256 mismatch, missing
|
||||
# manifest, declined confirmation), 2 on a bad argument, 3 on network/auth,
|
||||
# 4 refused for safety (board mismatch without --force) -- the same
|
||||
# four-code contract every other module here already answers to
|
||||
# (interfaces.md, "Коды выхода"); usr/sbin/gitbackup's cmd_restore is what
|
||||
# turns this into the process's actual exit code.
|
||||
# Returns 0 ok, 1 failure, 2 bad argument, 3 network/auth, 4 refused (board
|
||||
# mismatch without --force, or an unusable manifest).
|
||||
gb_restore() {
|
||||
_gb_device="$1"
|
||||
_gb_commit="${2:-}"
|
||||
@@ -95,15 +35,10 @@ gb_restore() {
|
||||
[ -n "$_gb_device" ] || { gb_log err 'gb_restore: device is required'; return 2; }
|
||||
: "${GB_URL:?gb_restore: GB_URL is not set}"
|
||||
|
||||
_gb_branch=$(_gb_restore_expand "$(gb_uci_get gitbackup.origin.branch 'device/{device}')" "$_gb_device")
|
||||
_gb_prefix=$(_gb_restore_expand "$(gb_uci_get gitbackup.main.path_prefix 'devices/{device}')" "$_gb_device")
|
||||
_gb_branch=$(gb_subst_device "$(gb_uci_get gitbackup.origin.branch 'device/{device}')" "$_gb_device")
|
||||
_gb_prefix=$(gb_subst_device "$(gb_uci_get gitbackup.main.path_prefix 'devices/{device}')" "$_gb_device")
|
||||
|
||||
# Everything from here lives under /tmp, same rule cmd_run's own WORK
|
||||
# follows (never flash, never USB) -- trap fires when THIS PROCESS (or
|
||||
# the caller's subshell, in a test) exits, not when this function
|
||||
# returns, so every early `return` below needs no manual cleanup of
|
||||
# its own, same convention cmd_run's scattered gb_die calls already
|
||||
# rely on.
|
||||
# Under /tmp, never flash.
|
||||
_gb_work=$(mktemp -d "${TMPDIR:-/tmp}/gitbackup-restore.XXXXXX") ||
|
||||
{ gb_log err 'gb_restore: cannot create a work directory under /tmp'; return 1; }
|
||||
trap 'rm -rf "$_gb_work"' EXIT INT TERM
|
||||
@@ -120,17 +55,7 @@ gb_restore() {
|
||||
gb_fetch_meta "$_gb_branch" "$_gb_repodir" || return 3
|
||||
_gb_target="$_gb_tip"
|
||||
else
|
||||
# A specific past commit needs the branch's full history to be
|
||||
# reachable at all -- gb_fetch_meta's own default depth of 1
|
||||
# (gitio.sh, tuned for `run`'s own "just the tip" need) cannot
|
||||
# reach it. Passing it an explicit empty depth skips --depth
|
||||
# entirely instead of this module repeating gb_fetch_meta's
|
||||
# init/remote-add/fetch sequence a second time (ticket 18: this
|
||||
# WAS that second copy, byte-for-byte, before gb_fetch_meta grew
|
||||
# a depth argument). Still one branch, not the repository:
|
||||
# "минимально: своя ветка, нужный коммит" (spec). Same mapping
|
||||
# to exit 3 on any failure as the tip-fetch branch above, whether
|
||||
# gb_fetch_meta failed at init or at the fetch itself.
|
||||
# A past commit needs full branch history (still one branch).
|
||||
gb_fetch_meta "$_gb_branch" "$_gb_repodir" '' || return 3
|
||||
_gb_target="$_gb_commit"
|
||||
fi
|
||||
@@ -140,13 +65,7 @@ gb_restore() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# The one checkout call this whole module needs: a pathspec-scoped
|
||||
# `checkout <commit> -- <path>` reads only $_gb_prefix's own tree from
|
||||
# $_gb_target and lazily fetches only the blobs under it (the
|
||||
# --filter=blob:none fetch above already marked this remote a
|
||||
# promisor, same mechanism gitio.sh's own gb_fetch_meta comment
|
||||
# documents for `cat-file -p`) -- this is "sparse на devices/<D>/"
|
||||
# without needing git's separate sparse-checkout feature at all.
|
||||
# Pathspec checkout lazily fetches just these blobs (promisor remote).
|
||||
_gb_co_err=$(git -C "$_gb_repodir" checkout -q "$_gb_target" -- "$_gb_prefix" 2>&1)
|
||||
_gb_co_rc=$?
|
||||
if [ "$_gb_co_rc" -ne 0 ] || [ ! -d "$_gb_repodir/$_gb_prefix" ]; then
|
||||
@@ -161,14 +80,8 @@ gb_restore() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Before anything reads a field out of it: the manifest is remote
|
||||
# input. Everything else in this function already treats the CONTENT
|
||||
# of files/ as untrusted-until-hashed; this pass extends the same
|
||||
# suspicion to the entry metadata that decides WHERE that content
|
||||
# goes and what it ends up owned by. Refusing the whole manifest
|
||||
# rather than skipping the offending entry is deliberate -- a
|
||||
# manifest this package did not write is not a backup that was
|
||||
# partially applied, it is a backup that should not be applied.
|
||||
# The manifest comes from the remote and is untrusted: validate every
|
||||
# field before it picks a destination or becomes chmod/chown argv.
|
||||
_gb_restore_check_entries "$_gb_manifest" || {
|
||||
gb_log err "gb_restore: $_gb_prefix/manifest.json at $_gb_target has unusable entries, nothing was written:$_gb_entries_bad"
|
||||
return 4
|
||||
@@ -180,20 +93,11 @@ gb_restore() {
|
||||
|
||||
_gb_restore_check_release "$_gb_srcroot/meta/os-release.txt"
|
||||
|
||||
# The whole point of this module: not one byte reaches GB_ROOT until
|
||||
# every file's content is proven correct against the manifest.
|
||||
# Not one byte is written until every file's sha256 is proven.
|
||||
_gb_restore_verify_sha "$_gb_srcroot/files" "$_gb_manifest" || return 1
|
||||
|
||||
# Ticket 19: a second read-only pass, same "сверили всё -> потом
|
||||
# пишем" principle as the sha256 one just above, run before the
|
||||
# dry-run branch for the same reason -- it finds out WHICH paths
|
||||
# cannot possibly be replaced before a single byte is written,
|
||||
# instead of finding out only when cp fails mid-restore.
|
||||
# _gb_precheck_bad is consumed below by _gb_restore_write_one, which
|
||||
# skips a doomed cp entirely for anything in it rather than let
|
||||
# busybox's own misleading "File exists" (produced by an unlink that
|
||||
# silently failed, then a create that hits EEXIST) stand in as the
|
||||
# reported reason.
|
||||
# Unreplaceable paths are skipped and reported instead of failing in cp
|
||||
# with busybox's misleading "File exists".
|
||||
_gb_restore_check_writable "$_gb_manifest"
|
||||
|
||||
if [ "$_gb_dry" -eq 1 ]; then
|
||||
@@ -211,37 +115,11 @@ gb_restore() {
|
||||
esac
|
||||
fi
|
||||
|
||||
# Files first (content only), THEN mode/uid/gid/empty-dirs/symlinks in
|
||||
# a second pass (spec: "разложить файлы, затем применить mode/uid/gid,
|
||||
# создать пустые каталоги, пересоздать симлинки") -- content and
|
||||
# ownership are deliberately two passes, not interleaved per entry.
|
||||
_gb_restore_write_files "$_gb_srcroot/files" "$_gb_manifest"
|
||||
_gb_write_rc=$?
|
||||
|
||||
# Ticket 19: this call used to be gated behind
|
||||
# `_gb_restore_write_files ... || { ...; return 1; }`, so ONE failed
|
||||
# write (one busy bind mount, one read-only path) skipped applying
|
||||
# rights to EVERY OTHER file. Found live on the owlab owrt2512 stand:
|
||||
# a restore that failed only on /etc/hosts (a Docker/OrbStack bind
|
||||
# mount there, but on a real router this is any path that is
|
||||
# mounted, busy, read-only, or immutable) left /etc/shadow 644
|
||||
# root:root -- world-readable -- because this line was never
|
||||
# reached, and dropbear separately refused its own restored host
|
||||
# keys for the same reason.
|
||||
#
|
||||
# Decision (spec explicitly leaves this to the implementer): a
|
||||
# partially-restored router with correct permissions on every path
|
||||
# that DID land is strictly safer than either extreme -- refusing
|
||||
# the WHOLE restore over one stubborn path throws away everything
|
||||
# else that would have worked (network, wireless, dropbear keys,
|
||||
# cron, ...) over a file that is present in literally every backup
|
||||
# (/etc/hosts is always in sysupgrade -l); silently reporting success
|
||||
# is worse still. So this module always writes what it can, always
|
||||
# applies rights to whatever got written, and always tells the
|
||||
# operator the truth (below) with a non-zero exit -- unconditionally,
|
||||
# not only under --force. --force here keeps meaning exactly what it
|
||||
# already means one check up (override a board mismatch); it is not
|
||||
# involved in this decision at all.
|
||||
# Always apply perms, even after a partial write: otherwise /etc/shadow
|
||||
# could stay world-readable.
|
||||
_gb_restore_apply_perms "$_gb_manifest"
|
||||
|
||||
_gb_restore_print_scrubbed "$_gb_manifest"
|
||||
@@ -260,48 +138,8 @@ gb_restore() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# _gb_restore_expand <template> <device> -- gb_expand's own substitution
|
||||
# loop (device.sh), duplicated rather than reused: gb_expand always
|
||||
# resolves {device} through gb_device_id() itself, which is exactly what
|
||||
# this module must NOT do (see this file's header). Kept in lockstep with
|
||||
# gb_expand by inspection -- the loop body is identical on purpose.
|
||||
_gb_restore_expand() {
|
||||
_gb_tpl="$1"
|
||||
_gb_dev="$2"
|
||||
_gb_out=''
|
||||
_gb_rest="$_gb_tpl"
|
||||
while true; do
|
||||
case "$_gb_rest" in
|
||||
*'{device}'*)
|
||||
_gb_out="$_gb_out${_gb_rest%%\{device\}*}$_gb_dev"
|
||||
_gb_rest="${_gb_rest#*\{device\}}"
|
||||
;;
|
||||
*)
|
||||
_gb_out="$_gb_out$_gb_rest"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
printf '%s\n' "$_gb_out"
|
||||
}
|
||||
|
||||
# _gb_restore_check_board <old-board.json> <force> -- 0 (match, or --force
|
||||
# overrode a mismatch) or 4 (refused). Compares "model" (the field
|
||||
# device.sh's own board strategy already reads, and the one confirmed
|
||||
# `null` on generic/armsr targets -- spec: "Поле board бывает null...
|
||||
# Это не дефект") and "release.target" (the actual build target, e.g.
|
||||
# "mediatek/filogic" -- present even when model is not, so it is what
|
||||
# actually catches a generic-target-to-generic-target restore across
|
||||
# genuinely different hardware that model alone would wave through as
|
||||
# "both null, must match").
|
||||
#
|
||||
# Confirmed live on the owlab 25.12.4 armsr/armv8 stand: `ubus call system
|
||||
# board` there has no "model" key at all (this target's own real-world
|
||||
# instance of "Поле board бывает null" -- not a fixture assumption) and
|
||||
# does have "release": {"target": "armsr/armv8", ...}; the real jsonfilter
|
||||
# parses that pretty-printed, multi-line, tab-indented object correctly
|
||||
# with `-e '@.release.target'` -- confirmed against the real binary, not
|
||||
# just this suite's flat-JSON test stub.
|
||||
# "model" is absent on generic/armsr; "release.target" catches
|
||||
# generic-to-generic hardware changes.
|
||||
_gb_restore_check_board() {
|
||||
_gb_old_board="$1"
|
||||
_gb_force="$2"
|
||||
@@ -327,12 +165,8 @@ _gb_restore_check_board() {
|
||||
return 4
|
||||
}
|
||||
|
||||
# _gb_restore_check_release <old-os-release.txt> -- a major-version
|
||||
# mismatch is a warning, never a refusal (spec: "Расхождение os-release.txt
|
||||
# в мажорной версии -> предупреждение, не отказ"). Missing/unparsable data
|
||||
# on either side is silently skipped, not a warning of its own -- an old
|
||||
# backup predating this field, or a target with no /etc/os-release at all,
|
||||
# is not itself evidence of anything wrong.
|
||||
# Major-version mismatch warns only. The backup is untrusted: VERSION_ID is
|
||||
# read with sed, the file is never sourced.
|
||||
_gb_restore_check_release() {
|
||||
_gb_old_rel="$1"
|
||||
[ -r "$_gb_old_rel" ] || return 0
|
||||
@@ -349,18 +183,7 @@ _gb_restore_check_release() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Field reads and entries[] iteration used to be this module's own private
|
||||
# copies (a quoted-string extractor, a bare-numeric one, and a hand-rolled
|
||||
# entries[] walker) -- ticket 18 moved all three to lib.sh as
|
||||
# gb_manifest_field and gb_manifest_each, shared with collect.sh's
|
||||
# gb_manifest_equal and scrub.sh's manifest rewriter instead of each
|
||||
# module maintaining its own copy of the same one-object-per-line,
|
||||
# no-nested-brackets parsing.
|
||||
|
||||
# gb_restore's own hard gate: computes _gb_sha_bad (space-separated bad
|
||||
# paths) via _gb_restore_verify_sha_one, then refuses as a whole if
|
||||
# anything mismatched. _gb_srcfiles is set right before the each_entry
|
||||
# call and consumed only inside it -- a temporary, not a public seam.
|
||||
# Hard gate: any mismatch refuses the whole restore.
|
||||
_gb_restore_verify_sha() {
|
||||
_gb_srcfiles="$1"
|
||||
_gb_manifest="$2"
|
||||
@@ -385,73 +208,8 @@ _gb_restore_verify_sha_one() {
|
||||
[ -n "$_gb_got" ] && [ "$_gb_got" = "$_gb_want" ] || _gb_sha_bad="$_gb_sha_bad $_gb_path"
|
||||
}
|
||||
|
||||
# _gb_restore_check_writable <manifest.json> -- ticket 19's preflight: a
|
||||
# second read-only pass, run alongside _gb_restore_verify_sha above (same
|
||||
# "сверили всё -> потом пишем" principle applied to writability, not just
|
||||
# content). Sets _gb_precheck_bad, a space-separated path list in the same
|
||||
# shape as _gb_sha_bad above, to every "file" entry whose destination
|
||||
# already exists and is, as far as metadata alone can predict, NOT going
|
||||
# to survive cp's own unlink-then-recreate:
|
||||
#
|
||||
# - its containing directory has no write permission. Also correctly
|
||||
# answers "no" when the directory's own FILESYSTEM is mounted
|
||||
# read-only, even for root -- access(2), which `[ -w ... ]` is built
|
||||
# on, checks the mount's own read-only flag regardless of permission
|
||||
# bits. The one half of this check reproducible from an
|
||||
# unprivileged host test (chmod 555 a directory).
|
||||
# - the destination's own device differs from its containing
|
||||
# directory's (`stat -c %d` on each). True only when the exact
|
||||
# destination PATH is itself a distinct mount point -- e.g. a
|
||||
# Docker/OrbStack bind mount of a single file over /etc/hosts.
|
||||
# Confirmed live on the owlab owrt2512 stand: `stat -c%d /etc/hosts`
|
||||
# there is 41, `stat -c%d /etc` is 1048628 -- while /etc/shadow and
|
||||
# /etc/config both report 1048628, same as /etc itself, so this does
|
||||
# NOT misfire on an ordinary file. This is exactly what made cp -f
|
||||
# fail there with a misleading "File exists" (busybox/coreutils cp's
|
||||
# own unlink-before-recreate silently failed the unlink, then hit
|
||||
# EEXIST on the create) -- also confirmed live: `cp -f /tmp/x
|
||||
# /etc/hosts` -> "cp: can't create '/etc/hosts': File exists". This
|
||||
# half cannot be fabricated from an unprivileged host test (that
|
||||
# would need a real mount of its own), so it is stand-verified only,
|
||||
# same as several other OS-level facts already in this file.
|
||||
#
|
||||
# Deliberately narrow: a permission-bit problem on the FILE itself, a
|
||||
# read-only attribute, or an immutable flag are not caught here -- there
|
||||
# is no metadata-only signal for those that would not also misfire on an
|
||||
# ordinary read-only file elsewhere in the tree (mode 444 is common and
|
||||
# perfectly overwritable by root). Those still only surface at actual
|
||||
# write time, exactly as before this ticket; this preflight only removes
|
||||
# the ONE failure mode the spec explicitly asks to catch ahead of time.
|
||||
# _gb_restore_check_entries <manifest.json> -- read-only validation of
|
||||
# every entry's METADATA, before any of it is used. Sets _gb_entries_bad
|
||||
# (a multi-line " <path>: <reason>" report, empty when everything is
|
||||
# usable) and returns 0 iff it stayed empty.
|
||||
#
|
||||
# What this does NOT do is restrict WHICH paths may be restored. Writing
|
||||
# back whatever the backup set contained, wherever it lived, is the whole
|
||||
# point of this module -- there is no allowlist here and there should not
|
||||
# be one. What it checks is that each entry says what it means, exactly
|
||||
# once, in the one spelling every later pass will agree on:
|
||||
#
|
||||
# - path is absolute and canonical (gb_path_canon, lib.sh). Two passes
|
||||
# read this field independently -- _gb_restore_check_writable_one
|
||||
# builds _gb_precheck_bad from it and _gb_restore_write_one looks
|
||||
# itself up in that list by string match -- so "/etc/./hosts" and
|
||||
# "/etc/hosts" naming the same file in two entries would make the
|
||||
# preflight and the write disagree about which one it decided on.
|
||||
# - type is one this module actually handles, so an unknown one is a
|
||||
# loud refusal instead of an entry silently skipped by all three
|
||||
# `case` statements below.
|
||||
# - mode is 3 or 4 octal digits, uid/gid are digits. These go straight
|
||||
# into `chmod`/`chown` argv (_gb_restore_perm_one), where a value
|
||||
# like "--reference=/etc/shadow" or a leading "-" would be read as an
|
||||
# option rather than as the mode it claims to be.
|
||||
#
|
||||
# A manifest that fails any of this was not written by this package's own
|
||||
# collect.sh -- every field above is machine-generated there from stat(1)
|
||||
# output -- so the honest response is to refuse the restore outright
|
||||
# (gb_restore does, with exit 4), not to sanitize a hostile manifest into
|
||||
# a plausible-looking one and apply it.
|
||||
# Offenders go to _gb_entries_bad. No path allowlist: restoring the set
|
||||
# wherever it lived is the point.
|
||||
_gb_restore_check_entries() {
|
||||
_gb_manifest="$1"
|
||||
_gb_entries_bad=''
|
||||
@@ -459,7 +217,6 @@ _gb_restore_check_entries() {
|
||||
[ -z "$_gb_entries_bad" ]
|
||||
}
|
||||
|
||||
# _gb_restore_entry_reject <shown-path> <reason> -- one line onto the report.
|
||||
_gb_restore_entry_reject() {
|
||||
_gb_entries_bad="$_gb_entries_bad
|
||||
$1: $2"
|
||||
@@ -518,6 +275,8 @@ _gb_restore_check_entries_one() {
|
||||
done
|
||||
}
|
||||
|
||||
# Sets _gb_precheck_bad: unwritable directory (read-only mount too), or the
|
||||
# path is its own mount point (e.g. a bind mount over /etc/hosts).
|
||||
_gb_restore_check_writable() {
|
||||
_gb_manifest="$1"
|
||||
_gb_precheck_bad=''
|
||||
@@ -532,8 +291,6 @@ _gb_restore_check_writable_one() {
|
||||
esac
|
||||
_gb_path=$(gb_manifest_field "$_gb_obj" path)
|
||||
_gb_dest="${GB_ROOT:-}$_gb_path"
|
||||
# Nothing at $_gb_dest yet -- cp will just create it, no replace
|
||||
# involved, so there is nothing here that could go wrong this way.
|
||||
[ -e "$_gb_dest" ] || return 0
|
||||
_gb_wdir=$(dirname "$_gb_dest")
|
||||
|
||||
@@ -549,11 +306,6 @@ _gb_restore_check_writable_one() {
|
||||
fi
|
||||
}
|
||||
|
||||
# _gb_restore_print_plan <manifest.json> -- --dry-run's own output, and
|
||||
# also what the interactive confirmation prompt shows before asking. Marks
|
||||
# each path "overwrite" (something is already there at GB_ROOT) or
|
||||
# "create" (nothing is), which is what "печатает, что будет перезаписано"
|
||||
# actually needs to be useful, not just a bare path list.
|
||||
_gb_restore_print_plan() {
|
||||
_gb_manifest="$1"
|
||||
printf 'The following paths will be written:\n'
|
||||
@@ -572,23 +324,7 @@ _gb_restore_plan_one() {
|
||||
fi
|
||||
}
|
||||
|
||||
# _gb_restore_write_files <srcfiles> <manifest.json> -- pass 1: file
|
||||
# CONTENT only, no mode/uid/gid yet (_gb_restore_apply_perms is pass 2).
|
||||
# Only reached after _gb_restore_verify_sha has already confirmed every
|
||||
# file's hash, so a failure here is an I/O problem (disk full, a path
|
||||
# collision with an existing directory, a mount that refuses to give up
|
||||
# its own path), not a data-integrity one.
|
||||
#
|
||||
# Sets _gb_write_bad, a multi-line " <path>: <reason>" report (empty
|
||||
# when nothing failed), and returns 0 iff it stayed empty. The caller
|
||||
# (gb_restore) deliberately does NOT gate _gb_restore_apply_perms on this
|
||||
# return value (ticket 19) -- it only reads _gb_write_rc/_gb_write_bad
|
||||
# afterward, to tell the operator the truth and end the call non-zero,
|
||||
# once rights have already been applied to whatever DID get written.
|
||||
# gb_manifest_each itself never stops iterating on a callback's own
|
||||
# return code (lib.sh), so one failing entry here already does not
|
||||
# prevent the REST of this same pass from running either -- only the
|
||||
# call site one level up used to throw that away.
|
||||
# Pass 1, content only; failures collect in _gb_write_bad.
|
||||
_gb_restore_write_files() {
|
||||
_gb_srcfiles="$1"
|
||||
_gb_manifest="$2"
|
||||
@@ -605,9 +341,6 @@ _gb_restore_write_one() {
|
||||
esac
|
||||
_gb_path=$(gb_manifest_field "$_gb_obj" path)
|
||||
|
||||
# Already known doomed by _gb_restore_check_writable above -- skip
|
||||
# the attempt outright rather than let cp run into it and report its
|
||||
# own less useful message (see that function's own comment for why).
|
||||
case " $_gb_precheck_bad " in
|
||||
*" $_gb_path "*)
|
||||
_gb_write_bad="$_gb_write_bad
|
||||
@@ -622,23 +355,10 @@ _gb_restore_write_one() {
|
||||
$_gb_path: $_gb_mkdir_err"
|
||||
return 0
|
||||
}
|
||||
# A symlink sitting where a "file" entry wants to land is removed
|
||||
# first, never written through. Two reasons, and the second is the
|
||||
# one that matters: a "file" entry must end up an actual file, which
|
||||
# is what the symlink branch of _gb_restore_perm_one already assumes
|
||||
# in the other direction (it rm -f's whatever is there before its own
|
||||
# ln -s); and `cp -f` disagrees with itself about this across the two
|
||||
# implementations this package runs on -- GNU cp opens the
|
||||
# destination O_TRUNC, which FOLLOWS the link and rewrites whatever
|
||||
# it points at, somewhere the manifest never named, while busybox cp
|
||||
# unlinks first (the -f comment below). Deciding it here makes both
|
||||
# behave the same, and makes the set of files a restore can touch
|
||||
# exactly the set of paths its manifest lists.
|
||||
# Never write through a symlink: cp -f follows it to a path the manifest
|
||||
# never named.
|
||||
[ -L "$_gb_dest" ] && rm -f "$_gb_dest" 2>/dev/null
|
||||
# -f: busybox cp (unlike GNU/BSD cp) refuses an existing destination
|
||||
# outright ("File exists") without it -- found live on the owlab
|
||||
# stand restoring over /etc/hosts, not caught by any host-side test
|
||||
# since macOS/GNU cp both overwrite by default with no flag at all.
|
||||
# -f: busybox cp refuses an existing destination without it.
|
||||
_gb_cp_err=$(cp -f "$_gb_srcfiles$_gb_path" "$_gb_dest" 2>&1) || {
|
||||
_gb_write_bad="$_gb_write_bad
|
||||
$_gb_path: $_gb_cp_err"
|
||||
@@ -646,11 +366,7 @@ _gb_restore_write_one() {
|
||||
}
|
||||
}
|
||||
|
||||
# _gb_restore_apply_perms <manifest.json> -- pass 2: mode/uid/gid for
|
||||
# files and directories, empty directories created outright (they have no
|
||||
# content of their own to have been written in pass 1), symlinks created
|
||||
# from scratch (their "content" IS the target string, never copied as a
|
||||
# file -- collect.sh's own R24.1 comment on the same asymmetry).
|
||||
# Pass 2: mode/uid/gid, empty dirs, symlinks.
|
||||
_gb_restore_apply_perms() {
|
||||
_gb_manifest="$1"
|
||||
gb_manifest_each "$_gb_manifest" entries _gb_restore_perm_one
|
||||
@@ -672,36 +388,13 @@ _gb_restore_perm_one() {
|
||||
[ -n "$_gb_uid" ] && [ -n "$_gb_gid" ] && chown "$_gb_uid:$_gb_gid" "$_gb_dest" 2>/dev/null
|
||||
;;
|
||||
symlink)
|
||||
# _gb_symtarget, NOT _gb_target: found live on the owlab stand --
|
||||
# gb_restore's own outer scope holds the commit sha being
|
||||
# restored in a variable of that exact name (no `local` anywhere
|
||||
# in this codebase, interfaces.md's own convention), and this
|
||||
# helper runs from inside gb_restore's own call chain
|
||||
# (_gb_restore_apply_perms -> gb_manifest_each -> here) --
|
||||
# reusing _gb_target here clobbered it, so the FINAL "restored
|
||||
# <device> from <commit>" message printed the last symlink's
|
||||
# target string instead of the commit sha whenever the backup
|
||||
# set contained even one symlink. The restore itself was
|
||||
# unaffected (this ran after every actual write), only the
|
||||
# closing report was wrong.
|
||||
# Not _gb_target: gb_restore keeps the commit sha there (no locals).
|
||||
_gb_symtarget=$(gb_manifest_field "$_gb_obj" target)
|
||||
mkdir -p "$(dirname "$_gb_dest")" 2>/dev/null
|
||||
rm -f "$_gb_dest" 2>/dev/null
|
||||
# `--`: the target string is the one manifest field with no
|
||||
# shape of its own to validate (any string is a legal symlink
|
||||
# target), so it is the one that can still arrive looking like
|
||||
# an option -- "-s", "--force". End-of-options settles it
|
||||
# without constraining what a symlink is allowed to point at.
|
||||
ln -s -- "$_gb_symtarget" "$_gb_dest" 2>/dev/null
|
||||
# -h: change the symlink's own ownership, not the (possibly
|
||||
# dangling, e.g. /etc/resolv.conf) target's -- busybox chown
|
||||
# documents -h for exactly this. Mode is never set on a
|
||||
# symlink: Linux always reports lstat mode 0777 for one
|
||||
# regardless of chmod, so a freshly `ln -s`'d link already
|
||||
# matches whatever collect.sh recorded (it read the same
|
||||
# lstat mode the same way). Confirmed live on the owlab
|
||||
# 25.12.4 stand: chown -h on this busybox actually changes
|
||||
# the symlink's own uid/gid, not a dangling target's.
|
||||
# -h: chown the link itself, not a possibly dangling target. No
|
||||
# chmod: Linux symlinks are always 0777.
|
||||
[ -n "$_gb_uid" ] && [ -n "$_gb_gid" ] && chown -h "$_gb_uid:$_gb_gid" "$_gb_dest" 2>/dev/null
|
||||
;;
|
||||
file)
|
||||
@@ -711,17 +404,7 @@ _gb_restore_perm_one() {
|
||||
esac
|
||||
}
|
||||
|
||||
# _gb_restore_print_scrubbed <manifest.json> -- prints scrubbed[]'s option
|
||||
# paths to stderr (informational for the operator, same stream
|
||||
# gb_accept_hostkey's own prompts use), one per line, with a header only
|
||||
# once and only if there is at least one. Silent when scrubbed[] is empty
|
||||
# -- a private backup restoring cleanly should say nothing extra.
|
||||
#
|
||||
# gb_manifest_each (lib.sh) walks "scrubbed" the same way
|
||||
# _gb_restore_apply_perms's own each-entry call above walks "entries" --
|
||||
# this used to be its own hand-rolled entries/scrubbed-array walker,
|
||||
# parallel to (and slightly different from) the one gb_restore's other
|
||||
# helpers shared, before ticket 18 moved both to lib.sh.
|
||||
# Redacted options on stderr, so the operator re-enters them.
|
||||
_gb_restore_print_scrubbed() {
|
||||
_gb_manifest="$1"
|
||||
_gb_scrub_any=0
|
||||
@@ -740,15 +423,8 @@ _gb_restore_scrubbed_one() {
|
||||
printf ' %s\n' "$_gb_opt" >&2
|
||||
}
|
||||
|
||||
# _gb_restore_packages <meta-dir> -- --with-packages, best-effort (spec:
|
||||
# "apk add по installed_packages.txt best-effort, в конце напечатать
|
||||
# список неустановившегося"). Package names come from the `{origin}`
|
||||
# group `apk list --installed` itself prints (meta/installed_packages.txt
|
||||
# is that command's own full-line output, per collect.sh's own comment) --
|
||||
# not from splitting the leading "name-version" token, which is exactly
|
||||
# the fragile parse the spec warns against (a version string can itself
|
||||
# contain a dash where the name also could). Restores repositories.txt
|
||||
# first so a custom feed's own packages have somewhere to come from.
|
||||
# Best effort. Names come from the {origin} group of `apk list --installed`:
|
||||
# splitting "name-version" is ambiguous.
|
||||
_gb_restore_packages() {
|
||||
_gb_meta="$1"
|
||||
_gb_repos="$_gb_meta/repositories.txt"
|
||||
|
||||
@@ -1,36 +1,11 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- schedule (spec "Расписание", R100/R101/R103-R107).
|
||||
#
|
||||
# Exposes gb_cron_valid, gb_preset_expr, gb_cron_apply and gb_cron_next.
|
||||
# Every _gb_-prefixed helper is private, same convention as lib.sh. Sourced,
|
||||
# never executed: nothing here runs at load time, so tests/run.sh can source
|
||||
# it and call one function without a router underneath it. Assumes lib.sh
|
||||
# (gb_uci_get, gb_log) and device.sh (gb_device_id) are already sourced,
|
||||
# same as every other module in this directory.
|
||||
# gitbackup -- schedule: gb_cron_valid, gb_preset_expr, gb_cron_apply. Needs
|
||||
# lib.sh and device.sh.
|
||||
|
||||
# gb_preset_expr <preset> <device> -- hourly/daily/weekly, spec's table.
|
||||
#
|
||||
# The minute (and, for daily/weekly, the hour) is not fixed: it is folded
|
||||
# out of a hash of <device> so that a fleet of routers all set to the same
|
||||
# preset do not wake up in the same second and hit GitHub's API together
|
||||
# (spec: "двадцать роутеров с daily... получат rate limit все разом").
|
||||
# Deterministic on purpose, not `$RANDOM` -- re-saving the settings form
|
||||
# must not reshuffle a router's own schedule.
|
||||
#
|
||||
# H is folded into 0-5 (spec's "ночное окно 0:00-5:59"), not the full
|
||||
# 0-23, so a daily/weekly backup never lands in the middle of the day.
|
||||
#
|
||||
# The hash is sha256sum, not a literal "crc" tool: the base image ships no
|
||||
# cksum(1) at all (`busybox cksum` -> "applet not found", measured on the
|
||||
# 25.12.4 stand), while sha256sum is already there and already a DEPENDS
|
||||
# collect.sh relies on. Any digest with a roughly uniform byte distribution
|
||||
# is equally deterministic for this purpose. 8 hex characters -> 32 bits,
|
||||
# read as decimal through the explicit "0x" arithmetic prefix -- ash's
|
||||
# arithmetic does not support the ksh/bash "16#..." base-literal syntax
|
||||
# (verified on the stand: `$((16#$h))` is an "arithmetic syntax error"),
|
||||
# but the POSIX "0x..." form works on both ash and the bash/zsh /bin/sh this
|
||||
# suite also runs under during development.
|
||||
# gb_preset_expr <preset> <device> -- time derived from a hash of <device>:
|
||||
# a fleet does not hit the remote at once, re-saving does not reshuffle.
|
||||
# sha256sum: busybox has no cksum; "0x": ash has no 16#N arithmetic.
|
||||
gb_preset_expr() {
|
||||
_gb_preset="$1"
|
||||
_gb_dev="$2"
|
||||
@@ -50,92 +25,20 @@ gb_preset_expr() {
|
||||
esac
|
||||
}
|
||||
|
||||
# _gb_strip_lead0 <digits> -- private. Prints <digits> with every leading
|
||||
# zero removed, down to a single "0" for an all-zero input ("007" -> "7",
|
||||
# "00" -> "0", "46" -> "46").
|
||||
#
|
||||
# Exists only so the rest of this file can safely hand a cron field's
|
||||
# leading-zero-tolerant value (e.g. the hour "08") to `$(( ))`: busybox
|
||||
# ash's arithmetic reads a leading zero as an octal prefix ("08" is then an
|
||||
# invalid octal digit and the whole expression errors out), and -- verified
|
||||
# on the stand, where it produced the exact same "arithmetic syntax
|
||||
# error" -- does not implement POSIX's "10#N" decimal-radix prefix as a
|
||||
# workaround either, unlike the bash/zsh /bin/sh this suite also runs
|
||||
# under during development. A plain glob-based strip has no radix opinion
|
||||
# at all.
|
||||
_gb_strip_lead0() {
|
||||
_gb_s0="$1"
|
||||
while true; do
|
||||
case "$_gb_s0" in
|
||||
0?*) _gb_s0="${_gb_s0#0}" ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
printf '%s' "$_gb_s0"
|
||||
}
|
||||
|
||||
# _gb_field_values <field-expr> <min> <max> -- private.
|
||||
#
|
||||
# Expands one cron field into its sorted, deduplicated, space-separated list
|
||||
# of matching integers, or prints nothing and returns 1 if the field is not
|
||||
# a comma-separated list of "*", "*/N", "A", "A-B" or "A-B/N" terms, or any
|
||||
# number in it falls outside [<min>,<max>]. This is the one engine behind
|
||||
# both gb_cron_valid (a field is valid exactly when this expands it without
|
||||
# error) and gb_cron_next (which needs the actual expansion to search for
|
||||
# the next match).
|
||||
#
|
||||
# Bounds checking always goes through `[ -ge ]`/`[ -le ]` (POSIX `test`),
|
||||
# never `$(( ))`: a leading-zero field value like the hour "08" is a normal
|
||||
# decimal 8 to `test`, but `$(( 08 ))` is an invalid octal literal and
|
||||
# busybox ash's arithmetic errors out on it outright (verified on the
|
||||
# stand: `v=08; echo $((v))` exits nonzero). Arithmetic is used only once a
|
||||
# value is already known to be a plain decimal string, and only after
|
||||
# _gb_strip_lead0 below has removed any leading zero -- ash's arithmetic
|
||||
# does not support the POSIX "10#N" decimal-radix prefix either (also
|
||||
# verified on the stand: `$((10#08))` is the same "arithmetic syntax
|
||||
# error", not the workaround it looks like on a bash/zsh /bin/sh).
|
||||
#
|
||||
# "*/N" starts from <min>, matching busybox for every field this package's
|
||||
# presets or a hand-written cron_expr actually vary (minute, hour, month,
|
||||
# weekday all have min 0 or 1 in a way that lines up with busybox's own
|
||||
# internal 0-based bitmap). The one field where this is a deliberate,
|
||||
# disclosed simplification is day-of-month: busybox's own bitmap for that
|
||||
# field reserves index 0 for a "day zero" that can never occur and starts
|
||||
# stepping from THAT, so its real "*/10" fires on 10/20/30 (index 0 is
|
||||
# inert), not the 1/11/21/31 this function returns starting from min=1 --
|
||||
# measured on the stand by running `busybox crond -f -d0` against fixture
|
||||
# crontabs and reading its own field-parse debug dump. Nothing in this
|
||||
# package's presets or acceptance criteria exercises a stepped day-of-month
|
||||
# field; gb_cron_next's answer for a hand-written one could come out one
|
||||
# step earlier than the real crond fires. Documented rather than silently
|
||||
# assumed away.
|
||||
_gb_field_values() {
|
||||
# _gb_field_valid <field-expr> <min> <max> -- bounds use `test`, never $(( )):
|
||||
# a leading zero would read as octal there.
|
||||
_gb_field_valid() {
|
||||
_gb_fv_expr="$1"
|
||||
_gb_fv_min="$2"
|
||||
_gb_fv_max="$3"
|
||||
_gb_fv_out=''
|
||||
|
||||
# A leading/trailing/doubled comma has to be rejected before the split
|
||||
# below, not after it: shell field-splitting on IFS="," drops a
|
||||
# *trailing* delimiter instead of yielding a trailing empty field (the
|
||||
# same expansion that turns "a," into the one field "a", not "a" and
|
||||
# ""), so "5," would otherwise sail through as the single term "5" --
|
||||
# measured by a fixture in tests/fixtures/cron.tsv that stayed green
|
||||
# for the wrong reason until this guard was added. A *leading* comma
|
||||
# does still produce a real empty first field and would be caught
|
||||
# below regardless, but checking both shapes here up front is one rule
|
||||
# instead of two.
|
||||
# IFS splitting silently drops a trailing empty field: "5," would pass.
|
||||
case "$_gb_fv_expr" in
|
||||
,*|*,|*,,*) return 1 ;;
|
||||
esac
|
||||
|
||||
_gb_fv_saved_ifs="$IFS"
|
||||
IFS=','
|
||||
# -f (noglob) around the split is not optional: an unquoted "*" or
|
||||
# "*/5" term is exactly the input this field parses, and without it the
|
||||
# shell pathname-expands "*" against the current directory instead of
|
||||
# leaving it as a literal word -- caught by a test run from the repo
|
||||
# root, where "*" silently became a couple dozen filenames.
|
||||
set -f
|
||||
# shellcheck disable=SC2086 # splitting on IFS=',' is the point
|
||||
set -- $_gb_fv_expr
|
||||
@@ -144,35 +47,22 @@ _gb_field_values() {
|
||||
[ "$#" -gt 0 ] || return 1
|
||||
|
||||
for _gb_fv_term in "$@"; do
|
||||
# An empty term is a bare/leading/trailing/doubled comma ("," ",,"),
|
||||
# never a valid field on its own.
|
||||
[ -n "$_gb_fv_term" ] || return 1
|
||||
|
||||
_gb_fv_step=1
|
||||
_gb_fv_range="$_gb_fv_term"
|
||||
case "$_gb_fv_term" in
|
||||
*/*)
|
||||
_gb_fv_range="${_gb_fv_term%%/*}"
|
||||
_gb_fv_step="${_gb_fv_term#*/}"
|
||||
case "$_gb_fv_step" in ''|*[!0-9]*) return 1 ;; esac
|
||||
# A step of 0, or one bigger than the field's own span,
|
||||
# parses without complaint in real busybox crond but
|
||||
# silently degenerates to firing on the field's single
|
||||
# lowest value forever -- measured on the stand
|
||||
# (`*/0`/`*/61` in a minute field both produce a bitmap
|
||||
# with only minute 0 set, no parse error at all). That is
|
||||
# exactly the kind of "looks configured, never really
|
||||
# runs" trap R101's live validation exists to catch
|
||||
# before it reaches a crontab.
|
||||
# Step 0 or beyond the span parses in busybox crond but fires
|
||||
# only on the lowest value.
|
||||
[ "$_gb_fv_step" -ge 1 ] && [ "$_gb_fv_step" -le "$_gb_fv_max" ] || return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "$_gb_fv_range" in
|
||||
'*')
|
||||
_gb_fv_lo="$_gb_fv_min"
|
||||
_gb_fv_hi="$_gb_fv_max"
|
||||
;;
|
||||
'*') ;;
|
||||
*-*)
|
||||
_gb_fv_lo="${_gb_fv_range%%-*}"
|
||||
_gb_fv_hi="${_gb_fv_range#*-}"
|
||||
@@ -180,52 +70,19 @@ _gb_field_values() {
|
||||
case "$_gb_fv_hi" in ''|*[!0-9]*) return 1 ;; esac
|
||||
[ "$_gb_fv_lo" -ge "$_gb_fv_min" ] && [ "$_gb_fv_lo" -le "$_gb_fv_max" ] || return 1
|
||||
[ "$_gb_fv_hi" -ge "$_gb_fv_min" ] && [ "$_gb_fv_hi" -le "$_gb_fv_max" ] || return 1
|
||||
# A reversed range ("5-2") parses in real busybox crond
|
||||
# too, but produces a bitmap that is wrong in a way no
|
||||
# user intended (measured on the stand) rather than an
|
||||
# error -- rejected here instead of reproduced.
|
||||
# Reversed range parses in busybox but yields a wrong bitmap.
|
||||
[ "$_gb_fv_lo" -le "$_gb_fv_hi" ] || return 1
|
||||
;;
|
||||
*)
|
||||
case "$_gb_fv_range" in ''|*[!0-9]*) return 1 ;; esac
|
||||
[ "$_gb_fv_range" -ge "$_gb_fv_min" ] && [ "$_gb_fv_range" -le "$_gb_fv_max" ] || return 1
|
||||
_gb_fv_lo="$_gb_fv_range"
|
||||
_gb_fv_hi="$_gb_fv_range"
|
||||
;;
|
||||
esac
|
||||
|
||||
# A plain "A/N" (no "-" range) is real, accepted busybox syntax --
|
||||
# measured on the stand -- but the step is a no-op: it fires on A
|
||||
# alone, never repeating. The loop below already reproduces that
|
||||
# with no special case, because it starts and ends at the same
|
||||
# value: one iteration, done.
|
||||
_gb_fv_v=$(_gb_strip_lead0 "$_gb_fv_lo")
|
||||
_gb_fv_v=$(( _gb_fv_v ))
|
||||
_gb_fv_hi_n=$(_gb_strip_lead0 "$_gb_fv_hi")
|
||||
_gb_fv_hi_n=$(( _gb_fv_hi_n ))
|
||||
_gb_fv_step_n=$(_gb_strip_lead0 "$_gb_fv_step")
|
||||
_gb_fv_step_n=$(( _gb_fv_step_n ))
|
||||
while [ "$_gb_fv_v" -le "$_gb_fv_hi_n" ]; do
|
||||
_gb_fv_out="$_gb_fv_out $_gb_fv_v"
|
||||
_gb_fv_v=$((_gb_fv_v + _gb_fv_step_n))
|
||||
done
|
||||
done
|
||||
|
||||
printf '%s' "$_gb_fv_out" | tr -s ' ' '\n' | sed '/^$/d' | sort -n -u | tr '\n' ' '
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
# gb_cron_valid <expr> -- 0 if busybox crond on 25.12 can actually run
|
||||
# <expr>, 1 otherwise, with a reason on stderr.
|
||||
#
|
||||
# Rejects the "@daily"/"@hourly"/"@reboot" family outright and by name:
|
||||
# `CONFIG_FEATURE_CROND_SPECIAL_TIMES=n` on the 25.12.4 build means the
|
||||
# binary has no macro table at all, so "@daily aa bb cc dd ee ff" gives a
|
||||
# loud "parse error at @daily" -- but a *short* macro line like the classic
|
||||
# "@daily /bin/echo A" (fewer than six whitespace-separated tokens) is
|
||||
# dropped by crond with no error and no run, ever (both measured on the
|
||||
# stand). A validator that let @-macros through on the strength of the
|
||||
# first, louder failure mode would still ship the second, silent one.
|
||||
# gb_cron_valid <expr> -- 1 with a reason on stderr. busybox crond has no
|
||||
# @-macros: "@daily ..." errors or, under six tokens, is dropped silently.
|
||||
gb_cron_valid() {
|
||||
_gb_cv_expr="${1-}"
|
||||
case "$_gb_cv_expr" in
|
||||
@@ -236,8 +93,6 @@ gb_cron_valid() {
|
||||
;;
|
||||
esac
|
||||
|
||||
# -f (noglob): see _gb_field_values -- a field is often a literal "*"
|
||||
# and must not be pathname-expanded by this split.
|
||||
set -f
|
||||
# shellcheck disable=SC2086 # splitting on whitespace is the point
|
||||
set -- $_gb_cv_expr
|
||||
@@ -248,19 +103,13 @@ gb_cron_valid() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Field bounds: minute 0-59, hour 0-23, day-of-month 1-31, month 1-12,
|
||||
# day-of-week 0-6 -- the last confirmed on the stand by the fact that
|
||||
# "0 3 * * 7" is itself a parse error (no Sunday-as-7 alias on this
|
||||
# busybox), while 0 and 6 both parse. Five explicit checks rather than a
|
||||
# table-driven loop: POSIX sh has no arrays, and indexing positional
|
||||
# parameters by an arithmetic offset would need `eval` for no real gain
|
||||
# over just naming each field once.
|
||||
# busybox has no Sunday-as-7 alias.
|
||||
_gb_cv_bad=''
|
||||
_gb_field_values "$1" 0 59 >/dev/null || _gb_cv_bad='minute'
|
||||
[ -n "$_gb_cv_bad" ] || { _gb_field_values "$2" 0 23 >/dev/null || _gb_cv_bad='hour'; }
|
||||
[ -n "$_gb_cv_bad" ] || { _gb_field_values "$3" 1 31 >/dev/null || _gb_cv_bad='day-of-month'; }
|
||||
[ -n "$_gb_cv_bad" ] || { _gb_field_values "$4" 1 12 >/dev/null || _gb_cv_bad='month'; }
|
||||
[ -n "$_gb_cv_bad" ] || { _gb_field_values "$5" 0 6 >/dev/null || _gb_cv_bad='day-of-week'; }
|
||||
_gb_field_valid "$1" 0 59 || _gb_cv_bad='minute'
|
||||
[ -n "$_gb_cv_bad" ] || { _gb_field_valid "$2" 0 23 || _gb_cv_bad='hour'; }
|
||||
[ -n "$_gb_cv_bad" ] || { _gb_field_valid "$3" 1 31 || _gb_cv_bad='day-of-month'; }
|
||||
[ -n "$_gb_cv_bad" ] || { _gb_field_valid "$4" 1 12 || _gb_cv_bad='month'; }
|
||||
[ -n "$_gb_cv_bad" ] || { _gb_field_valid "$5" 0 6 || _gb_cv_bad='day-of-week'; }
|
||||
if [ -n "$_gb_cv_bad" ]; then
|
||||
printf '%s field is not a valid value, range, step or list of them in "%s"\n' \
|
||||
"$_gb_cv_bad" "$_gb_cv_expr" >&2
|
||||
@@ -269,149 +118,16 @@ gb_cron_valid() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# gb_cron_next <expr> -- the next UTC "YYYY-MM-DD HH:MM" <expr> fires at or
|
||||
# after now, or nothing (exit 1) if <expr> is invalid.
|
||||
#
|
||||
# Searches whole days forward from today (up to ~4 years, comfortably past
|
||||
# any Feb-29-only expression), because every field this function has to
|
||||
# reason about -- month, day-of-month, day-of-week -- only ever changes at a
|
||||
# day boundary; only the final hour/minute search happens inside the
|
||||
# matching day. All arithmetic is on Unix time in UTC, which never has a day
|
||||
# shorter or longer than 86400 seconds (no DST to account for), so a whole
|
||||
# day is added by adding exactly 86400 -- no calendar library, no date
|
||||
# string round-trip, just `date -u -d @<epoch>` to read back the
|
||||
# month/day-of-month/weekday for a given instant (the "@seconds_since_1970"
|
||||
# form busybox `date --help` documents as recognized on the stand).
|
||||
gb_cron_next() {
|
||||
_gb_cn_expr="$1"
|
||||
gb_cron_valid "$_gb_cn_expr" >/dev/null 2>&1 || return 1
|
||||
|
||||
# -f (noglob): see _gb_field_values -- a field is often a literal "*"
|
||||
# and must not be pathname-expanded by this split.
|
||||
set -f
|
||||
# shellcheck disable=SC2086
|
||||
set -- $_gb_cn_expr
|
||||
set +f
|
||||
_gb_cn_minutes=$(_gb_field_values "$1" 0 59) || return 1
|
||||
_gb_cn_hours=$(_gb_field_values "$2" 0 23) || return 1
|
||||
_gb_cn_doms=$(_gb_field_values "$3" 1 31) || return 1
|
||||
_gb_cn_months=$(_gb_field_values "$4" 1 12) || return 1
|
||||
_gb_cn_dows=$(_gb_field_values "$5" 0 6) || return 1
|
||||
|
||||
# Standard cron day-selection rule: when BOTH day-of-month and
|
||||
# day-of-week are restricted (neither is "*"), a day qualifies if it
|
||||
# matches EITHER one (OR); when only one is restricted, that one alone
|
||||
# decides.
|
||||
_gb_cn_dom_star=0
|
||||
case "$3" in '*') _gb_cn_dom_star=1 ;; esac
|
||||
_gb_cn_dow_star=0
|
||||
case "$5" in '*') _gb_cn_dow_star=1 ;; esac
|
||||
|
||||
_gb_cn_now=$(date -u +%s) || return 1
|
||||
_gb_cn_midnight=$(( _gb_cn_now - (_gb_cn_now % 86400) ))
|
||||
|
||||
_gb_cn_day=0
|
||||
while [ "$_gb_cn_day" -le 1466 ]; do
|
||||
_gb_cn_day_epoch=$(( _gb_cn_midnight + _gb_cn_day * 86400 ))
|
||||
_gb_cn_ymw=$(date -u -d "@$_gb_cn_day_epoch" '+%m %d %w') || return 1
|
||||
_gb_cn_mo=${_gb_cn_ymw%% *}
|
||||
_gb_cn_rest=${_gb_cn_ymw#* }
|
||||
_gb_cn_dm=${_gb_cn_rest%% *}
|
||||
_gb_cn_wd=${_gb_cn_rest#* }
|
||||
# Same _gb_strip_lead0 as _gb_field_values above, needed here
|
||||
# because `date`'s own %m/%d output is zero-padded.
|
||||
_gb_cn_mo=$(_gb_strip_lead0 "$_gb_cn_mo")
|
||||
_gb_cn_mo=$(( _gb_cn_mo ))
|
||||
_gb_cn_dm=$(_gb_strip_lead0 "$_gb_cn_dm")
|
||||
_gb_cn_dm=$(( _gb_cn_dm ))
|
||||
_gb_cn_wd=$(_gb_strip_lead0 "$_gb_cn_wd")
|
||||
_gb_cn_wd=$(( _gb_cn_wd ))
|
||||
|
||||
case " $_gb_cn_months " in
|
||||
*" $_gb_cn_mo "*) _gb_cn_month_ok=1 ;;
|
||||
*) _gb_cn_month_ok=0 ;;
|
||||
esac
|
||||
|
||||
if [ "$_gb_cn_month_ok" -eq 1 ]; then
|
||||
_gb_cn_dom_ok=0
|
||||
case " $_gb_cn_doms " in *" $_gb_cn_dm "*) _gb_cn_dom_ok=1 ;; esac
|
||||
_gb_cn_dow_ok=0
|
||||
case " $_gb_cn_dows " in *" $_gb_cn_wd "*) _gb_cn_dow_ok=1 ;; esac
|
||||
|
||||
if [ "$_gb_cn_dom_star" -eq 1 ] && [ "$_gb_cn_dow_star" -eq 1 ]; then
|
||||
_gb_cn_day_ok=1
|
||||
elif [ "$_gb_cn_dom_star" -eq 1 ]; then
|
||||
_gb_cn_day_ok="$_gb_cn_dow_ok"
|
||||
elif [ "$_gb_cn_dow_star" -eq 1 ]; then
|
||||
_gb_cn_day_ok="$_gb_cn_dom_ok"
|
||||
elif [ "$_gb_cn_dom_ok" -eq 1 ] || [ "$_gb_cn_dow_ok" -eq 1 ]; then
|
||||
_gb_cn_day_ok=1
|
||||
else
|
||||
_gb_cn_day_ok=0
|
||||
fi
|
||||
|
||||
if [ "$_gb_cn_day_ok" -eq 1 ]; then
|
||||
for _gb_cn_h in $_gb_cn_hours; do
|
||||
for _gb_cn_m in $_gb_cn_minutes; do
|
||||
_gb_cn_cand=$(( _gb_cn_day_epoch + _gb_cn_h * 3600 + _gb_cn_m * 60 ))
|
||||
if [ "$_gb_cn_cand" -ge "$_gb_cn_now" ]; then
|
||||
date -u -d "@$_gb_cn_cand" '+%Y-%m-%d %H:%M'
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
done
|
||||
fi
|
||||
fi
|
||||
|
||||
_gb_cn_day=$((_gb_cn_day + 1))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# gb_cron_apply -- reads gitbackup.main.schedule (and cron_expr/device_id as
|
||||
# needed), (re)writes the single "# gitbackup"-marked line in the crontab,
|
||||
# and makes it live.
|
||||
#
|
||||
# Idempotent by construction: every existing line ending in "# gitbackup" is
|
||||
# dropped before the (at most one) new one is appended, so applying twice in
|
||||
# a row -- two saves of the settings form, a reboot re-running this from
|
||||
# start_service, whatever -- leaves exactly one line, never a growing pile,
|
||||
# and never touches a line that is not ours (spec: OpenWrt's own crontab
|
||||
# ships with unrelated vnstat/adblock entries out of the box; confirmed
|
||||
# present, untouched by this, on the owlab stand).
|
||||
#
|
||||
# schedule=off removes that line but leaves the file itself in place, even
|
||||
# at zero length: `/etc/init.d/cron`'s start_service begins with
|
||||
# `[ -z "$(ls /etc/crontabs/)" ] && return 1` -- crond refuses to start only
|
||||
# when the *directory* is empty, and a zero-byte file already satisfies
|
||||
# that (spec "Проверенные факты", confirmed unchanged on this stand).
|
||||
# Deleting the file on schedule=off would silently take crond down with it
|
||||
# for every *other* cron job on the router, which is not this package's
|
||||
# call to make.
|
||||
#
|
||||
# GB_CRONTAB overrides the crontab path so tests/run.sh can point this at a
|
||||
# plain temp file; the router never sets it. The final
|
||||
# `/etc/init.d/cron enable && /etc/init.d/cron restart` is guarded by
|
||||
# `[ -x /etc/init.d/cron ]` for the same reason -- there is no such script
|
||||
# on a development host -- and is the one piece of this function verified
|
||||
# only on the owlab stand (via `ps`), never in tests/run.sh, per this
|
||||
# ticket's own acceptance criteria.
|
||||
# gb_cron_apply -- rewrites only the "# gitbackup"-marked line. schedule=off
|
||||
# keeps the crontab file even if empty: cron's init script refuses to start
|
||||
# on an empty crontabs directory, which would kill every other job.
|
||||
gb_cron_apply() {
|
||||
_gb_ca_crontab="${GB_CRONTAB:-/etc/crontabs/root}"
|
||||
_gb_ca_sched=$(gb_uci_get gitbackup.main.schedule daily)
|
||||
_gb_ca_expr=''
|
||||
|
||||
# Deliberately not named with a "gitbackup" + "." substring next to
|
||||
# each other: t_config_sections_match_code's own grep reads any
|
||||
# "gitbackup.<word>." it finds anywhere in this directory as a UCI
|
||||
# section reference, and a suffix like that would fail it outright
|
||||
# for naming a temp file, not a config path (hit during development).
|
||||
#
|
||||
# A stray one of these from some earlier crashed run (this file's own
|
||||
# development left several on the owlab stand before a bug in
|
||||
# _gb_field_values was fixed) is swept up here rather than left to
|
||||
# accumulate one file per bad run forever -- the crontab directory has
|
||||
# no other owner watching it for that.
|
||||
# No "gitbackup." substring in the temp name: a test grep would read it as
|
||||
# a UCI path. Sweeps leftovers from crashed runs.
|
||||
rm -f "${_gb_ca_crontab}".newtmp.* 2>/dev/null
|
||||
_gb_ca_tmp="${_gb_ca_crontab}.newtmp.$$"
|
||||
if [ -f "$_gb_ca_crontab" ]; then
|
||||
|
||||
@@ -1,100 +1,57 @@
|
||||
# gitbackup scrub list -- UCI options whose value is a secret, redacted by
|
||||
# scrub.sh when visibility=public. One pattern per line, "#" starts a
|
||||
# trailing comment that MUST name where the option was confirmed to exist --
|
||||
# a line without one is not something this ticket allows to exist (spec
|
||||
# "Scrub": "собирается обходом реальных источников, не из головы").
|
||||
# gitbackup scrub list: UCI options whose value is a secret, redacted by
|
||||
# scrub.sh when scrubbing. Every entry must cite where the option was
|
||||
# confirmed to exist -- no entries from memory.
|
||||
#
|
||||
# Pattern shape: "<config>.@<type>[*].<option>"
|
||||
# <config> the /etc/config/<config> file the option lives in.
|
||||
# <type> the UCI section TYPE the option is nested under (NOT a section
|
||||
# name -- names are user-chosen and unpredictable, e.g. "wan" or
|
||||
# "myvpn", but every section still declares a type). May end in
|
||||
# a literal "*" to prefix-match a family of dynamically-named
|
||||
# types (WireGuard peers, see below) -- scrub.sh treats a
|
||||
# trailing "*" here as a shell case-glob, everything else as a
|
||||
# literal string.
|
||||
# [*] every section of that type, addressed the same way `uci show`
|
||||
# itself would print an anonymous one (@type[N]); scrub.sh
|
||||
# resolves this itself by listing every section declaration of
|
||||
# the matching type in the file and checking whether <option> is
|
||||
# actually set on it, so a named section (radio0, wan, myvpn --
|
||||
# uci addresses those by name, not [N]) is still found. Nothing
|
||||
# is fabricated: an option that is not present on a matching
|
||||
# section is left alone, never created.
|
||||
# <option> the option name to redact.
|
||||
# Pattern: "<config>.@<type>[*].<option>"
|
||||
# <type> section TYPE, not name. A trailing "*" is a case-glob prefix
|
||||
# (dynamic WireGuard peer types); anything else is literal.
|
||||
# [*] every section of that type, named or anonymous; an option not
|
||||
# set on a section is left alone, never created.
|
||||
#
|
||||
# Checked and found to hold NO inline UCI secret (file-based secrets, if
|
||||
# any, are exclude.list's job, not this file's):
|
||||
# - dropbear: openwrt/openwrt package/network/services/dropbear/files/
|
||||
# dropbear.config -- enable/PasswordAuth/RootPasswordAuth/Port only.
|
||||
# - mwan3: openwrt/luci applications/luci-app-mwan3, every
|
||||
# view/mwan3/network/*.js -- no password/secret/key option, "keyup" in
|
||||
# the source is an event name, not an option.
|
||||
#
|
||||
# Deliberately NOT listed even though the subsystem has secrets, because the
|
||||
# secret lives in a FILE the option merely points at, not in the option
|
||||
# value itself (a file path is not "the value replaced by a placeholder" --
|
||||
# scrub.sh only ever rewrites option values, and a leaked path is not the
|
||||
# same class of leak as a leaked passphrase):
|
||||
# - openvpn: tls_auth/tls_crypt/secret/askpass/auth_user_pass point at
|
||||
# /etc/openvpn/*, per openwrt/packages net/openvpn/files/openvpn.config.
|
||||
# - wireguard: the private/preshared keys ARE inline UCI values (listed
|
||||
# below); an interface's ca/usercert-equivalent is not applicable here.
|
||||
# No inline secret: dropbear (dropbear.config), mwan3 (luci-app-mwan3 views).
|
||||
# Not listed: openvpn tls_auth/tls_crypt/secret/askpass/auth_user_pass are
|
||||
# file paths, not secret values; scrub.sh only rewrites values.
|
||||
|
||||
# wireless -- WPA/WPA2/WPA3 PSK. Given verbatim in the brief and already the
|
||||
# stock gitbackup.security.scrub_option default; confirmed as a wifi-iface
|
||||
# option (not wifi-device) in
|
||||
# openwrt/luci modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:2531
|
||||
# wireless -- WPA PSK on wifi-iface (not wifi-device), openwrt/luci
|
||||
# modules/luci-mod-network/htdocs/luci-static/resources/view/network/wireless.js:2531
|
||||
wireless.@wifi-iface[*].key
|
||||
# wireless -- WEP keys 1-4 (legacy, still settable). Same file, the
|
||||
# uci.unset/taboption block generating 'key1'..'key4', wireless.js:1857-1882
|
||||
# wireless -- WEP keys 1-4, wireless.js:1857-1882
|
||||
wireless.@wifi-iface[*].key1
|
||||
wireless.@wifi-iface[*].key2
|
||||
wireless.@wifi-iface[*].key3
|
||||
wireless.@wifi-iface[*].key4
|
||||
# wireless -- SAE/WPA3 "password" field (distinct option from 'key' in the
|
||||
# same form), wireless.js:2120,2531
|
||||
# wireless -- SAE/WPA3 password, wireless.js:2120,2531
|
||||
wireless.@wifi-iface[*].password
|
||||
# wireless -- 802.1X/WPA-Enterprise RADIUS secrets, wireless.js:1729,1746,1830
|
||||
# wireless -- 802.1X RADIUS secrets, wireless.js:1729,1746,1830
|
||||
wireless.@wifi-iface[*].auth_secret
|
||||
wireless.@wifi-iface[*].acct_secret
|
||||
wireless.@wifi-iface[*].dae_secret
|
||||
|
||||
# network -- PAP/CHAP login used by every ppp-family proto (pppoe, pptp,
|
||||
# l2tp, pppoa) on a generic 'interface' section; confirmed identical across
|
||||
# openwrt/luci protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/{pppoe,pptp,l2tp,pppoa,ppp}.js
|
||||
# network -- ppp-family login (pppoe, pptp, l2tp, pppoa), openwrt/luci
|
||||
# protocols/luci-proto-ppp/htdocs/luci-static/resources/protocol/{pppoe,pptp,l2tp,pppoa,ppp}.js
|
||||
network.@interface[*].username
|
||||
network.@interface[*].password
|
||||
# network -- OpenVPN client cert passphrase and login password once migrated
|
||||
# onto 'network' (config file says "package network", type stays
|
||||
# 'interface', proto=openvpn): openwrt/packages
|
||||
# net/openvpn/files/openvpn.config:16,19 (password, cert_password) plus the
|
||||
# migration confirming the target file/type,
|
||||
# network -- OpenVPN cert passphrase after migration to network/interface,
|
||||
# openwrt/packages net/openvpn/files/openvpn.config:16,19 and
|
||||
# net/openvpn/files/etc/uci-defaults/60_openvpn_migrate.sh
|
||||
network.@interface[*].cert_password
|
||||
# network -- OpenConnect: second factor/OTP password and an inline PEM
|
||||
# private key (form.TextValue, not a file path), openwrt/luci
|
||||
# network -- OpenConnect OTP password and inline PEM key, openwrt/luci
|
||||
# protocols/luci-proto-openconnect/htdocs/luci-static/resources/protocol/openconnect.js:148,166
|
||||
network.@interface[*].password2
|
||||
network.@interface[*].userkey
|
||||
# network -- WireGuard interface private key, openwrt/luci
|
||||
# network -- WireGuard private key, openwrt/luci
|
||||
# protocols/luci-proto-wireguard/htdocs/luci-static/resources/protocol/wireguard.js:128,380
|
||||
network.@interface[*].private_key
|
||||
# network -- WireGuard peer preshared key. Peer sections use a DYNAMIC
|
||||
# section type "wireguard_<ifname>" (one per WireGuard interface), not a
|
||||
# fixed type name -- confirmed by wireguard.js:195,344-384,572
|
||||
# ("uci.add('network', 'wireguard_' + s.section)"), hence the type prefix
|
||||
# wildcard instead of a literal type.
|
||||
# network -- WireGuard peer PSK; peer type is dynamic "wireguard_<ifname>",
|
||||
# wireguard.js:195,344-384,572
|
||||
network.@wireguard_*[*].preshared_key
|
||||
|
||||
# ddns -- login credentials substituted into the update URL, on a 'service'
|
||||
# section (uci.add('ddns', 'service', section_id)): openwrt/luci
|
||||
# ddns -- update-URL credentials, openwrt/luci
|
||||
# applications/luci-app-ddns/htdocs/luci-static/resources/view/ddns/overview.js:751,757-763
|
||||
ddns.@service[*].username
|
||||
ddns.@service[*].password
|
||||
|
||||
# uhttpd -- HTTP Basic-Auth credentials for a URL prefix, on a 'httpauth'
|
||||
# section (config_list_foreach "$cfg" httpauth create_httpauth):
|
||||
# openwrt/openwrt package/network/services/uhttpd/files/uhttpd.init:68-79
|
||||
# uhttpd -- Basic-Auth credentials, openwrt/openwrt
|
||||
# package/network/services/uhttpd/files/uhttpd.init:68-79
|
||||
uhttpd.@httpauth[*].username
|
||||
uhttpd.@httpauth[*].password
|
||||
@@ -1,55 +1,17 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- secret redaction for visibility=public (spec "Scrub").
|
||||
#
|
||||
# Exposes gb_scrub; every _gb_scrub_-prefixed helper below is private,
|
||||
# namespaced the same way collect.sh explains its own helpers are (every
|
||||
# *.sh here ends up sourced into one process by usr/sbin/gitbackup, so a
|
||||
# same-named helper in a sibling module would silently shadow this one
|
||||
# otherwise). Sourced, never executed: nothing here runs at load time --
|
||||
# t_scrub_no_side_effect_on_source in tests/run.sh asserts this directly by
|
||||
# sourcing the module over a tree that still has its secrets and checking
|
||||
# nothing changed.
|
||||
#
|
||||
# Depends on lib.sh (gb_log, gb_json_str, gb_uci_get, gb_manifest_field) already being sourced
|
||||
# by the caller, same convention every other module here uses. Does NOT
|
||||
# depend on collect.sh: gb_scrub computes manifest.json's path itself
|
||||
# (outdir/manifest.json, the one fixed shape gb_collect ever writes) rather
|
||||
# than calling collect.sh's gb_manifest_path, so this module has no load-order
|
||||
# requirement on it and the two stay testable apart.
|
||||
#
|
||||
# Who calls this and when is not this module's concern (spec: "Только при
|
||||
# visibility=public, где он принудителен" is a decision usr/sbin/gitbackup's
|
||||
# gb_validate_config/_gb_effective_scrub already makes and exposes as
|
||||
# GB_SCRUB -- see tests/run.sh's t_cli_public_forces_scrub). gb_scrub itself
|
||||
# has no opinion on visibility and no config lookup for whether to run: it
|
||||
# scrubs whatever tree it is given, unconditionally, the same way gb_collect
|
||||
# has no opinion on what to do with the tree it builds.
|
||||
#
|
||||
# GB_SCRUB_LIST/GB_EXCLUDE_LIST override where the static pattern files live
|
||||
# (defaulting under GB_SHARE, same convention collect.sh's GB_EXCLUDE_LIST
|
||||
# uses) so tests/run.sh can point this at a fixture instead of the real
|
||||
# package data. Neither is set by the router.
|
||||
# gitbackup -- secret redaction: gb_scrub. Needs lib.sh. Works only on the
|
||||
# collected copy, never the live config. Whether to run is the CLI's call
|
||||
# (GB_SCRUB); whole-file secrets are excluded by collect.sh instead.
|
||||
|
||||
GB_SCRUB_LIST="${GB_SCRUB_LIST:-${GB_SHARE:-/usr/share/gitbackup}/scrub.list}"
|
||||
GB_EXCLUDE_LIST="${GB_EXCLUDE_LIST:-${GB_SHARE:-/usr/share/gitbackup}/exclude.list}"
|
||||
|
||||
# The placeholder every redacted value becomes. No hash: spec "Scrub" --
|
||||
# "Хеша не остаётся: короткий PSK по хешу подбирается, и хеш сам по себе
|
||||
# был бы утечкой." A fixed constant, not overridable -- unlike the two
|
||||
# above, there is no legitimate reason a caller would want a different one.
|
||||
# Fixed placeholder, no hash: a short PSK is brute-forceable from its hash.
|
||||
GB_SCRUB_PLACEHOLDER='<gitbackup:redacted>'
|
||||
|
||||
# gb_scrub <outdir>
|
||||
#
|
||||
# Mutates outdir/files/etc/config/* in place via `uci -c`, never sed (spec:
|
||||
# "многострочные list ломают любой построчный разбор, и молча"), removes the
|
||||
# files exclude.list's public-only block names entirely, rewrites
|
||||
# outdir/manifest.json's scrubbed[] array and re-hashes/drops the affected
|
||||
# entries[] lines, and prints one "<path>\t<uci-option-path>" line per
|
||||
# redacted option to stdout for the caller's own logging. A tree with no
|
||||
# etc/config directory at all (a stripped-down fixture, or a router with
|
||||
# nothing there) is a no-op, not an error.
|
||||
# gb_scrub <outdir> -- redacts via `uci -c ... -t <savedir>`, never sed
|
||||
# (multi-line lists break line-based edits silently), re-hashes, fills
|
||||
# scrubbed[], prints "<path>\t<option>" per redacted option.
|
||||
gb_scrub() {
|
||||
_gb_outdir="$1"
|
||||
[ -n "$_gb_outdir" ] || { gb_log err 'gb_scrub: outdir is required'; return 1; }
|
||||
@@ -61,34 +23,24 @@ gb_scrub() {
|
||||
mkdir -p "$_gb_savedir"
|
||||
: >"$_gb_scratch/records"
|
||||
: >"$_gb_scratch/rehash"
|
||||
: >"$_gb_scratch/removed"
|
||||
|
||||
[ -d "$_gb_confdir" ] && _gb_scrub_options "$_gb_confdir" "$_gb_scratch"
|
||||
_gb_scrub_hard_exclude "$_gb_outdir" "$_gb_scratch"
|
||||
|
||||
cat "$_gb_scratch/records"
|
||||
|
||||
[ -r "$_gb_manifest" ] && _gb_scrub_write_manifest "$_gb_manifest" \
|
||||
"$_gb_scratch/records" "$_gb_scratch/rehash" "$_gb_scratch/removed"
|
||||
"$_gb_scratch/records" "$_gb_scratch/rehash"
|
||||
|
||||
rm -rf "$_gb_scratch"
|
||||
}
|
||||
|
||||
# _gb_scrub_options <confdir> <scratch> -- the UCI-editing half of gb_scrub.
|
||||
# Walks scrub.list (+ the UCI-configurable list scrub_option) grouped by
|
||||
# config file, so each touched file is `uci show`n and committed exactly
|
||||
# once regardless of how many patterns target it.
|
||||
# Grouped by config, so each file is shown and committed once.
|
||||
_gb_scrub_options() {
|
||||
_gb_confdir="$1"
|
||||
_gb_scratch="$2"
|
||||
_gb_savedir="$_gb_scratch/save"
|
||||
|
||||
# sort -u: gitbackup.security's own shipped default
|
||||
# (list scrub_option 'wireless.@wifi-iface[*].key', ticket 01) is the
|
||||
# same line scrub.list already carries -- found live on the owlab
|
||||
# stand, where an unfiltered run redacted the one real option correctly
|
||||
# but printed and recorded it twice. One pattern, one record, however
|
||||
# many sources agreed on it.
|
||||
# sort -u: the shipped scrub_option default duplicates a scrub.list line.
|
||||
_gb_scrub_load_patterns | sort -u >"$_gb_scratch/patterns"
|
||||
[ -s "$_gb_scratch/patterns" ] || return 0
|
||||
|
||||
@@ -98,10 +50,8 @@ _gb_scrub_options() {
|
||||
|
||||
_gb_sections="$_gb_scratch/sections.$_gb_cfg"
|
||||
: >"$_gb_sections"
|
||||
# One line per section DECLARATION ("cfg.ref=type"), ref TAB type.
|
||||
# A declaration line has exactly one dot after the config name; an
|
||||
# option line ("cfg.ref.option=value") has two, so the ".*" guard
|
||||
# tells them apart without needing to know option names in advance.
|
||||
# Section declarations only ("cfg.ref=type" -> ref TAB type); option
|
||||
# lines have a second dot.
|
||||
uci -c "$_gb_confdir" show "$_gb_cfg" 2>/dev/null | \
|
||||
while IFS= read -r _gb_l; do
|
||||
case "$_gb_l" in
|
||||
@@ -122,24 +72,14 @@ _gb_scrub_options() {
|
||||
[ "$_gb_pc" = "$_gb_cfg" ] || continue
|
||||
while IFS=' ' read -r _gb_ref _gb_type; do
|
||||
[ -n "$_gb_ref" ] || continue
|
||||
# Unquoted on the right on purpose: a scrub.list type may end
|
||||
# in "*" to prefix-match a dynamically-named type family
|
||||
# (WireGuard peers, "wireguard_<ifname>") -- see scrub.list's
|
||||
# own header. A plain type name has no glob metacharacters,
|
||||
# so this is an exact match for every pattern that is not
|
||||
# using the prefix form.
|
||||
# Unquoted: a trailing "*" prefix-matches (wireguard_<ifname>).
|
||||
# shellcheck disable=SC2254
|
||||
case "$_gb_type" in
|
||||
$_gb_ptype) ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
_gb_path="$_gb_cfg.$_gb_ref.$_gb_popt"
|
||||
# Existence check first: `uci set` on a nonexistent option
|
||||
# CREATES it (verified against openwrt/uci's list.c uci_set,
|
||||
# the "!ptr->o && ptr->option" branch), and fabricating an
|
||||
# option the user never set is not this module's job to do,
|
||||
# ever -- scrub redacts what is there, it does not invent
|
||||
# config the restored router would then apply.
|
||||
# `uci set` on a missing option would create it.
|
||||
uci -c "$_gb_confdir" get "$_gb_path" >/dev/null 2>&1 || continue
|
||||
uci -c "$_gb_confdir" -t "$_gb_savedir" set \
|
||||
"$_gb_path=$GB_SCRUB_PLACEHOLDER" >/dev/null 2>&1 || continue
|
||||
@@ -149,15 +89,8 @@ _gb_scrub_options() {
|
||||
done <"$_gb_scratch/patterns"
|
||||
|
||||
if [ -f "$_gb_touched" ]; then
|
||||
# -t, not -P: -P also sets CLI_FLAG_NOCOMMIT (verified live on
|
||||
# the owlab stand -- a `commit` issued with -P silently no-ops,
|
||||
# ret=0, file untouched), which would make this commit do
|
||||
# nothing. -t sets the same custom save directory without that
|
||||
# flag, so `set` and `commit` agree on where the pending delta
|
||||
# lives without ever touching the router's own default savedir
|
||||
# (/tmp/.uci) -- needed since two gb_scrub calls (or two test
|
||||
# cases) touching a config of the same name must not collide
|
||||
# through a savedir keyed only by that name.
|
||||
# -t, not -P: -P also sets no-commit, so commit would silently
|
||||
# no-op. -t keeps set/commit on a private savedir, off /tmp/.uci.
|
||||
uci -c "$_gb_confdir" -t "$_gb_savedir" commit "$_gb_cfg" >/dev/null 2>&1
|
||||
_gb_sha=$(sha256sum "$_gb_confdir/$_gb_cfg" 2>/dev/null | awk '{print $1}')
|
||||
[ -n "$_gb_sha" ] && printf '/etc/config/%s\t%s\n' "$_gb_cfg" "$_gb_sha" >>"$_gb_scratch/rehash"
|
||||
@@ -165,21 +98,14 @@ _gb_scrub_options() {
|
||||
done
|
||||
}
|
||||
|
||||
# _gb_scrub_load_patterns -- prints "config type option" (space separated)
|
||||
# for every recognized scrub.list line plus every gitbackup.security's list
|
||||
# scrub_option entry, same format, spec "Расширяется через list scrub_option
|
||||
# в UCI". gb_uci_get (lib.sh) already returns a UCI list option's values
|
||||
# space-joined on one line -- the same shape `uci get` itself prints -- so
|
||||
# splitting it with a plain for loop needs no extra parsing.
|
||||
# scrub.list plus gitbackup.security.scrub_option (space-joined UCI list).
|
||||
_gb_scrub_load_patterns() {
|
||||
if [ -r "$GB_SCRUB_LIST" ]; then
|
||||
while IFS= read -r _gb_l || [ -n "$_gb_l" ]; do
|
||||
_gb_scrub_parse_pattern "$_gb_l"
|
||||
done <"$GB_SCRUB_LIST"
|
||||
fi
|
||||
# set -f: same reasoning as _gb_scrub_parse_pattern's own -- a pattern's
|
||||
# "[*]" is a valid glob bracket expression, so splitting this on words
|
||||
# must not also pathname-expand it.
|
||||
# noglob: "[*]" is a valid glob bracket expression.
|
||||
# shellcheck disable=SC2046 # word-splitting is the point: one pattern per word
|
||||
set -f
|
||||
for _gb_p in $(gb_uci_get gitbackup.security.scrub_option); do
|
||||
@@ -188,18 +114,11 @@ _gb_scrub_load_patterns() {
|
||||
set +f
|
||||
}
|
||||
|
||||
# _gb_scrub_parse_pattern <line> -- parses one "<config>.@<type>[*].<option>"
|
||||
# line (comment and surrounding blanks already allowed in <line>) and prints
|
||||
# "<config> <type> <option>", or nothing for a blank/comment-only line, or a
|
||||
# warning (never a partial/guessed result) for anything else.
|
||||
# "<config>.@<type>[*].<option>" -> "<config> <type> <option>"; anything
|
||||
# unrecognized warns, never a guessed result.
|
||||
_gb_scrub_parse_pattern() {
|
||||
_gb_raw="${1%%#*}"
|
||||
# set -f: a pattern's own "[*]" wildcard marker is a syntactically valid
|
||||
# glob bracket expression (matches one literal "*"), so plain word
|
||||
# splitting here could also pathname-expand against the working
|
||||
# directory -- the exact live bug _gb_scrub_hard_exclude's own comment
|
||||
# describes, guarded against here for the same reason even though no
|
||||
# fixture has hit it yet.
|
||||
# noglob: "[*]" would otherwise pathname-expand.
|
||||
set -f
|
||||
# shellcheck disable=SC2086 # word-splitting trims surrounding blanks; a pattern itself has none
|
||||
set -- $_gb_raw
|
||||
@@ -222,91 +141,7 @@ _gb_scrub_parse_pattern() {
|
||||
esac
|
||||
}
|
||||
|
||||
# _gb_scrub_hard_exclude <outdir> <scratch> -- removes exclude.list's
|
||||
# public-only lines (every "#public: <pattern>" line -- a plain comment to
|
||||
# collect.sh, see exclude.list's own header on why) from outdir/files,
|
||||
# recording each removed absolute path (one per line, matching
|
||||
# manifest.json's own path format) to scratch/removed so
|
||||
# _gb_scrub_write_manifest drops it from entries[] too. Same glob rules as
|
||||
# collect.sh's own hard-exclude ("/**" suffix = itself and everything under
|
||||
# it, else a plain case/shell glob) -- reimplemented here rather than
|
||||
# calling collect.sh's private _gb_collect_is_excluded, which is not this
|
||||
# module's to reach into (see this file's header on cross-module coupling).
|
||||
_gb_scrub_hard_exclude() {
|
||||
_gb_outdir="$1"
|
||||
_gb_scratch="$2"
|
||||
_gb_files="$_gb_outdir/files"
|
||||
[ -r "$GB_EXCLUDE_LIST" ] || return 0
|
||||
while IFS= read -r _gb_pat || [ -n "$_gb_pat" ]; do
|
||||
case "$_gb_pat" in
|
||||
'#public:'*) _gb_pat="${_gb_pat#'#public:'}" ;;
|
||||
*) continue ;;
|
||||
esac
|
||||
# set -f around this: found live on the owlab stand -- with globbing
|
||||
# on, this bare `set --` doesn't just trim the mandatory space after
|
||||
# "#public:", it pathname-expands the pattern against the CURRENT
|
||||
# working directory right then, and an absolute pattern like
|
||||
# "/etc/dropbear/*_host_key*" glob-matches the ROUTER'S OWN real
|
||||
# /etc/dropbear -- not outdir/files -- silently replacing the
|
||||
# pattern with whatever real host keys exist on the box running the
|
||||
# test. The intended glob (against "$_gb_files$_gb_pat" below) never
|
||||
# ran on the right string, and the real host key was never removed.
|
||||
set -f
|
||||
# shellcheck disable=SC2086
|
||||
set -- $_gb_pat
|
||||
set +f
|
||||
_gb_pat="${1:-}"
|
||||
[ -n "$_gb_pat" ] || continue
|
||||
case "$_gb_pat" in
|
||||
*/'**')
|
||||
_gb_prefix=${_gb_pat%/\*\*}
|
||||
find "$_gb_files$_gb_prefix" -mindepth 0 2>/dev/null | \
|
||||
while IFS= read -r _gb_found; do
|
||||
_gb_relpath="${_gb_found#"$_gb_files"}"
|
||||
printf '%s\n' "$_gb_relpath" >>"$_gb_scratch/removed"
|
||||
done
|
||||
rm -rf "${_gb_files:?}$_gb_prefix"
|
||||
;;
|
||||
*)
|
||||
# Unquoted glob on purpose: real filesystem expansion against
|
||||
# outdir/files is exactly what a hard-exclude pattern like
|
||||
# "/etc/dropbear/*_host_key*" needs. No match leaves the
|
||||
# literal pattern string, which -e then rejects -- ash and
|
||||
# every POSIX sh here run with nullglob off, same as a bare
|
||||
# `for f in *.nosuchext` on an empty match.
|
||||
# shellcheck disable=SC2086,SC2231
|
||||
for _gb_found in "$_gb_files"$_gb_pat; do
|
||||
[ -e "$_gb_found" ] || continue
|
||||
_gb_relpath="${_gb_found#"$_gb_files"}"
|
||||
printf '%s\n' "$_gb_relpath" >>"$_gb_scratch/removed"
|
||||
rm -f "$_gb_found"
|
||||
done
|
||||
;;
|
||||
esac
|
||||
done <"$GB_EXCLUDE_LIST"
|
||||
}
|
||||
|
||||
# _gb_scrub_join -- reads raw (unindented, comma-free) object-body lines on
|
||||
# stdin and writes them 4-space-indented and comma-joined (no trailing comma
|
||||
# on the last one), the exact array-body shape collect.sh's own
|
||||
# _gb_collect_join produces -- reimplemented locally rather than called
|
||||
# (collect.sh's version is private to it, see this file's header). Empty
|
||||
# input produces empty output, matching what collect.sh itself writes for an
|
||||
# empty array.
|
||||
_gb_scrub_join() {
|
||||
_gb_first=1
|
||||
while IFS= read -r _gb_b || [ -n "$_gb_b" ]; do
|
||||
[ -n "$_gb_b" ] || continue
|
||||
if [ "$_gb_first" -eq 1 ]; then _gb_first=0; else printf ',\n'; fi
|
||||
printf ' %s' "$_gb_b"
|
||||
done
|
||||
[ "$_gb_first" -eq 1 ] || printf '\n'
|
||||
}
|
||||
|
||||
# _gb_scrub_records_json <records> -- <records> is "<path>\t<option>" lines;
|
||||
# prints one {"path":...,"option":...} object per line. No value and no hash
|
||||
# ever appears here (spec: "значений в нём нет") -- only the two fields the
|
||||
# records file itself carries.
|
||||
# Never a value or hash in the manifest.
|
||||
_gb_scrub_records_json() {
|
||||
[ -s "$1" ] || return 0
|
||||
while IFS=' ' read -r _gb_rp _gb_ro || [ -n "$_gb_rp" ]; do
|
||||
@@ -315,80 +150,22 @@ _gb_scrub_records_json() {
|
||||
done <"$1"
|
||||
}
|
||||
|
||||
# _gb_scrub_write_manifest <manifest.json> <records> <rehash> <removed>
|
||||
#
|
||||
# Line-oriented, same technique collect.sh's own gb_manifest_equal already
|
||||
# relies on (gb_collect emits exactly one JSON object per array item, no
|
||||
# nested newlines) -- this is editing gitbackup's OWN manifest format, not
|
||||
# UCI config text, so the "no sed on multi-line list" rule this module
|
||||
# exists for does not apply to it. Rewrites entries[] (dropping the paths in
|
||||
# <removed>, re-hashing the ones in <rehash>) and scrubbed[] (built fresh
|
||||
# from <records>) in one pass; everything else in the file is copied
|
||||
# unchanged.
|
||||
# _gb_scrub_write_manifest <manifest.json> <records> <rehash> -- relies on our
|
||||
# own manifest having one entry per line.
|
||||
_gb_scrub_write_manifest() {
|
||||
_gb_manifest="$1"
|
||||
_gb_records="$2"
|
||||
_gb_rehash="$3"
|
||||
_gb_removed="$4"
|
||||
_gb_tmp="$_gb_manifest.tmp.$$"
|
||||
_gb_ent="$_gb_manifest.entries.$$"
|
||||
: >"$_gb_tmp"
|
||||
: >"$_gb_ent"
|
||||
_gb_in_entries=0
|
||||
_gb_in_scrubbed=0
|
||||
|
||||
while IFS= read -r _gb_line || [ -n "$_gb_line" ]; do
|
||||
if [ "$_gb_in_entries" -eq 1 ]; then
|
||||
case "$_gb_line" in
|
||||
' ],')
|
||||
_gb_scrub_join <"$_gb_ent" >>"$_gb_tmp"
|
||||
printf '%s\n' "$_gb_line" >>"$_gb_tmp"
|
||||
_gb_in_entries=0
|
||||
;;
|
||||
*)
|
||||
# Strip the trailing "," (absent only on the last item)
|
||||
# and the fixed 4-space indent collect.sh always writes,
|
||||
# so _gb_scrub_join can re-add both consistently once the
|
||||
# surviving set is known.
|
||||
_gb_obj="${_gb_line%,}"
|
||||
_gb_obj="${_gb_obj# }"
|
||||
_gb_p=$(gb_manifest_field "$_gb_obj" path)
|
||||
if [ -s "$_gb_removed" ] && grep -qxF "$_gb_p" "$_gb_removed"; then
|
||||
: # dropped -- hard-excluded, no longer in the tree
|
||||
else
|
||||
_gb_newsha=''
|
||||
if [ -s "$_gb_rehash" ]; then
|
||||
_gb_newsha=$(awk -F'\t' -v p="$_gb_p" '$1==p{print $2; exit}' "$_gb_rehash")
|
||||
fi
|
||||
if [ -n "$_gb_newsha" ]; then
|
||||
_gb_obj=$(printf '%s' "$_gb_obj" | sed 's/"sha256":"[^"]*"/"sha256":"'"$_gb_newsha"'"/')
|
||||
fi
|
||||
printf '%s\n' "$_gb_obj" >>"$_gb_ent"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
continue
|
||||
fi
|
||||
|
||||
case "$_gb_line" in
|
||||
' "entries": ['*)
|
||||
printf '%s\n' "$_gb_line" >>"$_gb_tmp"
|
||||
_gb_in_entries=1
|
||||
continue
|
||||
;;
|
||||
' "scrubbed": ['*)
|
||||
printf '%s\n' "$_gb_line" >>"$_gb_tmp"
|
||||
_gb_scrub_records_json "$_gb_records" | _gb_scrub_join >>"$_gb_tmp"
|
||||
_gb_in_scrubbed=1
|
||||
continue
|
||||
;;
|
||||
' ]')
|
||||
[ "$_gb_in_scrubbed" -eq 1 ] && _gb_in_scrubbed=0
|
||||
;;
|
||||
esac
|
||||
printf '%s\n' "$_gb_line" >>"$_gb_tmp"
|
||||
done <"$_gb_manifest"
|
||||
|
||||
rm -f "$_gb_ent"
|
||||
mv "$_gb_tmp" "$_gb_manifest"
|
||||
_gb_tmp="$1.tmp.$$"
|
||||
_gb_scrub_records_json "$2" | gb_manifest_join >"$_gb_tmp.scrubbed"
|
||||
cp "$1" "$_gb_tmp"
|
||||
while IFS=' ' read -r _gb_rp _gb_rs || [ -n "$_gb_rp" ]; do
|
||||
[ -n "$_gb_rp" ] || continue
|
||||
_gb_needle="\"path\":$(gb_json_str "$_gb_rp"),"
|
||||
awk -v n="$_gb_needle" -v sha="$_gb_rs" '
|
||||
index($0, n) { sub(/"sha256":"[^"]*"/, "\"sha256\":\"" sha "\"") } { print }
|
||||
' "$_gb_tmp" >"$_gb_tmp.new" && mv "$_gb_tmp.new" "$_gb_tmp"
|
||||
done <"$3"
|
||||
awk -v f="$_gb_tmp.scrubbed" '
|
||||
{ print }
|
||||
/^ "scrubbed": \[/ { while ((getline l < f) > 0) print l }
|
||||
' "$_gb_tmp" >"$1"
|
||||
rm -f "$_gb_tmp" "$_gb_tmp.scrubbed"
|
||||
}
|
||||
@@ -1,39 +1,17 @@
|
||||
# shellcheck shell=sh
|
||||
#
|
||||
# gitbackup -- the visibility gate (spec "Гейт видимости").
|
||||
#
|
||||
# There is no encryption anywhere in this project, so this is the only thing
|
||||
# standing between a public repository and the root password hash, private
|
||||
# dropbear host keys, WPA PSK and WireGuard keys it would carry. A public
|
||||
# repository is exit 4 with no way to override it -- not a warning.
|
||||
#
|
||||
# Sourced, never executed: nothing here runs at load time. gb_visibility_ok
|
||||
# calls gb_uci_get, gb_log, gb_die and gb_json_bool (lib.sh) and gb_parse_url/
|
||||
# gb_provider (remoteurl.sh), so a caller must source both first, same
|
||||
# convention as device.sh.
|
||||
#
|
||||
# GB_STATE_DIR overrides the cache directory (default /var/run/gitbackup),
|
||||
# the same variable usr/sbin/gitbackup already uses -- tests point it at a
|
||||
# tmp directory, the router never sets it.
|
||||
# gitbackup -- the visibility gate: gb_visibility_ok. With no encryption, this
|
||||
# is all that keeps /etc/shadow, host keys and PSKs out of a public repo:
|
||||
# public is exit 4 with no override. Needs lib.sh and remoteurl.sh.
|
||||
|
||||
# gb_visibility_ok <url>
|
||||
#
|
||||
# Anonymously asks the provider's API whether <url>'s repository is visible
|
||||
# to a passer-by, and returns:
|
||||
# 0 not visible anonymously (404) or the operator explicitly accepted the
|
||||
# risk for a provider that cannot be checked at all -- proceed.
|
||||
# 3 inconclusive: the network is down, the API answered 5xx, or answered
|
||||
# with an HTTP status this gate does not recognize. Deliberately NOT 0:
|
||||
# a caller that cannot tell "verified private" from "could not check"
|
||||
# apart would push on a repository nobody actually confirmed is closed
|
||||
# to the world. Reuses the shared network/auth exit code (interfaces.md,
|
||||
# "Коды выхода") rather than inventing a fifth one.
|
||||
# 4 visible anonymously (HTTP 200) -- refused, unconditionally.
|
||||
# Dies with exit 2 -- invalid configuration, same as every other config
|
||||
# check in this package -- when <url> does not parse, or when the remote is
|
||||
# provider=generic without gitbackup.origin.acknowledged=1 (nothing there
|
||||
# can ever be checked, so silence would mean "assume private" on zero
|
||||
# evidence, exactly the failure mode this whole gate exists to prevent).
|
||||
# gb_visibility_ok <url> -- anonymous provider API probe:
|
||||
# 0 not visible anonymously (404) -- proceed.
|
||||
# 3 inconclusive (offline, 5xx, unknown status). Never 0: "could not
|
||||
# check" must not be treated as "private".
|
||||
# 4 visible anonymously (200) -- refused.
|
||||
# Dies 2 on an unparsable URL, or on provider=generic without
|
||||
# gitbackup.origin.acknowledged=1 (nothing to check; silence is not proof).
|
||||
# Definite answers are cached for 24h.
|
||||
gb_visibility_ok() {
|
||||
_gb_url="$1"
|
||||
_gb_parsed=$(gb_parse_url "$_gb_url") ||
|
||||
@@ -86,9 +64,7 @@ gb_visibility_ok() {
|
||||
_gb_result=0
|
||||
;;
|
||||
*)
|
||||
# Inconclusive: never cache a guess, so the very next call (this run
|
||||
# or the next) tries the network again instead of trusting a stale
|
||||
# "could not tell" for a whole day.
|
||||
# Inconclusive: never cached, so the next call retries.
|
||||
return 3
|
||||
;;
|
||||
esac
|
||||
@@ -98,15 +74,8 @@ gb_visibility_ok() {
|
||||
return "$_gb_result"
|
||||
}
|
||||
|
||||
# _gb_visibility_api_url <provider> <scheme> <host> <port> <owner> <repo>
|
||||
#
|
||||
# The anonymous, unauthenticated read-only endpoint each provider answers
|
||||
# with 200 (visible) or 404 (not visible/does not exist) for -- spec
|
||||
# "Гейт видимости" and "Проверенные факты 25.12.4 → Провайдеры", each row
|
||||
# measured against the real API, not guessed. Always https: git access may
|
||||
# be over ssh, but every one of these platforms serves its API over https on
|
||||
# the same host. Returns 1 for a provider with no known API (generic, or
|
||||
# anything gitbackup.origin.provider names that this gate does not recognize).
|
||||
# Anonymous endpoint answering 200 (visible) / 404 (not); https even for ssh
|
||||
# remotes. Returns 1 for a provider with no known API.
|
||||
_gb_visibility_api_url() {
|
||||
case "$1" in
|
||||
github)
|
||||
@@ -127,14 +96,7 @@ _gb_visibility_api_url() {
|
||||
esac
|
||||
}
|
||||
|
||||
# _gb_hostport_for_api <scheme> <host> <port> -- host[:port] for the same
|
||||
# server's HTTPS API.
|
||||
#
|
||||
# Only a port that was itself already on an https:// remote is trustworthy
|
||||
# here. A scp-like or ssh:// remote's port is dropbear/openssh's, which a
|
||||
# self-hosted Gitea/Forgejo/GitLab install commonly runs on a different port
|
||||
# than its web UI (e.g. ssh on 2222, https on the default 443) -- carrying
|
||||
# it over would probe the wrong port.
|
||||
# Keeps the port only from an https remote; an ssh port is not the API port.
|
||||
_gb_hostport_for_api() {
|
||||
if [ "$1" = https ] && [ "${3:-0}" != 0 ]; then
|
||||
printf '%s:%s\n' "$2" "$3"
|
||||
@@ -143,18 +105,9 @@ _gb_hostport_for_api() {
|
||||
fi
|
||||
}
|
||||
|
||||
# _gb_visibility_probe <api-url>
|
||||
#
|
||||
# Returns 0 when the repository is visible anonymously (HTTP 200), 1 when it
|
||||
# is not (HTTP 404 -- private and nonexistent are the same answer here on
|
||||
# purpose, spec: "приватный анонимно неотличим от несуществующего"), 2 for
|
||||
# anything else: another HTTP status, a timeout, a refused connection, a TLS
|
||||
# failure, or offline. -q is deliberately not passed to uclient-fetch: the
|
||||
# only place it prints "HTTP error <code>" is to stderr, and only when not
|
||||
# quiet -- verified against uclient/uclient-fetch.c (header_done_cb's default
|
||||
# case; net/error branches for connection/timeout/TLS return non-8 codes and
|
||||
# are folded into the same "inconclusive" outcome here as any other network
|
||||
# failure).
|
||||
# 0 on 200, 1 on 404 (private and nonexistent look the same), 2 otherwise.
|
||||
# No -q: uclient-fetch prints "HTTP error <code>" only when not quiet, and
|
||||
# exit 8 alone does not tell 404 from other statuses.
|
||||
_gb_visibility_probe() {
|
||||
_gb_probe_msg=$(uclient-fetch --timeout=10 -O /dev/null "$1" 2>&1 >/dev/null)
|
||||
_gb_probe_rc=$?
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# This file exists only so `owlab build` can find package/gitbackup/Makefile --
|
||||
# `owlab build` runs the OpenWrt SDK, which is NOT how gitbackup is actually released
|
||||
# (see /owfeed.yml and /tools/stage.sh: `owfeed build` is the primary, fast path for a
|
||||
# noarch package with nothing to compile). This one stays because the Makefile is still
|
||||
# a valid, if slower, way for anyone to build the package themselves.
|
||||
#
|
||||
# owlab's build command looks for an OpenWrt package Makefile in its config
|
||||
# directory and that directory's IMMEDIATE children only (read from source:
|
||||
# owfeed/owlab/cmd/owlab/build.go, findPackage) -- it does not walk two
|
||||
# levels down from a repo root the way the real OpenWrt SDK's own feed
|
||||
# scanner does. The repository's own top-level owlab.yaml is what
|
||||
# `owlab up`/`sync`/`test`/`exec` use for the live router; owlab.yaml
|
||||
# discovery walks upward from the working directory and stops at the FIRST
|
||||
# file it finds, so this one only has to exist here to be picked up before
|
||||
# that search ever reaches the repository root. It repeats the release the
|
||||
# top-level file tests against rather than deferring to it, because there is
|
||||
# no way to defer: discovery does not chain two config files together.
|
||||
#
|
||||
# cd package/gitbackup && owlab build --release 25.12.4
|
||||
|
||||
version: 1
|
||||
|
||||
project:
|
||||
name: gitbackup
|
||||
|
||||
routers:
|
||||
- id: owrt2512
|
||||
release: "25.12.4"
|
||||
ports: { http: 8029, ssh: 2230 }
|
||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease-common-bin
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=38
|
||||
PKG_RELEASE:=39
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=7862188a4a1fe17d7d275f5ecfa7c24acc5d2049
|
||||
PKG_SOURCE_VERSION:=003afc294e0a56b8d92b967af5684dcd33a23bf3
|
||||
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=41
|
||||
PKG_RELEASE:=42
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=7862188a4a1fe17d7d275f5ecfa7c24acc5d2049
|
||||
PKG_SOURCE_VERSION:=003afc294e0a56b8d92b967af5684dcd33a23bf3
|
||||
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
@@ -11,7 +11,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkeasefull
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=3.0.22
|
||||
PKG_RELEASE:=40
|
||||
PKG_RELEASE:=41
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=7862188a4a1fe17d7d275f5ecfa7c24acc5d2049
|
||||
PKG_SOURCE_VERSION:=003afc294e0a56b8d92b967af5684dcd33a23bf3
|
||||
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
||||
|
||||
@@ -6,7 +6,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-daede
|
||||
PKG_VERSION:=1.15
|
||||
PKG_RELEASE:=44
|
||||
PKG_RELEASE:=45
|
||||
PKG_MAINTAINER:=kenzok8
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ const DEFAULT_TEMPLATE =
|
||||
' }\n' +
|
||||
' routing {\n' +
|
||||
' request {\n' +
|
||||
' qname(suffix:googleapis.cn) -> fallbackdns\n' +
|
||||
' qname(geosite:cn) -> cndns\n' +
|
||||
' fallback: fallbackdns\n' +
|
||||
' }\n' +
|
||||
@@ -70,6 +71,7 @@ const DEFAULT_TEMPLATE =
|
||||
' l4proto(udp) && dport(123) -> direct\n' +
|
||||
' domain(connectivitycheck.gstatic.com) -> direct\n' +
|
||||
' domain(msftconnecttest.com) -> direct\n' +
|
||||
' domain(suffix:googleapis.cn) -> proxy\n' +
|
||||
' dip(geoip:cn) -> direct\n' +
|
||||
' domain(geosite:cn) -> direct\n' +
|
||||
' fallback: proxy\n' +
|
||||
@@ -704,7 +706,7 @@ function renderDaeEditor() {
|
||||
save.disabled = true;
|
||||
flashStatus(_('Validating…'));
|
||||
fs.write(VALIDATE_PATH, textarea.value, 384)
|
||||
.then(function() { return fs.exec('/usr/bin/dae', ['validate', '-c', VALIDATE_PATH]); })
|
||||
.then(function() { return fs.exec('/usr/bin/env', ['DAE_LOCATION_ASSET=/usr/share/v2ray', '/usr/bin/dae', 'validate', '-c', VALIDATE_PATH]); })
|
||||
.then(function(res) {
|
||||
if (res && res.code !== 0) {
|
||||
const err = (res.stderr || res.stdout || ('exit ' + res.code)).trim().split('\n')[0];
|
||||
|
||||
@@ -53,6 +53,7 @@ DAE_INIT="/etc/init.d/dae"
|
||||
if [ -f "$DAE_INIT" ]; then
|
||||
sed -i 's/--disable-timestamp//' "$DAE_INIT"
|
||||
sed -i '/procd_append_param command *$/d' "$DAE_INIT"
|
||||
sed -i 's|^[[:space:]]*"$PROG" validate -c "$config_file"| DAE_LOCATION_ASSET="/usr/share/v2ray" "$PROG" validate -c "$config_file"|' "$DAE_INIT"
|
||||
sed -i 's|procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray".*|procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"|' "$DAE_INIT"
|
||||
fi
|
||||
|
||||
|
||||
@@ -271,6 +271,7 @@ generate() {
|
||||
echo " routing {"
|
||||
echo " request {"
|
||||
[ "$block_ads" = "1" ] && echo " qname(geosite:category-ads-all) -> reject"
|
||||
echo " qname(suffix:googleapis.cn) -> fallbackdns"
|
||||
echo " qname(geosite:cn) -> cndns"
|
||||
echo " fallback: fallbackdns"
|
||||
echo " }"
|
||||
@@ -295,6 +296,7 @@ generate() {
|
||||
# OS connectivity checks direct (avoid captive-portal false positives)
|
||||
echo " domain(connectivitycheck.gstatic.com) -> direct"
|
||||
echo " domain(msftconnecttest.com) -> direct"
|
||||
echo " domain(suffix:googleapis.cn) -> ${fallback}"
|
||||
if [ "$cn_direct" = "1" ]; then
|
||||
echo " dip(geoip:cn) -> direct"
|
||||
echo " domain(geosite:cn) -> direct"
|
||||
@@ -308,7 +310,7 @@ generate() {
|
||||
|
||||
# overwrite live config only if validation passes
|
||||
local out
|
||||
out="$("$DAE_BIN" validate -c "$TMP_GEN" 2>&1)"
|
||||
out="$(DAE_LOCATION_ASSET="/usr/share/v2ray" "$DAE_BIN" validate -c "$TMP_GEN" 2>&1)"
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "validate failed: $(echo "$out" | head -1)" >&2
|
||||
rm -f "$TMP_GEN"
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"/tmp/luci-app-daede.backup.log": [ "read" ],
|
||||
"/bin/pidof dae": [ "exec" ],
|
||||
"/bin/pidof daed": [ "exec" ],
|
||||
"/usr/bin/dae validate -c /tmp/dae-validate.dae": [ "exec" ],
|
||||
"/usr/bin/env DAE_LOCATION_ASSET=/usr/share/v2ray /usr/bin/dae validate -c /tmp/dae-validate.dae": [ "exec" ],
|
||||
"/usr/bin/dae --version": [ "exec" ],
|
||||
"/bin/rm -f /tmp/dae-validate.dae": [ "exec" ],
|
||||
"/usr/share/luci-app-daede/pkg-info.sh dae": [ "exec" ],
|
||||
|
||||
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.45
|
||||
PKG_RELEASE:=35
|
||||
PKG_RELEASE:=36
|
||||
|
||||
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
|
||||
@@ -9,6 +9,7 @@ LuCI **Firewall Live View** — client-side JS view polling `ubus fwlive poll` (
|
||||
| `Makefile` | `LUCI_TITLE`, `LUCI_DEPENDS`, includes `luci.mk` |
|
||||
| `htdocs/luci-static/resources/view/status/fwlive.js` | LuCI view (`view.extend`) |
|
||||
| `htdocs/luci-static/resources/fwlive/log.js` | Parser/filter module (mirror of repo `core/fwlive-log.js`) |
|
||||
| `htdocs/luci-static/resources/fwlive/proto.js` | Protocol filter pair (select + custom field; typed custom wins) |
|
||||
| `htdocs/luci-static/resources/fwlive/constants.js` | Shared view constants (`baseclass.extend` module) |
|
||||
| `htdocs/luci-static/resources/fwlive/css.js` | Inline stylesheet string (`styleText` for `E('style', …)`) |
|
||||
| `htdocs/luci-static/resources/fwlive/tint.js` | Row-tint paint helpers (`baseclass.extend` module) |
|
||||
@@ -24,10 +25,12 @@ LuCI **Firewall Live View** — client-side JS view polling `ubus fwlive poll` (
|
||||
| `root/usr/share/luci/menu.d/*.json` | Menu entry (`admin/status/fwlive`) |
|
||||
| `root/usr/share/rpcd/acl.d/*.json` | ubus ACL (read + write for logging enable/disable) |
|
||||
| `root/usr/libexec/rpcd/fwlive` | rpcd plugin (`rules`, `poll`, `resolve`, `logging_status`, `enable_wan_logging`, `disable_wan_logging`) |
|
||||
| `root/usr/libexec/fwlive-adaptive-cap.sh` | Layer 1 adaptive poll cap (sourced by `rpcd/fwlive`) |
|
||||
| `root/usr/libexec/fwlive-logging.sh` | WAN zone logging helpers |
|
||||
| `/etc/fwlive/wan-log-baseline` | Written on first **Enable logging**; restored on uninstall (`prerm`). Disable of a pre-existing/foreign log bit is not snapshotted, so uninstall will not restore that bit. |
|
||||
| `root/usr/libexec/fwlive-log-filter.sh` | Server-side firewall-only filter (`isFirewallEvent` parity) |
|
||||
| `root/usr/libexec/fwlive-is-firewall-event.sh` | Shared filter logic (sourced by filter + tests) |
|
||||
| `root/usr/libexec/fwlive-is-firewall-event.awk` | **Generated** standalone classifier from `CLASSIFY_SPEC`; loaded by `fwlive-is-firewall-event.sh` |
|
||||
| `po/templates/luci-app-fwlive.pot` | i18n template (English msgid scaffolding) |
|
||||
|
||||
No `luasrc/` — modern JS-only app.
|
||||
|
||||
@@ -16,9 +16,32 @@
|
||||
* Modules must not mutate state. host is cleared then rebuilt (idempotent replace).
|
||||
*/
|
||||
|
||||
function chipFieldLabel(spec) {
|
||||
switch (spec.key) {
|
||||
case 'q':
|
||||
return _('Search');
|
||||
case 'action':
|
||||
return _('Action');
|
||||
case 'interface':
|
||||
return _('Interface');
|
||||
case 'proto':
|
||||
return _('Proto');
|
||||
case 'src':
|
||||
return _('Source');
|
||||
case 'dst':
|
||||
return _('Destination');
|
||||
case 'sport':
|
||||
return _('Source port');
|
||||
case 'dport':
|
||||
return _('Destination port');
|
||||
default:
|
||||
return spec.label || spec.key;
|
||||
}
|
||||
}
|
||||
|
||||
function chipValueNodes(spec, val) {
|
||||
const key = spec.key;
|
||||
const label = spec.label || key;
|
||||
const label = chipFieldLabel(spec);
|
||||
const p = log.parseFilterValue(val);
|
||||
if (!p.value) return [''];
|
||||
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
* luciUrl, firewallZonesPath, firewallZonesUrl, firewallZonesLink
|
||||
*
|
||||
* Filter-aware helpers (require an onFilterClick callback):
|
||||
* filterLink, addrFilterLink, ruleAdminPath, ruleAdminLink, ifaceLink
|
||||
* filterLink, addrFilterLink, ruleAdminLink, ifaceLink
|
||||
*
|
||||
* Constant path helper:
|
||||
* ruleAdminPath
|
||||
*
|
||||
* No host element — all functions return DOM nodes or strings.
|
||||
* May require fwlive.log for formatCell and fwlive.hostname for cache reads.
|
||||
|
||||
@@ -90,7 +90,7 @@ return baseclass.extend({
|
||||
CLASSIFY_SPEC: CLASSIFY_SPEC,
|
||||
|
||||
TCP_FLAG_TAIL:
|
||||
/\b((?:SYN|ACK|FIN|RST|PSH|URG)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG))*)(?:\s+[A-Z][A-Z0-9_]*=[^\s]+)*\s*$/i,
|
||||
/\b((?:SYN|ACK|FIN|RST|PSH|URG|ECE|CWR)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG|ECE|CWR))*)(?:\s+[A-Z][A-Z0-9_]*=[^\s]+)*\s*$/i,
|
||||
NETFILTER_KV_GLUE: NETFILTER_KV_GLUE,
|
||||
|
||||
kvHas: function (msg, key) {
|
||||
@@ -105,7 +105,8 @@ return baseclass.extend({
|
||||
MAX_DATE_SECONDS: 8640000000000,
|
||||
|
||||
normalizeNetfilterMessage: function (message) {
|
||||
return (message || '').replace(this.NETFILTER_KV_GLUE, '$1 ');
|
||||
if (typeof message !== 'string') message = '';
|
||||
return message.replace(this.NETFILTER_KV_GLUE, '$1 ');
|
||||
},
|
||||
|
||||
parseKeyValueLog: function (message) {
|
||||
|
||||
@@ -69,6 +69,7 @@ function blockerCode(state) {
|
||||
blockers.indexOf('nf_log_ipv6_missing') >= 0
|
||||
)
|
||||
return 'nf_log_missing';
|
||||
if (blockers.length > 0) return 'unknown';
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -92,6 +93,63 @@ function appendLoggingNotice(host, state) {
|
||||
host.appendChild(E('span', { 'class': 'fwlive-logging-notice' }, [state.loggingNotice]));
|
||||
}
|
||||
|
||||
function appendBlockerStatus(host, blocker, st) {
|
||||
if (blocker === 'no_wan_zone') {
|
||||
host.appendChild(
|
||||
E(
|
||||
'span',
|
||||
{ 'class': 'fwlive-logging-status' },
|
||||
labelWithZoneCandidates(_('WAN logging unavailable: no WAN zone'), st)
|
||||
)
|
||||
);
|
||||
host.appendChild(links.firewallZonesLink());
|
||||
return;
|
||||
}
|
||||
|
||||
if (blocker === 'nf_log_missing') {
|
||||
host.appendChild(
|
||||
E('span', { 'class': 'fwlive-logging-status' }, [
|
||||
_('WAN logging unavailable: missing kernel log modules')
|
||||
])
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (blocker === 'unknown') {
|
||||
host.appendChild(
|
||||
E('span', { 'class': 'fwlive-logging-status' }, [_('WAN logging unavailable')])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function appendWanLogDisableControl(host, state, callbacks) {
|
||||
const st = state.loggingStatus;
|
||||
const limit = st.wan_log_limit || _('default 10/minute');
|
||||
const busy = !!state.loggingBusy;
|
||||
const children = busy
|
||||
? [_('Disabling…')]
|
||||
: [
|
||||
E('span', { 'class': 'fwlive-log-on-dot', 'aria-hidden': 'true' }, ['']),
|
||||
E('span', { 'class': 'fwlive-log-label' }, [_('WAN logging on')]),
|
||||
E('span', { 'class': 'fwlive-log-rate' }, [_('· %s').format(limit)])
|
||||
];
|
||||
host.appendChild(
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
'class': 'cbi-button fwlive-log-merged',
|
||||
'type': 'button',
|
||||
'title': _('WAN logging on (%s). Click to disable.').format(limit),
|
||||
'disabled': busy ? '' : null,
|
||||
'click': function () {
|
||||
callbacks.onDisable();
|
||||
}
|
||||
},
|
||||
children
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function renderToolbar(host, state, callbacks) {
|
||||
host.innerHTML = '';
|
||||
const st = state.loggingStatus;
|
||||
@@ -103,54 +161,15 @@ function renderToolbar(host, state, callbacks) {
|
||||
host.style.display = 'contents';
|
||||
const blocker = blockerCode(state);
|
||||
|
||||
if (blocker === 'no_wan_zone') {
|
||||
host.appendChild(
|
||||
E(
|
||||
'span',
|
||||
{ 'class': 'fwlive-logging-status' },
|
||||
labelWithZoneCandidates(_('WAN logging unavailable: no WAN zone'), st)
|
||||
)
|
||||
);
|
||||
host.appendChild(links.firewallZonesLink());
|
||||
appendLoggingNotice(host, state);
|
||||
return;
|
||||
}
|
||||
if (blocker) appendBlockerStatus(host, blocker, st);
|
||||
|
||||
if (blocker === 'nf_log_missing') {
|
||||
host.appendChild(
|
||||
E('span', { 'class': 'fwlive-logging-status' }, [
|
||||
_('WAN logging unavailable: missing kernel log modules')
|
||||
])
|
||||
);
|
||||
appendLoggingNotice(host, state);
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = st.wan_log_limit || _('default 10/minute');
|
||||
if (st.wan_log) {
|
||||
const busy = !!state.loggingBusy;
|
||||
const children = busy
|
||||
? [_('Disabling…')]
|
||||
: [
|
||||
E('span', { 'class': 'fwlive-log-on-dot', 'aria-hidden': 'true' }, ['']),
|
||||
E('span', { 'class': 'fwlive-log-label' }, [_('WAN logging on')]),
|
||||
E('span', { 'class': 'fwlive-log-rate' }, [_('· %s').format(limit)])
|
||||
];
|
||||
host.appendChild(
|
||||
E(
|
||||
'button',
|
||||
{
|
||||
'class': 'cbi-button fwlive-log-merged',
|
||||
'type': 'button',
|
||||
'title': _('WAN logging on (%s). Click to disable.').format(limit),
|
||||
'disabled': busy ? '' : null,
|
||||
'click': function () {
|
||||
callbacks.onDisable();
|
||||
}
|
||||
},
|
||||
children
|
||||
)
|
||||
);
|
||||
appendWanLogDisableControl(host, state, callbacks);
|
||||
appendLoggingNotice(host, state);
|
||||
return;
|
||||
}
|
||||
|
||||
if (blocker) {
|
||||
appendLoggingNotice(host, state);
|
||||
return;
|
||||
}
|
||||
@@ -277,6 +296,16 @@ function buildEmptyStateNodes(state, callbacks) {
|
||||
return nodes;
|
||||
}
|
||||
|
||||
if (blocker === 'unknown') {
|
||||
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [_('WAN logging unavailable')]));
|
||||
nodes.push(
|
||||
E('p', {}, [
|
||||
_('This router reported a logging blocker that Live View does not recognize yet.')
|
||||
])
|
||||
);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
if (st && st.wan_log) {
|
||||
nodes.push(E('p', { 'class': 'fwlive-empty-title' }, [_('Waiting for firewall events')]));
|
||||
nodes.push(
|
||||
|
||||
@@ -312,7 +312,8 @@ function buildExpansionRow(row, state, columns) {
|
||||
E('td', { 'colspan': String(columns.length) }, [
|
||||
E('div', { 'class': 'fwlive-msg-expand-label' }, [_('Message')]),
|
||||
E('pre', { 'class': 'fwlive-msg-expand-body' }, [
|
||||
log.formatMessageDisplay(row.message, 'wrap') || '—'
|
||||
/* oneline is uncapped; wrap ellipsizes at 240 */
|
||||
log.formatMessageDisplay(row.message, 'oneline') || '—'
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
@@ -156,6 +156,8 @@ return view.extend({
|
||||
hostnameFailed: null,
|
||||
resolveInFlight: false,
|
||||
resolveGeneration: 0,
|
||||
/* Coalesce hostname-cache paints deferred while tablePaused. */
|
||||
resolvePaintPending: false,
|
||||
lastPollError: false,
|
||||
lastRulesError: null,
|
||||
followLive: true,
|
||||
@@ -166,6 +168,7 @@ return view.extend({
|
||||
loggingStatus: null,
|
||||
loggingBusy: false,
|
||||
loggingNotice: '',
|
||||
_loggingNoticeFromToggle: false,
|
||||
/* Session-only dismiss of first-run consent (Not now without checkbox). */
|
||||
consentDismissedSession: false,
|
||||
_loggingToolbarSig: '',
|
||||
@@ -212,7 +215,7 @@ return view.extend({
|
||||
parts.push('maxraw=%s'.format(encodeURIComponent(this.manualFetchLines)));
|
||||
}
|
||||
if (this.viewMode === 'detailed') parts.push('view=detailed');
|
||||
location.hash = parts.join('&');
|
||||
history.replaceState(history.state, '', '#' + parts.join('&'));
|
||||
},
|
||||
|
||||
hashEntries() {
|
||||
@@ -739,7 +742,7 @@ return view.extend({
|
||||
const status = await callFwliveLoggingStatus();
|
||||
if (this.viewDisposed) return;
|
||||
this.loggingStatus = status;
|
||||
this.loggingNotice = '';
|
||||
if (!this._loggingNoticeFromToggle) this.loggingNotice = '';
|
||||
this.weakDevice = !!(this.loggingStatus && this.loggingStatus.weak_device === true);
|
||||
} catch (_e) {
|
||||
if (this.viewDisposed) return;
|
||||
@@ -761,17 +764,20 @@ return view.extend({
|
||||
|
||||
this.loggingBusy = true;
|
||||
this.loggingNotice = '';
|
||||
this._loggingNoticeFromToggle = false;
|
||||
opts.initialUi();
|
||||
|
||||
try {
|
||||
const res = await opts.call();
|
||||
if (!res || !res.ok) {
|
||||
this.loggingNotice = opts.failureNotice(res);
|
||||
this._loggingNoticeFromToggle = true;
|
||||
await this.loadLoggingStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
this.loggingNotice = opts.successNotice(res);
|
||||
this._loggingNoticeFromToggle = !!this.loggingNotice;
|
||||
if (opts.onSuccess) opts.onSuccess(res);
|
||||
if (this.loggingStatus && typeof opts.wanLog === 'boolean')
|
||||
this.loggingStatus = Object.assign({}, this.loggingStatus, {
|
||||
@@ -780,8 +786,11 @@ return view.extend({
|
||||
await this.loadLoggingStatus();
|
||||
} catch (_e) {
|
||||
this.loggingNotice = opts.catchNotice();
|
||||
this._loggingNoticeFromToggle = true;
|
||||
await this.loadLoggingStatus();
|
||||
} finally {
|
||||
/* Survive only the refresh owned by this toggle. */
|
||||
this._loggingNoticeFromToggle = false;
|
||||
this.loggingBusy = false;
|
||||
this.updateEmptyStateUi();
|
||||
this.updateLoggingToolbarUi();
|
||||
@@ -803,6 +812,14 @@ return view.extend({
|
||||
return _(
|
||||
'Another change is staged for the firewall; apply or revert it first.'
|
||||
);
|
||||
if (res && res.error === 'no_wan_zone')
|
||||
return _('No WAN zone found; cannot toggle logging without one.');
|
||||
if (res && res.error === 'lock_failed')
|
||||
return _('Could not acquire the logging lock.');
|
||||
if (res && res.error === 'baseline_snapshot_failed')
|
||||
return _('Could not snapshot the current logging state.');
|
||||
if (res && res.error === 'firewall_reload_failed')
|
||||
return _('The firewall did not reload; saved and live logging may differ.');
|
||||
return _('Could not enable logging.');
|
||||
},
|
||||
successNotice: (res) =>
|
||||
@@ -826,6 +843,12 @@ return view.extend({
|
||||
return _(
|
||||
'Another change is staged for the firewall; apply or revert it first.'
|
||||
);
|
||||
if (res && res.error === 'no_wan_zone')
|
||||
return _('No WAN zone found; cannot toggle logging without one.');
|
||||
if (res && res.error === 'lock_failed')
|
||||
return _('Could not acquire the logging lock.');
|
||||
if (res && res.error === 'firewall_reload_failed')
|
||||
return _('The firewall did not reload; saved and live logging may differ.');
|
||||
return _('Could not disable logging.');
|
||||
},
|
||||
successNotice: (res) => (res.changed ? _('WAN drop/reject logging is off.') : ''),
|
||||
@@ -1363,23 +1386,28 @@ return view.extend({
|
||||
this.updateSummaryUi();
|
||||
},
|
||||
|
||||
updateSummaryUi() {
|
||||
const card = document.getElementById('fwlive-summary');
|
||||
syncEmptyScrollVisibility(rowCount) {
|
||||
const hideTable = this.summaryMode && !this.summaryRowsShown;
|
||||
const scroll = document.getElementById('fwlive-scroll');
|
||||
const empty = document.getElementById('fwlive-empty');
|
||||
if (scroll) {
|
||||
if (!scroll.style) scroll.style = { display: '' };
|
||||
scroll.style.display = hideTable ? 'none' : '';
|
||||
}
|
||||
if (empty) {
|
||||
if (!empty.style) empty.style = { display: '' };
|
||||
empty.style.display = hideTable ? 'none' : rowCount ? 'none' : 'block';
|
||||
}
|
||||
},
|
||||
|
||||
updateSummaryUi() {
|
||||
const card = document.getElementById('fwlive-summary');
|
||||
const toggle = document.getElementById('fwlive-summary-rows');
|
||||
if (card) {
|
||||
if (!card.style) card.style = { display: '' };
|
||||
card.style.display = this.summaryMode ? 'block' : 'none';
|
||||
}
|
||||
if (scroll) {
|
||||
if (!scroll.style) scroll.style = { display: '' };
|
||||
scroll.style.display = this.summaryMode && !this.summaryRowsShown ? 'none' : '';
|
||||
}
|
||||
if (empty) {
|
||||
if (!empty.style) empty.style = { display: '' };
|
||||
empty.style.display = this.summaryMode && !this.summaryRowsShown ? 'none' : '';
|
||||
}
|
||||
this.syncEmptyScrollVisibility(this.filteredRows().length);
|
||||
if (toggle) {
|
||||
toggle.textContent = this.summaryRowsShown ? _('Hide rows') : _('Show rows');
|
||||
toggle.setAttribute('aria-pressed', this.summaryRowsShown ? 'true' : 'false');
|
||||
@@ -1400,13 +1428,12 @@ return view.extend({
|
||||
this.summaryData = null;
|
||||
this.updateSummaryUi();
|
||||
if (this.tablePaused) {
|
||||
const empty = document.getElementById('fwlive-empty');
|
||||
if (empty) {
|
||||
const rows = this.filteredRows();
|
||||
empty.style.display = rows.length ? 'none' : 'block';
|
||||
}
|
||||
this.syncEmptyScrollVisibility(this.filteredRows().length);
|
||||
this.updateStatus();
|
||||
} else this.renderRows(true);
|
||||
} else {
|
||||
this.resolvePaintPending = false;
|
||||
this.renderRows(true);
|
||||
}
|
||||
},
|
||||
|
||||
onSummaryRowsToggle() {
|
||||
@@ -1420,6 +1447,16 @@ return view.extend({
|
||||
this.ensureRenderScheduler().schedule(!!force);
|
||||
},
|
||||
|
||||
scheduleResolvePaint() {
|
||||
if (this.tablePaused) {
|
||||
this.resolvePaintPending = true;
|
||||
this.updateStatus();
|
||||
return;
|
||||
}
|
||||
this.resolvePaintPending = false;
|
||||
this.scheduleRenderRows(true);
|
||||
},
|
||||
|
||||
updateFloodBanner() {
|
||||
const el = document.getElementById('fwlive-flood');
|
||||
if (!el) return;
|
||||
@@ -1601,7 +1638,10 @@ return view.extend({
|
||||
this.requestPoll()
|
||||
.then(() => {
|
||||
/* A hide/show bump abandons this epoch; the catch-up poll paints. */
|
||||
if (epoch === this.currentPollEpoch()) this.renderRows(true);
|
||||
if (epoch === this.currentPollEpoch()) {
|
||||
this.resolvePaintPending = false;
|
||||
this.renderRows(true);
|
||||
}
|
||||
})
|
||||
.catch(function () {});
|
||||
}
|
||||
@@ -1767,7 +1807,7 @@ return view.extend({
|
||||
}
|
||||
|
||||
this.updateAdaptiveBanner();
|
||||
if (updated) this.scheduleRenderRows(true);
|
||||
if (updated) this.scheduleResolvePaint();
|
||||
} catch (_e) {
|
||||
/* resolve unavailable — show IPs */
|
||||
} finally {
|
||||
@@ -1894,7 +1934,6 @@ return view.extend({
|
||||
if (!el || typeof el.querySelector !== 'function') return;
|
||||
|
||||
const body = el.querySelector('tbody');
|
||||
const empty = document.getElementById('fwlive-empty');
|
||||
const scroll = document.getElementById('fwlive-scroll');
|
||||
this.updateHash(this.readFilters());
|
||||
|
||||
@@ -1908,6 +1947,7 @@ return view.extend({
|
||||
|
||||
if (!paint) {
|
||||
this.updateFloodBanner();
|
||||
this.syncEmptyScrollVisibility(rows.length);
|
||||
this.updateStatus(rows);
|
||||
return;
|
||||
}
|
||||
@@ -1916,7 +1956,7 @@ return view.extend({
|
||||
|
||||
const prevScroll = scroll ? scroll.scrollTop : 0;
|
||||
|
||||
if (empty) empty.style.display = rows.length ? 'none' : 'block';
|
||||
this.syncEmptyScrollVisibility(rows.length);
|
||||
this.updateStatus(rows);
|
||||
this.renderFilterChips();
|
||||
|
||||
@@ -2068,7 +2108,18 @@ return view.extend({
|
||||
else if (this.summaryMode) {
|
||||
this.renderSummary();
|
||||
this.updateStatus();
|
||||
} else this.scheduleRenderRows();
|
||||
if (this.summaryRowsShown) {
|
||||
const forceHostnamePaint = this.resolvePaintPending;
|
||||
this.resolvePaintPending = false;
|
||||
this.scheduleRenderRows(forceHostnamePaint);
|
||||
}
|
||||
} else {
|
||||
/* Stale resume skips renderRows(true); catch-up polls must still
|
||||
* flush coalesced hostname paints. */
|
||||
const forceHostnamePaint = this.resolvePaintPending;
|
||||
this.resolvePaintPending = false;
|
||||
this.scheduleRenderRows(forceHostnamePaint);
|
||||
}
|
||||
|
||||
try {
|
||||
await this.resolveHostnamesForEntries(this.filteredRows());
|
||||
|
||||
@@ -16,7 +16,7 @@ msgstr ""
|
||||
msgid "%d matching · %d/%d stored"
|
||||
msgstr "%d matching · %d/%d stored"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:466
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:467
|
||||
msgid "%s: %s"
|
||||
msgstr "%s: %s"
|
||||
|
||||
@@ -83,7 +83,7 @@ msgstr "Klassisch (grün/rot)"
|
||||
msgid "Classic uses green/red; Accessible uses teal/orange"
|
||||
msgstr "Klassisch verwendet grün/rot; Barrierefrei verwendet teal/orange"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:169
|
||||
msgid "Clear all"
|
||||
msgstr "Alle löschen"
|
||||
|
||||
@@ -124,6 +124,11 @@ msgstr "Häufig"
|
||||
msgid "Connection lost — retrying…"
|
||||
msgstr "Connection lost — retrying…"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:818
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:849
|
||||
msgid "Could not acquire the logging lock."
|
||||
msgstr "Die Protokollierungssperre konnte nicht erworben werden."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609
|
||||
msgid "Could not disable logging."
|
||||
msgstr "Protokollierung konnte nicht deaktiviert werden."
|
||||
@@ -132,6 +137,10 @@ msgstr "Protokollierung konnte nicht deaktiviert werden."
|
||||
msgid "Could not refresh logging status; showing the last known state."
|
||||
msgstr "Could not refresh logging status; showing the last known state."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:820
|
||||
msgid "Could not snapshot the current logging state."
|
||||
msgstr "Der aktuelle Protokollierungszustand konnte nicht gesichert werden."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573
|
||||
msgid "Could not enable logging."
|
||||
msgstr "Protokollierung konnte nicht aktiviert werden."
|
||||
@@ -167,6 +176,10 @@ msgstr "Ziel"
|
||||
msgid "Destination IP contains (! to exclude)"
|
||||
msgstr "Ziel-IP enthält (! zum AusschlieÃen)"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:36
|
||||
msgid "Destination port"
|
||||
msgstr "Ziel-Port"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692
|
||||
msgid "Destination port (! to exclude)"
|
||||
msgstr "Ziel-Port (! zum AusschlieÃen)"
|
||||
@@ -201,6 +214,11 @@ msgstr "Für eine responsive Tabelle auf einem schwachen Gerät sollte Limit auf
|
||||
msgid "Switching tabs pauses polling; returning performs one catch-up poll. Hostnames are off by default because lookups add work."
|
||||
msgstr "Beim Wechseln des Tabs wird das Polling pausiert; bei der Rückkehr erfolgt eine einmalige Aufholabfrage. Hostnamen sind standardmäßig deaktiviert, da Auflösungen zusätzliche Arbeit verursachen."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:822
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:851
|
||||
msgid "The firewall did not reload; saved and live logging may differ."
|
||||
msgstr "Die Firewall wurde nicht neu geladen; gespeicherte und aktive Protokollierung können voneinander abweichen."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2250
|
||||
msgid "The router log ring may evict older events before fwlive reads them. fwlive cannot recover evicted entries or change forwarding behavior."
|
||||
msgstr "Der Protokollring des Routers kann ältere Ereignisse verdrängen, bevor fwlive sie liest. fwlive kann verdrängte Einträge nicht wiederherstellen und ändert das Weiterleitungsverhalten nicht."
|
||||
@@ -244,7 +262,7 @@ msgstr "Enabling…"
|
||||
msgid "Exclude"
|
||||
msgstr "Ausschließen"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:105
|
||||
msgid "Exclude instead"
|
||||
msgstr "Stattdessen ausschlieÃen"
|
||||
|
||||
@@ -322,7 +340,7 @@ msgstr ""
|
||||
"If the WAN is quiet, wait for probes or use the optional ping check in "
|
||||
"Help / the enabling-logs guide."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:105
|
||||
msgid "Include instead"
|
||||
msgstr "Stattdessen einschlieÃen"
|
||||
|
||||
@@ -399,6 +417,13 @@ msgstr ""
|
||||
msgid "No WAN zone found"
|
||||
msgstr "No WAN zone found"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:816
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:847
|
||||
msgid "No WAN zone found; cannot toggle logging without one."
|
||||
msgstr ""
|
||||
"Keine WAN-Zone gefunden; Protokollierung kann ohne eine solche nicht "
|
||||
"umgeschaltet werden."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163
|
||||
msgid "Not now"
|
||||
msgstr "Not now"
|
||||
@@ -454,7 +479,7 @@ msgstr "Protokoll — häufige Werte"
|
||||
msgid "Quick search"
|
||||
msgstr "Schnellsuche"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:128
|
||||
msgid "Remove filter"
|
||||
msgstr "Filter entfernen"
|
||||
|
||||
@@ -512,6 +537,10 @@ msgstr "Server hat diese Abfrage gekürzt (adaptives Limit)."
|
||||
msgid "SPort"
|
||||
msgstr "Quell-Port"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:22
|
||||
msgid "Search"
|
||||
msgstr "Suche"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650
|
||||
msgid "Show hostnames"
|
||||
msgstr "Hostnamen anzeigen"
|
||||
@@ -532,6 +561,10 @@ msgstr "Quelle"
|
||||
msgid "Source IP contains (! to exclude)"
|
||||
msgstr "Quell-IP enthält (! zum AusschlieÃen)"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:34
|
||||
msgid "Source port"
|
||||
msgstr "Quell-Port"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690
|
||||
msgid "Source port (! to exclude)"
|
||||
msgstr "Quell-Port (! zum AusschlieÃen)"
|
||||
@@ -554,6 +587,10 @@ msgstr ""
|
||||
"Die Tabelle aktualisiert sich automatisch, wenn Ihre Firewall Verkehr "
|
||||
"protokolliert. Pause nutzen, wenn es zu schnell wird."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
|
||||
msgid "This router reported a logging blocker that Live View does not recognize yet."
|
||||
msgstr "Dieser Router hat eine Protokollierungsblockade gemeldet, die Live View noch nicht kennt."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
|
||||
msgid "Theme tint fallback"
|
||||
msgstr "Theme-Farb-Fallback"
|
||||
@@ -618,6 +655,11 @@ msgstr "WAN-Protokollierung: EIN"
|
||||
msgid "WAN logging on (%s). Click to disable."
|
||||
msgstr "WAN-Protokollierung: EIN (%s). Klicken zum Deaktivieren."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:300
|
||||
msgid "WAN logging unavailable"
|
||||
msgstr "WAN-Protokollierung nicht verfügbar"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77
|
||||
msgid "WAN logging unavailable: missing kernel log modules"
|
||||
msgstr "WAN-Protokollierung nicht verfügbar: fehlende Kernel-Log-Module"
|
||||
@@ -647,7 +689,7 @@ msgstr "allow/deny rules, LAN logging, or anything else."
|
||||
msgid "buffer full"
|
||||
msgstr "Puffer voll"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:40
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:63
|
||||
msgid "contains"
|
||||
msgstr "enthält"
|
||||
|
||||
@@ -655,7 +697,7 @@ msgstr "enthält"
|
||||
msgid "default 10/minute"
|
||||
msgstr "Standard 10/Minute"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:52
|
||||
msgid "is"
|
||||
msgstr "ist"
|
||||
|
||||
@@ -667,8 +709,8 @@ msgstr "Lade Puffer"
|
||||
msgid "loading…"
|
||||
msgstr "loading…"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:61
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:68
|
||||
msgid "not"
|
||||
msgstr "nicht"
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ msgstr ""
|
||||
msgid "%d matching · %d/%d stored"
|
||||
msgstr "%d matching · %d/%d stored"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:466
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:467
|
||||
msgid "%s: %s"
|
||||
msgstr "%s: %s"
|
||||
|
||||
@@ -88,7 +88,7 @@ msgstr ""
|
||||
"ÐлаÑÑиÑеÑкий â зелÑнÑй/кÑаÑнÑй; доÑÑÑпнÑй â "
|
||||
"биÑÑзовÑй/оÑанжевÑй"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:169
|
||||
msgid "Clear all"
|
||||
msgstr "ÐÑиÑÑиÑÑ Ð²ÑÑ"
|
||||
|
||||
@@ -129,6 +129,11 @@ msgstr "Частые"
|
||||
msgid "Connection lost — retrying…"
|
||||
msgstr "Connection lost — retrying…"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:818
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:849
|
||||
msgid "Could not acquire the logging lock."
|
||||
msgstr "Не удалось получить блокировку журналирования."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609
|
||||
msgid "Could not disable logging."
|
||||
msgstr "Ðе ÑдалоÑÑ Ð¾ÑклÑÑиÑÑ Ð¶ÑÑналиÑование."
|
||||
@@ -137,6 +142,10 @@ msgstr "Ðе ÑдалоÑÑ Ð¾ÑклÑÑиÑÑ
|
||||
msgid "Could not refresh logging status; showing the last known state."
|
||||
msgstr "Could not refresh logging status; showing the last known state."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:820
|
||||
msgid "Could not snapshot the current logging state."
|
||||
msgstr "Не удалось сохранить снимок текущего состояния журналирования."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573
|
||||
msgid "Could not enable logging."
|
||||
msgstr "Ðе ÑдалоÑÑ Ð²ÐºÐ»ÑÑиÑÑ Ð¶ÑÑналиÑование."
|
||||
@@ -169,6 +178,10 @@ msgstr "ÐазнаÑение"
|
||||
msgid "Destination IP contains (! to exclude)"
|
||||
msgstr "IP назнаÑÐµÐ½Ð¸Ñ ÑодеÑÐ¶Ð¸Ñ (! Ð´Ð»Ñ Ð¸ÑклÑÑениÑ)"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:36
|
||||
msgid "Destination port"
|
||||
msgstr "Порт назначения"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692
|
||||
msgid "Destination port (! to exclude)"
|
||||
msgstr "ÐоÑÑ Ð½Ð°Ð·Ð½Ð°ÑÐµÐ½Ð¸Ñ (! Ð´Ð»Ñ Ð¸ÑклÑÑениÑ)"
|
||||
@@ -203,6 +216,11 @@ msgstr "Для отзывчивой таблицы на слабом устро
|
||||
msgid "Switching tabs pauses polling; returning performs one catch-up poll. Hostnames are off by default because lookups add work."
|
||||
msgstr "При переключении вкладки опрос приостанавливается; при возврате выполняется один догоняющий опрос. Имена хостов по умолчанию отключены, поскольку их разрешение создаёт дополнительную нагрузку."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:822
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:851
|
||||
msgid "The firewall did not reload; saved and live logging may differ."
|
||||
msgstr "Брандмауэр не перезагрузился; сохранённые и активные настройки журналирования могут различаться."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2250
|
||||
msgid "The router log ring may evict older events before fwlive reads them. fwlive cannot recover evicted entries or change forwarding behavior."
|
||||
msgstr "Кольцевой журнал маршрутизатора может вытеснить старые события до того, как fwlive их прочитает. fwlive не может восстановить вытесненные записи и не изменяет пересылку."
|
||||
@@ -246,7 +264,7 @@ msgstr "Enabling…"
|
||||
msgid "Exclude"
|
||||
msgstr "Исключить"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:105
|
||||
msgid "Exclude instead"
|
||||
msgstr "ÐÑклÑÑиÑÑ Ð²Ð¼ÐµÑÑо ÑÑого"
|
||||
|
||||
@@ -324,7 +342,7 @@ msgstr ""
|
||||
"If the WAN is quiet, wait for probes or use the optional ping check in "
|
||||
"Help / the enabling-logs guide."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:105
|
||||
msgid "Include instead"
|
||||
msgstr "ÐклÑÑиÑÑ Ð²Ð¼ÐµÑÑо ÑÑого"
|
||||
|
||||
@@ -400,6 +418,11 @@ msgstr ""
|
||||
msgid "No WAN zone found"
|
||||
msgstr "No WAN zone found"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:816
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:847
|
||||
msgid "No WAN zone found; cannot toggle logging without one."
|
||||
msgstr "WAN-зона не найдена; без неё нельзя переключить журналирование."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163
|
||||
msgid "Not now"
|
||||
msgstr "Not now"
|
||||
@@ -455,7 +478,7 @@ msgstr "Протокол — частые значения"
|
||||
msgid "Quick search"
|
||||
msgstr "ÐÑÑÑÑÑй поиÑк"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:128
|
||||
msgid "Remove filter"
|
||||
msgstr "УдалиÑÑ ÑилÑÑÑ"
|
||||
|
||||
@@ -514,6 +537,10 @@ msgstr "Сервер сократил этот опрос (адаптивный
|
||||
msgid "SPort"
|
||||
msgstr "ÐоÑÑ Ð¸ÑÑ."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:22
|
||||
msgid "Search"
|
||||
msgstr "Поиск"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650
|
||||
msgid "Show hostnames"
|
||||
msgstr "ÐоказÑваÑÑ Ð¸Ð¼ÐµÐ½Ð° "
|
||||
@@ -534,6 +561,10 @@ msgstr "ÐÑÑоÑник"
|
||||
msgid "Source IP contains (! to exclude)"
|
||||
msgstr "IP иÑÑоÑника ÑодеÑÐ¶Ð¸Ñ (! Ð´Ð»Ñ Ð¸ÑклÑÑениÑ)"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:34
|
||||
msgid "Source port"
|
||||
msgstr "Порт источника"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690
|
||||
msgid "Source port (! to exclude)"
|
||||
msgstr "ÐоÑÑ Ð¸ÑÑоÑника (! Ð´Ð»Ñ Ð¸ÑклÑÑениÑ)"
|
||||
@@ -557,6 +588,10 @@ msgstr ""
|
||||
"ÑайÑвол пиÑÐµÑ Ð² жÑÑнал. ÐÑполÑзÑйÑе ÐаÑзÑ, еÑли "
|
||||
"ÑлиÑком бÑÑÑÑо."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
|
||||
msgid "This router reported a logging blocker that Live View does not recognize yet."
|
||||
msgstr "Этот маршрутизатор сообщил о блоке журналирования, который Live View пока не распознаёт."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
|
||||
msgid "Theme tint fallback"
|
||||
msgstr "ÐапаÑной ÑÐ²ÐµÑ ÑемÑ"
|
||||
@@ -621,6 +656,11 @@ msgstr "WAN-жÑÑнал: ÐÐÐ"
|
||||
msgid "WAN logging on (%s). Click to disable."
|
||||
msgstr "WAN-жÑÑнал: ÐÐÐ (%s). ÐажмиÑе, ÑÑÐ¾Ð±Ñ Ð¾ÑклÑÑиÑÑ."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:300
|
||||
msgid "WAN logging unavailable"
|
||||
msgstr "WAN-журналирование недоступно"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77
|
||||
msgid "WAN logging unavailable: missing kernel log modules"
|
||||
msgstr ""
|
||||
@@ -652,7 +692,7 @@ msgstr "allow/deny rules, LAN logging, or anything else."
|
||||
msgid "buffer full"
|
||||
msgstr "бÑÑÐµÑ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:40
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:63
|
||||
msgid "contains"
|
||||
msgstr "содержит"
|
||||
|
||||
@@ -660,7 +700,7 @@ msgstr "содержит"
|
||||
msgid "default 10/minute"
|
||||
msgstr "по ÑмолÑÐ°Ð½Ð¸Ñ 10/мин"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:52
|
||||
msgid "is"
|
||||
msgstr "еÑÑÑ"
|
||||
|
||||
@@ -672,8 +712,8 @@ msgstr "загÑÑзка бÑÑеÑа"
|
||||
msgid "loading…"
|
||||
msgstr "loading…"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:61
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:68
|
||||
msgid "not"
|
||||
msgstr "не"
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -16,7 +16,7 @@ msgstr ""
|
||||
msgid "%d matching · %d/%d stored"
|
||||
msgstr "%d matching · %d/%d stored"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:466
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:467
|
||||
msgid "%s: %s"
|
||||
msgstr "%s: %s"
|
||||
|
||||
@@ -77,7 +77,7 @@ msgstr "ç»"
|
||||
msgid "Classic uses green/red; Accessible uses teal/orange"
|
||||
msgstr "ç»"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:116
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:169
|
||||
msgid "Clear all"
|
||||
msgstr "æ"
|
||||
|
||||
@@ -115,6 +115,11 @@ msgstr "常用"
|
||||
msgid "Connection lost — retrying…"
|
||||
msgstr "Connection lost — retrying…"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:818
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:849
|
||||
msgid "Could not acquire the logging lock."
|
||||
msgstr "无法获取日志记录锁。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:609
|
||||
msgid "Could not disable logging."
|
||||
msgstr "æ æ³"
|
||||
@@ -123,6 +128,10 @@ msgstr "æ æ³"
|
||||
msgid "Could not refresh logging status; showing the last known state."
|
||||
msgstr "Could not refresh logging status; showing the last known state."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:820
|
||||
msgid "Could not snapshot the current logging state."
|
||||
msgstr "无法快照当前日志记录状态。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:573
|
||||
msgid "Could not enable logging."
|
||||
msgstr "æ æ³å¼å¯æ¥å¿è®°å½ã"
|
||||
@@ -155,6 +164,10 @@ msgstr "ç®æ å°å"
|
||||
msgid "Destination IP contains (! to exclude)"
|
||||
msgstr "ç®æ IP å"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:36
|
||||
msgid "Destination port"
|
||||
msgstr "目标端口"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1692
|
||||
msgid "Destination port (! to exclude)"
|
||||
msgstr "ç®æ 端å£ï¼! 表示æé¤ï¼"
|
||||
@@ -187,6 +200,11 @@ msgstr "要让弱设备上的表格保持响应,请将 Limit 保持在 250 行
|
||||
msgid "Switching tabs pauses polling; returning performs one catch-up poll. Hostnames are off by default because lookups add work."
|
||||
msgstr "切换到其他标签页会暂停轮询;返回时执行一次追赶轮询。主机名默认关闭,因为解析会增加工作量。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:822
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:851
|
||||
msgid "The firewall did not reload; saved and live logging may differ."
|
||||
msgstr "防火墙未能重新加载;已保存的日志设置与当前生效状态可能不一致。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:2250
|
||||
msgid "The router log ring may evict older events before fwlive reads them. fwlive cannot recover evicted entries or change forwarding behavior."
|
||||
msgstr "路由器日志环形缓冲区可能在 fwlive 读取前淘汰较旧事件。fwlive 无法恢复已淘汰的条目,也不会改变转发行为。"
|
||||
@@ -230,7 +248,7 @@ msgstr "Enabling…"
|
||||
msgid "Exclude"
|
||||
msgstr "排除"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:105
|
||||
msgid "Exclude instead"
|
||||
msgstr "æ¹ä¸ºæé¤"
|
||||
|
||||
@@ -305,7 +323,7 @@ msgstr ""
|
||||
"If the WAN is quiet, wait for probes or use the optional ping check in "
|
||||
"Help / the enabling-logs guide."
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:80
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:105
|
||||
msgid "Include instead"
|
||||
msgstr "æ¹ä¸ºå"
|
||||
|
||||
@@ -377,6 +395,11 @@ msgstr "在 /etc/config/firewall 中未找到 WAN 防火墙区域。请在以下
|
||||
msgid "No WAN zone found"
|
||||
msgstr "No WAN zone found"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:816
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:847
|
||||
msgid "No WAN zone found; cannot toggle logging without one."
|
||||
msgstr "未找到 WAN 区域;没有它无法切换日志记录。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:163
|
||||
msgid "Not now"
|
||||
msgstr "Not now"
|
||||
@@ -432,7 +455,7 @@ msgstr "协议 — 常用值"
|
||||
msgid "Quick search"
|
||||
msgstr "å¿«éæç´¢"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:91
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:128
|
||||
msgid "Remove filter"
|
||||
msgstr "ç§»é¤çé"
|
||||
|
||||
@@ -487,6 +510,10 @@ msgstr "服务器已截断本次轮询(自适应上限)。"
|
||||
msgid "SPort"
|
||||
msgstr "æºç«¯å£"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:22
|
||||
msgid "Search"
|
||||
msgstr "搜索"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1650
|
||||
msgid "Show hostnames"
|
||||
msgstr "æ¾ç¤ºä¸»æºå"
|
||||
@@ -507,6 +534,10 @@ msgstr "æºå°å"
|
||||
msgid "Source IP contains (! to exclude)"
|
||||
msgstr "æº IP å"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:34
|
||||
msgid "Source port"
|
||||
msgstr "源端口"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1690
|
||||
msgid "Source port (! to exclude)"
|
||||
msgstr "æºç«¯å£ï¼! 表示æé¤ï¼"
|
||||
@@ -524,6 +555,10 @@ msgid ""
|
||||
"if it moves too fast."
|
||||
msgstr "é²ç«å¢å"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:303
|
||||
msgid "This router reported a logging blocker that Live View does not recognize yet."
|
||||
msgstr "此路由器报告了 Live View 尚无法识别的日志阻止项。"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:1551
|
||||
msgid "Theme tint fallback"
|
||||
msgstr "主é¢çè²åé"
|
||||
@@ -587,6 +622,11 @@ msgstr "WAN æ¥å¿ï¼å¼"
|
||||
msgid "WAN logging on (%s). Click to disable."
|
||||
msgstr "WAN æ¥å¿ï¼å¼ï¼%sï¼ãç¹å»å¯"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:120
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:300
|
||||
msgid "WAN logging unavailable"
|
||||
msgstr "WAN 日志不可用"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:77
|
||||
msgid "WAN logging unavailable: missing kernel log modules"
|
||||
msgstr "WAN æ¥å¿ä¸å¯ç¨ï¼ç¼ºå°å"
|
||||
@@ -616,7 +656,7 @@ msgstr "allow/deny rules, LAN logging, or anything else."
|
||||
msgid "buffer full"
|
||||
msgstr "ç¼å²åºå·²æ»¡"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:40
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:63
|
||||
msgid "contains"
|
||||
msgstr "包含"
|
||||
|
||||
@@ -624,7 +664,7 @@ msgstr "包含"
|
||||
msgid "default 10/minute"
|
||||
msgstr "é»è®¤ 10 æ¡/åé"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:28
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:52
|
||||
msgid "is"
|
||||
msgstr "æ¯"
|
||||
|
||||
@@ -636,8 +676,8 @@ msgstr "æ£å¨å è½½ç¼å²åº"
|
||||
msgid "loading…"
|
||||
msgstr "loading…"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:37
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:44
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:61
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:68
|
||||
msgid "not"
|
||||
msgstr "é"
|
||||
|
||||
|
||||
@@ -406,11 +406,13 @@ maybe_snapshot_wan_log_baseline() {
|
||||
|
||||
# Restore WAN zone log from the install-time baseline (package prerm).
|
||||
# No-op when baseline is missing. Returns 1 on failure; baseline file is
|
||||
# kept until restore commits successfully.
|
||||
# kept until restore commits and the post-restore firewall reload succeeds.
|
||||
#
|
||||
# Hold the logging lock across the current-value read, equality cleanup,
|
||||
# commit, and baseline unlink — otherwise a concurrent enable can snapshot
|
||||
# the old baseline (or race the unlink) and lose the only restore value.
|
||||
# Hold the logging lock across the current-value read, equality check,
|
||||
# and commit — otherwise a concurrent enable can snapshot the old baseline
|
||||
# (or race the unlink) and lose the only restore value. Reload runs
|
||||
# without the lock (BusyBox flock has no -w); unlink after a successful
|
||||
# reload.
|
||||
restore_wan_log_baseline() {
|
||||
path="$(wan_log_baseline_path)"
|
||||
[ -f "$path" ] || return 0
|
||||
@@ -428,8 +430,18 @@ restore_wan_log_baseline() {
|
||||
fi
|
||||
current=$(wan_zone_log_value "$zone")
|
||||
if [ "${current:-}" = "${baseline:-}" ]; then
|
||||
rm -f "$path"
|
||||
# UCI already matches; live fw4 may still be stale after a
|
||||
# previous reload failure. Retry reload before dropping the
|
||||
# marker.
|
||||
if firewall_changes_pending; then
|
||||
release_wan_log_lock
|
||||
logger -t fwlive "WAN log baseline restore skipped: firewall changes pending" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
release_wan_log_lock
|
||||
if ! restore_wan_log_after_reload "$path" "$zone" "$baseline"; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
zone_json=$(json_null_or_string "$zone")
|
||||
@@ -453,9 +465,36 @@ restore_wan_log_baseline() {
|
||||
logger -t fwlive "WAN log baseline restore: commit gate failed" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
release_wan_log_lock
|
||||
if ! restore_wan_log_after_reload "$path" "$zone" "$baseline"; then
|
||||
return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Reload without the logging lock (BusyBox flock has no -w). Re-acquire
|
||||
# before unlinking so a concurrent enable cannot snapshot-skip then leave
|
||||
# UCI off the saved baseline while this path still deletes the marker.
|
||||
restore_wan_log_after_reload() {
|
||||
path="$1"
|
||||
zone="$2"
|
||||
baseline="$3"
|
||||
if ! reload_firewall; then
|
||||
logger -t fwlive "WAN log baseline restored; firewall reload failed" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
if ! acquire_wan_log_lock; then
|
||||
logger -t fwlive "WAN log baseline restore: lock unavailable after reload" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
current=$(wan_zone_log_value "$zone")
|
||||
if [ "${current:-}" != "${baseline:-}" ]; then
|
||||
release_wan_log_lock
|
||||
logger -t fwlive "WAN log baseline restore: post-reload verify raced" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
rm -f "$path"
|
||||
release_wan_log_lock
|
||||
reload_firewall || logger -t fwlive "WAN log baseline restored; firewall reload failed" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -469,6 +508,11 @@ wan_filter_log_decimal() {
|
||||
case "$_log_val" in
|
||||
''|*[!0-9]*) return 1 ;;
|
||||
esac
|
||||
# Bitmask option. Reject oversized digit runs before $(( )) so a
|
||||
# 20-digit UCI value cannot kill dash or wrap on BusyBox.
|
||||
if [ "${#_log_val}" -gt 10 ]; then
|
||||
return 1
|
||||
fi
|
||||
while [ "${_log_val#0}" != "$_log_val" ]; do
|
||||
_log_val=${_log_val#0}
|
||||
done
|
||||
@@ -698,10 +742,16 @@ restore_wan_zone_log() {
|
||||
# below must be able to undo only our rollback staging without reverting
|
||||
# the other writer's change.
|
||||
committed=$(wan_zone_log_value "$zone")
|
||||
# Fail closed if set/delete never staged. Swallowed rc plus an empty
|
||||
# changes list would make `uci commit` succeed as a no-op.
|
||||
if [ -z "$previous" ]; then
|
||||
uci -q delete "firewall.${zone}.log" 2>/dev/null || true
|
||||
if ! uci -q delete "firewall.${zone}.log" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
uci -q set "firewall.${zone}.log=${previous}" 2>/dev/null || true
|
||||
if ! uci -q set "firewall.${zone}.log=${previous}" 2>/dev/null; then
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
_staged=$(uci -q changes firewall 2>/dev/null || true)
|
||||
_foreign=$(wan_log_foreign_staged_lines "$zone" "$_staged")
|
||||
@@ -1126,6 +1176,27 @@ run_logging_selftest() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Oversized/malformed digit runs reject before arithmetic.
|
||||
# Do not assert a host-specific wrap integer.
|
||||
if wan_filter_log_decimal '12345678901234567890' >/dev/null; then
|
||||
echo 'wan_filter_log_decimal 20-digit: expected reject' >&2
|
||||
return 1
|
||||
fi
|
||||
if wan_filter_log_decimal '12a3' >/dev/null; then
|
||||
echo 'wan_filter_log_decimal malformed: expected reject' >&2
|
||||
return 1
|
||||
fi
|
||||
got=$(wan_filter_log_target_value '12345678901234567890')
|
||||
if [ "$got" != '1' ]; then
|
||||
echo "enable oversized log: expected 1 got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
got=$(wan_filter_log_clear_value '12345678901234567890')
|
||||
if [ -n "$got" ]; then
|
||||
echo "disable oversized log: expected empty got $got" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Enable/disable parity around multi-bit values.
|
||||
got=$(wan_filter_log_target_value '2')
|
||||
if [ "$got" != '3' ]; then
|
||||
|
||||
@@ -221,7 +221,7 @@ nft_dump_fields() {
|
||||
next
|
||||
print prefix "\t" comment
|
||||
}
|
||||
'
|
||||
' || return 1
|
||||
}
|
||||
|
||||
# pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
|
||||
@@ -313,10 +313,14 @@ enrich_rules_dump() {
|
||||
if [ -z "$_nft_tsv" ]; then
|
||||
_enrich_error='mktemp_failed'
|
||||
else
|
||||
nft_dump_fields <"$_rules_dump" >"$_nft_tsv"
|
||||
map_from_nft_stream labeled <"$_nft_tsv"
|
||||
if [ -z "$_map_overflow" ]; then
|
||||
map_from_nft_stream unlabeled <"$_nft_tsv"
|
||||
# awk under set -eu must not abort the plugin before JSON.
|
||||
if nft_dump_fields <"$_rules_dump" >"$_nft_tsv"; then
|
||||
map_from_nft_stream labeled <"$_nft_tsv"
|
||||
if [ -z "$_map_overflow" ]; then
|
||||
map_from_nft_stream unlabeled <"$_nft_tsv"
|
||||
fi
|
||||
else
|
||||
_enrich_error='tsv_failed'
|
||||
fi
|
||||
rm -f "$_nft_tsv"
|
||||
fi
|
||||
@@ -486,6 +490,12 @@ fetch_firewall_logs() {
|
||||
case "$out" in
|
||||
*'"error":'*) _filter_ok=0 ;;
|
||||
esac
|
||||
# Zero-exit JSON without a numeric messages_received is not a healthy
|
||||
# sample. The merge fallback of 0 must not clear hot/shed.
|
||||
case "$out" in
|
||||
*'"messages_received":'[0-9]*) ;;
|
||||
*) _filter_ok=0 ;;
|
||||
esac
|
||||
end_cs=$(fwlive_adaptive_clock_cs)
|
||||
# Uptime wrap / skew: end < start — do not record as cold.
|
||||
if [ "$_filter_ok" = 1 ] && [ "$end_cs" -ge "$start_cs" ]; then
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2026 VizzleTF
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-gitbackup
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
PKG_MAINTAINER:=VizzleTF <vizzlef@gmail.com>
|
||||
|
||||
LUCI_TITLE:=Automatic configuration backup to a Git repository
|
||||
# gitbackup itself (package/gitbackup) is where usr/sbin/gitbackup, the
|
||||
# rpcd plugin and every shell module live -- this app is client-side JS
|
||||
# only (ticket 11 acceptance criterion: "ни строки Lua, ни luci-compat, ни
|
||||
# luci-lib-ipkg -- ни в коде, ни в зависимостях") and has nothing to render
|
||||
# without it.
|
||||
LUCI_DEPENDS:=+gitbackup
|
||||
# Every byte here is POSIX shell (menu.d/acl.d JSON is not shell, but is not
|
||||
# compiled either), plain JSON and client-side JS -- not one compiled byte,
|
||||
# same reasoning as package/gitbackup's own PKGARCH:=all (spec, "Упаковка").
|
||||
# LUCI_PKGARCH already defaults to "all" via luci.mk's own
|
||||
# `LUCI_PKGARCH?=$(if $(realpath src/Makefile),,all)` (no src/Makefile here),
|
||||
# but ticket 11's acceptance criterion asks for it spelled out explicitly.
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
# This repository is a standalone third-party app, not a checkout living
|
||||
# inside the openwrt/luci monorepo (unlike e.g. applications/luci-app-ttyd,
|
||||
# which can say `include $(TOPDIR)/feeds/luci/luci.mk is two
|
||||
# directories up from it there) -- so the helper has to be referenced by
|
||||
# its real location in a buildroot that has the "luci" feed checked out,
|
||||
# exactly as spec "Упаковка" names it.
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,211 @@
|
||||
'use strict';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
|
||||
// Remote URL parsing mirrors remoteurl.sh: the Settings form reacts to it
|
||||
// live, so it cannot be an rpc round trip. Plain ES5 for the buildbot's jsmin.c.
|
||||
//
|
||||
// Every E() child is passed as an ARRAY: LuCI's dom.append() turns array
|
||||
// items into text nodes but assigns a bare string to innerHTML, and diff
|
||||
// bodies, commit subjects and error reasons come off the backup remote.
|
||||
// tests/dom_text_children.test.mjs enforces this in every view.
|
||||
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
method: 'status'
|
||||
});
|
||||
|
||||
var callHistory = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
method: 'history',
|
||||
params: [ 'limit' ]
|
||||
});
|
||||
|
||||
var callTest = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
method: 'test'
|
||||
});
|
||||
|
||||
// Same three forms as gb_parse_url: https, ssh:// and scp-like.
|
||||
function gbParseRemote(url) {
|
||||
var m;
|
||||
|
||||
url = url || '';
|
||||
|
||||
m = url.match(/^https:\/\/([^/:]+)(?::\d+)?\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
||||
if (!m)
|
||||
m = url.match(/^ssh:\/\/(?:[^@/]+@)?([^/:]+)(?::\d+)?\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
||||
if (!m)
|
||||
m = url.match(/^(?:[^@/:]+@)?([^/:]+):([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
|
||||
|
||||
return m ? { host: m[1], owner: m[2], repo: m[3] } : null;
|
||||
}
|
||||
|
||||
function gbProvider(url, option) {
|
||||
var parsed;
|
||||
|
||||
if (option && option !== 'auto')
|
||||
return option;
|
||||
|
||||
parsed = gbParseRemote(url);
|
||||
switch (parsed ? parsed.host : '') {
|
||||
case 'github.com':
|
||||
return 'github';
|
||||
case 'gitlab.com':
|
||||
return 'gitlab';
|
||||
case 'bitbucket.org':
|
||||
return 'bitbucket';
|
||||
case 'codeberg.org':
|
||||
return 'gitea';
|
||||
default:
|
||||
return 'generic';
|
||||
}
|
||||
}
|
||||
|
||||
function gbWebBase(parsed, provider) {
|
||||
switch (provider) {
|
||||
case 'github':
|
||||
return 'https://github.com/' + parsed.owner + '/' + parsed.repo;
|
||||
case 'bitbucket':
|
||||
return 'https://bitbucket.org/' + parsed.owner + '/' + parsed.repo;
|
||||
case 'gitlab':
|
||||
case 'gitea':
|
||||
return 'https://' + parsed.host + '/' + parsed.owner + '/' + parsed.repo;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
var GB_DEPLOY_KEY_PATH = {
|
||||
github: '/settings/keys/new',
|
||||
gitlab: '/-/settings/repository',
|
||||
gitea: '/settings/keys',
|
||||
bitbucket: '/admin/access-keys/'
|
||||
};
|
||||
|
||||
var GB_COMMIT_PATH = {
|
||||
github: '/commit/',
|
||||
gitlab: '/-/commit/',
|
||||
gitea: '/commit/',
|
||||
bitbucket: '/commits/'
|
||||
};
|
||||
|
||||
function gbDeployKeyLink(url, option) {
|
||||
var parsed = gbParseRemote(url);
|
||||
var provider = gbProvider(url, option);
|
||||
var base;
|
||||
|
||||
if (!parsed)
|
||||
return { kind: 'unparsed' };
|
||||
|
||||
base = gbWebBase(parsed, provider);
|
||||
return base ?
|
||||
{ kind: 'link', url: base + GB_DEPLOY_KEY_PATH[provider] } :
|
||||
{ kind: 'generic', host: parsed.host };
|
||||
}
|
||||
|
||||
function gbCommitUrl(url, option, sha) {
|
||||
var parsed = gbParseRemote(url);
|
||||
var provider = gbProvider(url, option);
|
||||
var base = (parsed && sha) ? gbWebBase(parsed, provider) : null;
|
||||
|
||||
return base ? base + GB_COMMIT_PATH[provider] + sha : null;
|
||||
}
|
||||
|
||||
var GB_CSS = [
|
||||
'.gitbackup-view { container-type: inline-size; }',
|
||||
'.gitbackup-view [hidden] { display: none; }',
|
||||
'.gitbackup-caption, .gitbackup-card-hint, .gitbackup-hint { font-size: .9em; color: var(--text-color-medium, #666); }',
|
||||
'.gitbackup-hint-ok { color: var(--success-color-high, #2e7d32); }',
|
||||
'.gitbackup-hint-warn { color: var(--warn-color-high, #b45f06); }',
|
||||
'.gitbackup-hint-error { color: var(--error-color-high, #c62828); }',
|
||||
'.gitbackup-actions { display: flex; flex-wrap: wrap; gap: .5em; margin: .75em 0; align-items: center; }',
|
||||
'.gitbackup-log { max-height: 320px; overflow: auto; background: var(--background-color-low, #f5f5f5); color: var(--text-color-high, #333); border: 1px solid var(--background-color-medium, #ddd); border-radius: 4px; padding: .6em .8em; font-family: monospace; font-size: .85em; white-space: pre-wrap; }',
|
||||
'.gitbackup-leak-list { margin: .3em 0; padding-left: 1.2em; color: var(--text-color-high, #333); }',
|
||||
'.gitbackup-diff { max-height: 420px; overflow: auto; background: var(--background-color-low, #f5f5f5); border: 1px solid var(--background-color-medium, #ddd); border-radius: 4px; padding: .6em .8em; font-family: monospace; font-size: .85em; white-space: pre-wrap; }',
|
||||
'.gitbackup-diffline-add { display: block; color: var(--success-color-high, #2e7d32); }',
|
||||
'.gitbackup-diffline-del { display: block; color: var(--error-color-high, #c62828); }',
|
||||
'.gitbackup-diffline-change { display: block; color: var(--warn-color-high, #b45f06); }',
|
||||
'.gitbackup-diffline-hunk { display: block; color: var(--text-color-medium, #666); }',
|
||||
'.gitbackup-diffline-meta { display: block; font-weight: bold; color: var(--text-color-medium, #666); }',
|
||||
'.gitbackup-diffline-ctx { display: block; color: var(--text-color-high, #333); }'
|
||||
];
|
||||
|
||||
function gbUnifiedLineClass(line) {
|
||||
if (line.indexOf('+++') === 0 || line.indexOf('---') === 0)
|
||||
return 'gitbackup-diffline-meta';
|
||||
if (line.charAt(0) === '+')
|
||||
return 'gitbackup-diffline-add';
|
||||
if (line.charAt(0) === '-')
|
||||
return 'gitbackup-diffline-del';
|
||||
if (line.indexOf('@@') === 0)
|
||||
return 'gitbackup-diffline-hunk';
|
||||
if (line.indexOf('diff --git') === 0 || line.indexOf('index ') === 0)
|
||||
return 'gitbackup-diffline-meta';
|
||||
return 'gitbackup-diffline-ctx';
|
||||
}
|
||||
|
||||
function gbManifestLineClass(line) {
|
||||
if (line.indexOf('+ ') === 0)
|
||||
return 'gitbackup-diffline-add';
|
||||
if (line.indexOf('- ') === 0)
|
||||
return 'gitbackup-diffline-del';
|
||||
if (line.indexOf('~ ') === 0)
|
||||
return 'gitbackup-diffline-change';
|
||||
return 'gitbackup-diffline-ctx';
|
||||
}
|
||||
|
||||
function gbBuildPre(text, classify) {
|
||||
if (!text)
|
||||
return E('p', { 'class': 'gitbackup-card-hint' }, [ _('No differences.') ]);
|
||||
|
||||
return E('pre', { 'class': 'gitbackup-diff' }, text.split('\n').map(function(line) {
|
||||
return E('span', { 'class': classify(line) }, [ line + '\n' ]);
|
||||
}));
|
||||
}
|
||||
|
||||
return L.Class.extend({
|
||||
callStatus: callStatus,
|
||||
callHistory: callHistory,
|
||||
callTest: callTest,
|
||||
|
||||
css: GB_CSS,
|
||||
|
||||
provider: gbProvider,
|
||||
|
||||
deployKeyLink: gbDeployKeyLink,
|
||||
commitUrl: gbCommitUrl,
|
||||
|
||||
commitTime: function(subject) {
|
||||
var m = (subject || '').match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2})/);
|
||||
return m ? m[1] : null;
|
||||
},
|
||||
|
||||
leakList: function() {
|
||||
return E('ul', { 'class': 'gitbackup-leak-list' }, [
|
||||
E('li', {}, [ '/etc/shadow' ]),
|
||||
E('li', {}, [ _('dropbear private host keys') ]),
|
||||
E('li', {}, [ 'authorized_keys' ]),
|
||||
E('li', {}, [ _('WPA pre-shared keys (Wi-Fi passwords)') ]),
|
||||
E('li', {}, [ _('WireGuard private keys') ]),
|
||||
E('li', {}, [ _('PPPoE credentials') ]),
|
||||
E('li', {}, [ 'uhttpd.key' ])
|
||||
]);
|
||||
},
|
||||
|
||||
// ' ' text nodes between buttons match LuCI's own modal spacing.
|
||||
modalButtons: function(dismissLabel, actions) {
|
||||
var children = [ E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, [ dismissLabel || _('Close') ]) ];
|
||||
|
||||
(actions || []).forEach(function(a) { children.push(' ', a); });
|
||||
return E('div', { 'class': 'right' }, children);
|
||||
},
|
||||
|
||||
unified: function(text) {
|
||||
return gbBuildPre(text, gbUnifiedLineClass);
|
||||
},
|
||||
|
||||
manifest: function(text) {
|
||||
return gbBuildPre(text, gbManifestLineClass);
|
||||
}
|
||||
});
|
||||
@@ -1,142 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
// gitbackup -- shared diff viewer (ticket 23, spec "Не пиши второй
|
||||
// просмотрщик diff"). Before this file existed, history.js was the only
|
||||
// place in the project that rendered a diff into a colored <pre>; ticket 23
|
||||
// adds a second place that needs one (Overview's "Configuration" card, R124/
|
||||
// R125's own config_diff indicator, which used to show only the bare
|
||||
// differs/does-not-differ flag and threw the actual text away). A second,
|
||||
// separately-typed copy of history.js's own gbDiffLineClass/gbBuildDiffPre
|
||||
// would be exactly the "третья реализация" ticket 23 explicitly forbids, so
|
||||
// both call sites use this file instead.
|
||||
//
|
||||
// This is deliberately TWO renderers, not one that auto-detects its input,
|
||||
// because the two rpcd methods that feed them answer genuinely different
|
||||
// questions in genuinely different formats -- collapsing them into one
|
||||
// "smart" parser would have to guess which format it was looking at from
|
||||
// the text alone, and a manifest line that happened to start with "+" or
|
||||
// "-" (this format's own add/remove marker) would be indistinguishable from
|
||||
// a coincidence:
|
||||
//
|
||||
// unified(text) -- for `diff` (gbrpc_diff, two already-committed shas,
|
||||
// History's per-commit view): plain `git diff --end-of-options <a> <b>`
|
||||
// output (usr/sbin/gitbackup's cmd_diff, its two-argument form) -- real
|
||||
// unified-diff syntax, "diff --git a/X b/X" headers, "@@ ... @@" hunks,
|
||||
// body lines prefixed ' '/'+'/'-'.
|
||||
//
|
||||
// manifest(text) -- for `config_diff` (gbrpc_config_diff, no params,
|
||||
// Overview's live-drift check): cmd_diff's OTHER, zero-argument form,
|
||||
// which never calls `git diff` at all -- it walks both manifests'
|
||||
// entries field by field (_gb_diff_compare/_gb_diff_describe_change,
|
||||
// usr/sbin/gitbackup) and prints one line per changed path: "+ path
|
||||
// (type)" (added), "- path (type)" (removed), or "~ path: field
|
||||
// a->b; ..." (changed) -- the only way this project can show a chmod/
|
||||
// chown a real `git diff` never sees at all (same blob, same tree entry
|
||||
// mode class either way; interfaces.md, ticket 17/D03).
|
||||
//
|
||||
// No content goes through innerHTML anywhere below -- every line is its own
|
||||
// text node built through E(), the same "чужие данные никогда не
|
||||
// становятся разметкой" rule history.js's own diff viewer already followed
|
||||
// (that file's own header comment, out of this ticket's zone, read before
|
||||
// writing this).
|
||||
//
|
||||
// That holds only because every child below is handed to E() INSIDE AN
|
||||
// ARRAY, and this file originally got that wrong. LuCI's dom.append()
|
||||
// branches on the shape of its `children` argument: the Array branch is the
|
||||
// one that runs each string through document.createTextNode(), while a bare
|
||||
// string falls all the way through to `node.innerHTML = '' + children` and
|
||||
// is parsed as markup. `E('span', attrs, line)` therefore rendered a diff
|
||||
// line AS HTML -- and the text here is `git diff` output, i.e. the contents
|
||||
// of files fetched from the backup remote, which anyone with push access to
|
||||
// that repository controls byte for byte. `E('span', attrs, [ line ])` is
|
||||
// the fix, and the bracket is load-bearing, not punctuation: nothing else
|
||||
// escapes on the way here (gb_json_esc escapes only what JSON needs, LuCI's
|
||||
// String.format('%s') does not escape at all, and a LuCI page carries no
|
||||
// CSP). tests/dom_text_children.test.mjs holds the whole view directory to
|
||||
// this, so a future call site cannot quietly drop the brackets again.
|
||||
|
||||
// css -- the class names both renderers below emit, meant to be spliced
|
||||
// into each CALLING view's own <style> block (rule 1 of the style guide:
|
||||
// every view returns its styles inside its own tree, never through
|
||||
// document.head.appendChild -- a shared module has no tree of its own to
|
||||
// return one in). A plain exported array of rule strings, not a <style>
|
||||
// this file injects itself, keeps that rule intact while still giving both
|
||||
// call sites the exact same rules instead of two hand-copied approximations
|
||||
// of them.
|
||||
var GB_DIFFVIEW_CSS = [
|
||||
'.gitbackup-diff { max-height: 420px; overflow: auto; background: var(--background-color-low, #f5f5f5); border: 1px solid var(--background-color-medium, #ddd); border-radius: 4px; padding: .6em .8em; font-family: monospace; font-size: .85em; white-space: pre-wrap; }',
|
||||
'.gitbackup-diffline-add { display: block; color: var(--success-color-high, #2e7d32); }',
|
||||
'.gitbackup-diffline-del { display: block; color: var(--error-color-high, #c62828); }',
|
||||
'.gitbackup-diffline-change { display: block; color: var(--warn-color-high, #b45f06); }',
|
||||
'.gitbackup-diffline-hunk { display: block; color: var(--text-color-medium, #666); }',
|
||||
'.gitbackup-diffline-meta { display: block; font-weight: bold; color: var(--text-color-medium, #666); }',
|
||||
'.gitbackup-diffline-ctx { display: block; color: var(--text-color-high, #333); }'
|
||||
];
|
||||
|
||||
// gbUnifiedLineClass <line> -- one prefixed class per line of a real `git
|
||||
// diff` (spec: "Diff-viewer -- свой <pre> с префиксованными классами и
|
||||
// токенами --success-*/--error-*. Никаких внешних библиотек подсветки").
|
||||
// Moved here byte-for-byte from history.js's own gbDiffLineClass.
|
||||
function gbUnifiedLineClass(line) {
|
||||
if (line.indexOf('+++') === 0 || line.indexOf('---') === 0)
|
||||
return 'gitbackup-diffline-meta';
|
||||
if (line.charAt(0) === '+')
|
||||
return 'gitbackup-diffline-add';
|
||||
if (line.charAt(0) === '-')
|
||||
return 'gitbackup-diffline-del';
|
||||
if (line.indexOf('@@') === 0)
|
||||
return 'gitbackup-diffline-hunk';
|
||||
if (line.indexOf('diff --git') === 0 || line.indexOf('index ') === 0)
|
||||
return 'gitbackup-diffline-meta';
|
||||
return 'gitbackup-diffline-ctx';
|
||||
}
|
||||
|
||||
// gbManifestLineClass <line> -- one prefixed class per line of cmd_diff's
|
||||
// zero-argument, manifest-field form: "+ "/"- " mark a whole path added or
|
||||
// removed (green/red, same coloring a real diff's own +/- lines get),
|
||||
// "~ " marks a path whose fields changed in place (warn, since it is
|
||||
// neither a pure addition nor a pure removal -- e.g. a chmod), anything
|
||||
// else (there should be nothing else) falls back to plain context styling.
|
||||
function gbManifestLineClass(line) {
|
||||
if (line.indexOf('+ ') === 0)
|
||||
return 'gitbackup-diffline-add';
|
||||
if (line.indexOf('- ') === 0)
|
||||
return 'gitbackup-diffline-del';
|
||||
if (line.indexOf('~ ') === 0)
|
||||
return 'gitbackup-diffline-change';
|
||||
return 'gitbackup-diffline-ctx';
|
||||
}
|
||||
|
||||
// gbBuildPre <text> <classify> <emptyText> -- shared DOM builder: an empty
|
||||
// or blank <text> renders <emptyText> as a plain hint paragraph (spec: "diff
|
||||
// на неизменившейся системе печатает пустой результат" -- both cmd_diff
|
||||
// shapes agree on this), anything else becomes one <span> text node per
|
||||
// line inside a scrollable <pre class="gitbackup-diff">, classified by
|
||||
// <classify>.
|
||||
function gbBuildPre(text, classify, emptyText) {
|
||||
var lines = (text || '').split('\n');
|
||||
|
||||
if (!lines.length || (lines.length === 1 && !lines[0]))
|
||||
return E('p', { 'class': 'gitbackup-card-hint' }, [ emptyText ]);
|
||||
|
||||
return E('pre', { 'class': 'gitbackup-diff' }, lines.map(function(line) {
|
||||
return E('span', { 'class': classify(line) }, [ line + '\n' ]);
|
||||
}));
|
||||
}
|
||||
|
||||
return L.Class.extend({
|
||||
css: GB_DIFFVIEW_CSS,
|
||||
|
||||
// unified(text) -- History's per-commit diff (two already-committed
|
||||
// shas): real `git diff` output.
|
||||
unified: function(text) {
|
||||
return gbBuildPre(text, gbUnifiedLineClass, _('No differences.'));
|
||||
},
|
||||
|
||||
// manifest(text) -- Overview's live-drift diff (config_diff): cmd_diff's
|
||||
// own "+ path (type)" / "- path (type)" / "~ path: field a->b; ..."
|
||||
// lines.
|
||||
manifest: function(text) {
|
||||
return gbBuildPre(text, gbManifestLineClass, _('No differences.'));
|
||||
}
|
||||
});
|
||||
@@ -2,50 +2,10 @@
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require view.gitbackup.diffview as diffview';
|
||||
'require view.gitbackup.common as common';
|
||||
'require view.gitbackup.oplog as oplog';
|
||||
/* global diffview, oplog */
|
||||
// See overview.js's own identical comment: the project's eslint config
|
||||
// (openwrt/luci's own) only knows a fixed list of stock LuCI module names
|
||||
// as globals, not this app's own diffview.js/oplog.js aliases.
|
||||
|
||||
// gitbackup -- History (ticket 14, spec "Решения по реализации -> LuCI ->
|
||||
// History", "Формат коммита", "Восстановление"). Client-side JS only,
|
||||
// luci-base only, same jsmin-conservative style (plain ES5, `var`/
|
||||
// `function`, no let/const/arrow/template literals, every statement
|
||||
// semicolon-terminated) as overview.js (ticket 11) and paths.js (ticket 13),
|
||||
// both read before writing this file -- their style/rpcd/poller-cleanup
|
||||
// patterns are reused here rather than reinvented.
|
||||
//
|
||||
// Brief, verbatim: "History -- последние N коммитов своей ветки -- дата из
|
||||
// subject, SHA, список изменившихся путей. Клик -> diff в ui.showModal.
|
||||
// Кнопка Restore с двойным подтверждением: первый экран показывает список
|
||||
// перезаписываемых файлов, второй требует ввести имя устройства; если
|
||||
// board.json выбранного коммита не совпадает с текущим, на первом же
|
||||
// экране -- красное предупреждение о том, что имена интерфейсов и wireless
|
||||
// не подойдут."
|
||||
//
|
||||
// ---------------------------------------------------------------------
|
||||
// rpcd bindings -- copied from root/usr/share/rpcd/acl.d/luci-app-
|
||||
// gitbackup.json and usr/libexec/rpcd/luci.gitbackup's own gbrpc_*
|
||||
// handlers (package/gitbackup, out of this ticket's zone). This view adds
|
||||
// no rpcd method of its own: `status` (device name for the restore
|
||||
// confirmation gate), `history` (the commit list), `diff` (two already-
|
||||
// committed shas -- interfaces.md, ticket 13's own "Чем пользоваться":
|
||||
// "diff -- твоё") and `restore` (write tier) are the whole surface it
|
||||
// needs.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
var callStatus = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
method: 'status'
|
||||
});
|
||||
|
||||
var callHistory = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
method: 'history',
|
||||
params: [ 'limit' ]
|
||||
});
|
||||
/* global common, oplog */
|
||||
// openwrt/luci's eslint config only knows stock module names as globals.
|
||||
|
||||
var callDiff = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
@@ -53,52 +13,16 @@ var callDiff = rpc.declare({
|
||||
params: [ 'from', 'to' ]
|
||||
});
|
||||
|
||||
var callLog = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
method: 'log',
|
||||
params: [ 'lines' ]
|
||||
});
|
||||
|
||||
var callRestore = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
method: 'restore',
|
||||
params: [ 'device', 'commit', 'dry_run', 'force', 'with_packages' ]
|
||||
});
|
||||
|
||||
// "Последние N коммитов" (brief, verbatim) -- one fixed page size, not a
|
||||
// user-adjustable control: the brief never asks for pagination, and
|
||||
// `history` already runs a real `git fetch` per call (interfaces.md,
|
||||
// ticket 11-13: "Долгие вызовы (config_diff, history) не вешаются на
|
||||
// первичную загрузку страницы"), so a bigger N only means a slower single
|
||||
// load, not a cheaper one.
|
||||
// Every `history` call is a real git fetch.
|
||||
var GB_HISTORY_LIMIT = 30;
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Pure helpers -- no DOM access, easy to reason about and to keep in sync
|
||||
// with the shell side by inspection.
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// gbCommitTime <subject> -- the leading "YYYY-MM-DD HH:MM" a backup
|
||||
// commit's subject always starts with (spec "Формат коммита", G03: "по
|
||||
// списку коммитов в вебе видно, когда снят каждый, без открывания") --
|
||||
// same extraction overview.js's own gbCommitTime already does for the
|
||||
// single most recent commit; duplicated here rather than shared because
|
||||
// each view here is its own self-contained file, same convention
|
||||
// paths.js's own private GB_CSS/gbSequentialMap already follow.
|
||||
function gbCommitTime(subject) {
|
||||
var m = (subject || '').match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2})/);
|
||||
return m ? m[1] : null;
|
||||
}
|
||||
|
||||
// gbShortenRepoPath <path> -- a path as it appears in this device's own
|
||||
// branch tree (e.g. "devices/r1/files/etc/config/network",
|
||||
// "devices/r1/meta/board.json", "devices/r1/manifest.json") rewritten to
|
||||
// what an operator actually recognizes: the real absolute path under
|
||||
// "files/", or a short "meta/..." / "manifest.json" label for anything
|
||||
// else. The device-specific "devices/<id>/" prefix itself is never
|
||||
// hard-coded here -- collect.sh's own `path_prefix` (gitbackup.main.
|
||||
// path_prefix) can be a custom template, so this looks for the "/files/"
|
||||
// or "/meta/" marker instead of assuming a fixed prefix shape.
|
||||
// path_prefix is a template, so look for the marker, not a fixed prefix.
|
||||
function gbShortenRepoPath(path) {
|
||||
var idx = path.indexOf('/files/');
|
||||
if (idx !== -1)
|
||||
@@ -114,12 +38,6 @@ function gbShortenRepoPath(path) {
|
||||
return path;
|
||||
}
|
||||
|
||||
// gbSummarizeList <items[]> <max> -- "a, b, c" or, past <max> entries,
|
||||
// "a, b, c and N more" -- the same "до трёх... больше трёх -- и ещё N"
|
||||
// convention the commit subject itself already uses (spec "Формат
|
||||
// коммита"), applied here to `changed[]` (history's own full per-commit
|
||||
// path list, which for a device's very first backup commit is every file
|
||||
// it has -- unabridged, unlike the subject's own three-item cap).
|
||||
function gbSummarizeList(items, max) {
|
||||
if (!items.length)
|
||||
return '';
|
||||
@@ -128,21 +46,6 @@ function gbSummarizeList(items, max) {
|
||||
return items.slice(0, max).join(', ') + ' ' + _('and %d more').format(items.length - max);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Diff parsing -- `diff` (gbrpc_diff) hands back plain `git diff <a> <b>`
|
||||
// text, unrestricted (interfaces.md: "diff принимает два уже закоммиченных
|
||||
// sha... и даёт git diff между ними"). Everything below reads that text
|
||||
// only; no assumption is made beyond git's own documented unified-diff
|
||||
// output shape (`diff --git a/X b/X`, optional "new file mode"/"deleted
|
||||
// file mode", "--- "/"+++ " headers, "@@ ... @@" hunks, body lines
|
||||
// prefixed ' '/'+'/'-').
|
||||
// ---------------------------------------------------------------------
|
||||
|
||||
// gbDiffSections <text> -- one entry per "diff --git " block: { header,
|
||||
// body[] } (body = every following line up to, not including, the next
|
||||
// "diff --git " line). The one grouping both the file list and the
|
||||
// board.json comparison below are built from, so the two can never
|
||||
// disagree about where one file's diff ends and the next begins.
|
||||
function gbDiffSections(text) {
|
||||
var lines = (text || '').split('\n');
|
||||
var sections = [];
|
||||
@@ -161,18 +64,11 @@ function gbDiffSections(text) {
|
||||
return sections;
|
||||
}
|
||||
|
||||
// gbSectionPath <header> -- the "b/..." side of a "diff --git a/X b/X"
|
||||
// line. Git always fills in the same path on both sides for a plain add
|
||||
// or delete (no rename detection requested), so this never needs the
|
||||
// "a/" side at all.
|
||||
function gbSectionPath(header) {
|
||||
var m = header.match(/^diff --git a\/(.+) b\/(.+)$/);
|
||||
return m ? m[2] : null;
|
||||
}
|
||||
|
||||
// gbSectionStatus <section> -- 'added' | 'deleted' | 'modified', from
|
||||
// git's own "new file mode" / "deleted file mode" body lines (present
|
||||
// only for those two cases).
|
||||
function gbSectionStatus(section) {
|
||||
var i;
|
||||
for (i = 0; i < section.body.length; i++) {
|
||||
@@ -184,28 +80,14 @@ function gbSectionStatus(section) {
|
||||
return 'modified';
|
||||
}
|
||||
|
||||
// gbDiffFileList <text> -- [{ path, status }], one per changed file.
|
||||
function gbDiffFileList(text) {
|
||||
return gbDiffSections(text).map(function(s) {
|
||||
return { path: gbSectionPath(s.header), status: gbSectionStatus(s) };
|
||||
}).filter(function(f) { return f.path; });
|
||||
}
|
||||
|
||||
// gbBoardMismatch <text> -- true when the diff between the current branch
|
||||
// tip and the commit about to be restored changed meta/board.json's own
|
||||
// "model" or "target" field. Mirrors restore.sh's own authoritative check
|
||||
// (_gb_restore_check_board: "model" plus "release.target", the same two
|
||||
// fields device.sh's board strategy and the armsr/generic-target case
|
||||
// both rely on -- interfaces.md's own note that `model` is `null` on
|
||||
// those targets) rather than "board.json changed at all": board.json also
|
||||
// carries a plain `hostname` field, and flagging every rename as a board
|
||||
// mismatch would train an operator to ignore the warning. This can only
|
||||
// approximate restore.sh's live `ubus call system board` comparison with
|
||||
// the closest thing a read-only rpcd surface exposes -- the most recent
|
||||
// commit's own meta/board.json, i.e. what the last successful backup run
|
||||
// saw. restore.sh itself remains the actual, authoritative gate (it always
|
||||
// re-checks against the live board and refuses without --force); this is
|
||||
// only the up-front warning the brief asks for.
|
||||
// The same two fields restore.sh checks against the live board; restore.sh
|
||||
// stays the real gate, this is only the up-front warning.
|
||||
function gbBoardMismatch(text) {
|
||||
var sections = gbDiffSections(text);
|
||||
var i, j, path, line, ch;
|
||||
@@ -230,30 +112,12 @@ function gbBoardMismatch(text) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// gbFilesPath <path> -- the real absolute filesystem path for an entry
|
||||
// under ".../files/...", or null for anything else (meta/*, manifest.json).
|
||||
// `restore` (restore.sh's own gb_restore) only ever writes manifest
|
||||
// entries -- i.e. the "files/" tree -- back onto the router; it never
|
||||
// touches meta/board.json, manifest.json or any other bookkeeping file on
|
||||
// disk. A changed-file list built straight from `gbDiffFileList` would
|
||||
// therefore mislabel bookkeeping-only commits (a manifest.json-only diff,
|
||||
// say) as "these files will be overwritten" when nothing on disk would
|
||||
// actually change.
|
||||
function gbFilesPath(path) {
|
||||
var idx = path.indexOf('/files/');
|
||||
return idx === -1 ? null : path.substring(idx + '/files'.length);
|
||||
}
|
||||
|
||||
// gbRestorePlan <diffText> -- [{ path, action }], action is 'create' (the
|
||||
// path exists in the target commit but not in the current branch tip, so
|
||||
// restore will write it fresh) or 'overwrite' (exists in both, content
|
||||
// differs). A file that exists in the current tip but NOT in the target
|
||||
// ('deleted' in git-diff terms, moving tip -> target) is deliberately
|
||||
// left out: restore.sh has no delete pass at all (spec: "разложить файлы,
|
||||
// затем применить mode/uid/gid... пересоздать симлинки" -- nothing about
|
||||
// removing a file the target commit does not mention), so that path would
|
||||
// simply be left untouched by a real restore, not "removed", and listing
|
||||
// it as something that will be "overwritten" would be flatly wrong.
|
||||
// A file only in the tip is left out: restore has no delete pass.
|
||||
function gbRestorePlan(diffText) {
|
||||
var files = gbDiffFileList(diffText);
|
||||
var plan = [];
|
||||
@@ -271,40 +135,8 @@ function gbRestorePlan(diffText) {
|
||||
return plan;
|
||||
}
|
||||
|
||||
// The colored-<pre> diff viewer itself moved to diffview.js (ticket 23:
|
||||
// "не пиши второй просмотрщик diff") once Overview needed one too, for a
|
||||
// different diff format (config_diff's manifest-field lines, not a real
|
||||
// `git diff`) -- see that file's own header comment for why this is two
|
||||
// explicit renderers in one shared place rather than one that guesses.
|
||||
// handleViewDiff below calls diffview.unified(res.diff): `diff` (gbrpc_diff,
|
||||
// two already-committed shas) is real `git diff --end-of-options <a> <b>`
|
||||
// output (usr/sbin/gitbackup's cmd_diff, its two-argument form), exactly
|
||||
// what diffview.unified expects.
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
// Restore log classification -- restore.sh's own gb_log calls (all of
|
||||
// them: success, every refusal, every fetch/checkout failure) go through
|
||||
// `logger -t gitbackup` (lib.sh's gb_log), which `log`/logread already
|
||||
// tag-filters -- the exact same mechanism overview.js's own live-log
|
||||
// poller already reads for `run`/`test`. Restore's plain `printf` on
|
||||
// success ("restored %s from %s") is NOT captured here: gbrpc_restore
|
||||
// backgrounds the CLI with its stdout sent to /dev/null (same shape as
|
||||
// run/test), so only the gb_log line right before it ("gb_restore:
|
||||
// restored $device from $target on $branch") is ever visible through this
|
||||
// poller -- reason this regex keys on that line, not the printf one.
|
||||
// ---------------------------------------------------------------------
|
||||
// Ticket 23: 'writing one or more files failed' used to sit here instead of
|
||||
// the line below, and never matched anything restore.sh actually logs --
|
||||
// found while tracing ticket 19's own change (restore.sh, _gb_restore:
|
||||
// "a partially-restored router with correct permissions... always tells
|
||||
// the operator the truth (below) with a non-zero exit" prints
|
||||
// "gb_restore: the following paths were NOT written:<list>" via gb_log,
|
||||
// never the old string) against this poller's own terminal-line set. With
|
||||
// the wrong string here, a partial restore (ticket 19's own headline
|
||||
// outcome -- content written, permissions applied, exit 1) never matched
|
||||
// GB_RESTORE_TERMINAL_RE at all and this poller ran all the way to its own
|
||||
// 60s-idle/5-minute ceiling before saying anything, exactly the
|
||||
// "непонятно, чем кончилось" ticket 23 exists to fix.
|
||||
// restore.sh's success printf goes to /dev/null, so the gb_log line is what
|
||||
// counts. Checked against tests/fixtures/terminal-markers.tsv.
|
||||
var GB_RESTORE_TERMINAL_RE = new RegExp(
|
||||
[
|
||||
'gb_restore: restored .+ from .+ on ',
|
||||
@@ -320,24 +152,8 @@ var GB_RESTORE_TERMINAL_RE = new RegExp(
|
||||
].join('|')
|
||||
);
|
||||
|
||||
function gbRestoreLogSuccess(line) {
|
||||
return line.indexOf('gb_restore: restored ') !== -1;
|
||||
}
|
||||
|
||||
// gbClassifyRestoreLog <line> -- the human-readable, three-severity outcome
|
||||
// behind GB_RESTORE_TERMINAL_RE's own match (ticket 23: "каждая долгая
|
||||
// операция заканчивается явным сообщением... причина неудачи написана
|
||||
// человеческим языком"). 'partial' is its own kind, deliberately distinct
|
||||
// from both 'success' and 'error' -- ticket 19's restore.sh always applies
|
||||
// permissions to whatever DID get written even when something else did not
|
||||
// (spec: "частичный успех... обязан быть виден и отличаться от полного"),
|
||||
// and folding it into either extreme would misreport which one actually
|
||||
// happened. 'blocked' covers the one refusal that is a safety check working
|
||||
// as intended (a board mismatch neither this view's own heuristic nor the
|
||||
// operator's confirmation caught), kept apart from a genuine 'error' (a
|
||||
// network/fetch/config problem) since the next step for each is different.
|
||||
function gbClassifyRestoreLog(line) {
|
||||
if (gbRestoreLogSuccess(line))
|
||||
if (line.indexOf('gb_restore: restored ') !== -1)
|
||||
return { kind: 'success', message: _('Restore finished. This router now matches the backup you selected.') };
|
||||
|
||||
if (line.indexOf('the following paths were NOT written:') !== -1)
|
||||
@@ -373,11 +189,6 @@ function gbClassifyRestoreLog(line) {
|
||||
return { kind: 'error', message: _('Restore failed -- see the log below for details.') };
|
||||
}
|
||||
|
||||
// gbRestoreSeverity <kind> -- gbClassifyRestoreLog's own four kinds
|
||||
// collapsed to the three severities showRestoreResult below actually
|
||||
// styles: 'success' -> ok, 'partial'/'blocked' -> warn (something is worth
|
||||
// a second look, but this is not the network/config failures 'error'
|
||||
// means), 'error' (and anything unrecognized) -> error.
|
||||
function gbRestoreSeverity(kind) {
|
||||
if (kind === 'success')
|
||||
return 'ok';
|
||||
@@ -387,10 +198,6 @@ function gbRestoreSeverity(kind) {
|
||||
}
|
||||
|
||||
var GB_CSS = [
|
||||
'.gitbackup-view { container-type: inline-size; }',
|
||||
'.gitbackup-card-hint { font-size: .9em; color: var(--text-color-medium, #666); }',
|
||||
'.gitbackup-banner { border-radius: 4px; padding: .6em 1em; margin: 0 0 .75em; background: var(--error-color-medium, #f44336); color: var(--on-error-color, #fff); }',
|
||||
'.gitbackup-banner p { margin: .2em 0; }',
|
||||
'.gitbackup-history-list { list-style: none; margin: .75em 0; padding: 0; }',
|
||||
'.gitbackup-history-row { border: 1px solid var(--background-color-medium, #ddd); border-radius: 4px; padding: .6em .9em; margin: 0 0 .6em; background: var(--background-color-low, #f5f5f5); }',
|
||||
'.gitbackup-history-head { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: .5em 1em; }',
|
||||
@@ -406,37 +213,17 @@ var GB_CSS = [
|
||||
'.gitbackup-restore-action-create { color: var(--success-color-high, #2e7d32); }',
|
||||
'.gitbackup-restore-action-overwrite { color: var(--warn-color-high, #b45f06); }',
|
||||
'.gitbackup-confirm-input { width: 100%; box-sizing: border-box; margin: .5em 0; }',
|
||||
'.gitbackup-modal-actions { display: flex; justify-content: flex-end; gap: .5em; margin-top: 1em; }',
|
||||
'.gitbackup-log { max-height: 320px; overflow: auto; background: var(--background-color-low, #f5f5f5); color: var(--text-color-high, #333); border: 1px solid var(--background-color-medium, #ddd); border-radius: 4px; padding: .6em .8em; font-family: monospace; font-size: .85em; white-space: pre-wrap; }',
|
||||
// gitbackup-hint-ok/-warn/-error -- same three severities settings.js's
|
||||
// own copy already uses (ticket 12); reused here (ticket 23) for
|
||||
// showRestoreResult's own success/partial-or-blocked/error message.
|
||||
'.gitbackup-hint-ok { color: var(--success-color-high, #2e7d32); }',
|
||||
'.gitbackup-hint-warn { color: var(--warn-color-high, #b45f06); }',
|
||||
'.gitbackup-hint-error { color: var(--error-color-high, #c62828); }',
|
||||
'.gitbackup-restore-outcome { font-weight: bold; }'
|
||||
];
|
||||
|
||||
return view.extend({
|
||||
// Not a config form -- see overview.js's own identical comment. Rule 11
|
||||
// of the spec's style guide: the stock Save/Apply/Reset row is switched
|
||||
// off through this documented hook, never hidden with CSS.
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
|
||||
// history is a git fetch; render() starts it after the first paint.
|
||||
load: function() {
|
||||
// `status` only (device name for the second restore-confirmation
|
||||
// screen) -- local file reads, same "cheap" bucket overview.js's
|
||||
// own load() puts status/log/service in. `history` itself is
|
||||
// deliberately NOT here: it is a real `git fetch` against the
|
||||
// configured remote with no timeout of its own (interfaces.md,
|
||||
// tickets 11/13: "Долгие вызовы... не вешаются на первичную
|
||||
// загрузку страницы" -- measured live at several seconds against a
|
||||
// real GitHub remote). render() below starts it once the rest of
|
||||
// the page is already on screen, same pattern overview.js's own
|
||||
// refreshHistory() already uses.
|
||||
return L.resolveDefault(callStatus(), null);
|
||||
return L.resolveDefault(common.callStatus(), null);
|
||||
},
|
||||
|
||||
render: function(status) {
|
||||
@@ -447,37 +234,18 @@ return view.extend({
|
||||
self._commits = null;
|
||||
|
||||
view = E('div', { 'class': 'gitbackup-view' }, [
|
||||
E('style', { 'type': 'text/css' }, [ GB_CSS.concat(diffview.css).join('\n') ]),
|
||||
E('style', { 'type': 'text/css' }, [ common.css.concat(GB_CSS).join('\n') ]),
|
||||
|
||||
E('h2', {}, [ _('Git Backup - History / Restore') ]),
|
||||
|
||||
// Ticket 22: found-in-the-field complaint was "restore exists
|
||||
// but nobody looks for it on a tab called History" -- this
|
||||
// hint is the fix's cheap half (the tab rename above is the
|
||||
// other): it states the round-trip capability up front, in
|
||||
// plain text, with no hover required, before a single commit
|
||||
// row has even loaded.
|
||||
E('p', { 'class': 'gitbackup-card-hint' },
|
||||
[ _('You can restore this router to any backup listed below, using that backup\'s own "Restore" button. Click "View diff" first to see exactly what it would change.') ]),
|
||||
|
||||
E('div', { 'id': 'gitbackup-history-body' }, [
|
||||
E('p', { 'class': 'gitbackup-card-hint' }, [ _('Loading…') ])
|
||||
])
|
||||
|
||||
// Ticket 23: the old page-level "#gitbackup-restore-log" <pre>
|
||||
// that used to live here is gone -- the confirmation modal
|
||||
// itself now stays open for the whole restore and shows the
|
||||
// live log and the final outcome in place (see
|
||||
// handleConfirmRestore/showRestoreProgress/showRestoreResult
|
||||
// below), instead of hiding itself immediately and leaving an
|
||||
// operator to notice a page element they were never told to
|
||||
// watch (spec: "модальное окно... не должно просто
|
||||
// закрываться... а в конце говорит, что восстановлено").
|
||||
]);
|
||||
|
||||
// Fired once render() has already returned the tree above, exactly
|
||||
// like overview.js's own refreshHistory() -- see load()'s own
|
||||
// comment for why `history` cannot run inside load() itself.
|
||||
self.refreshHistory();
|
||||
|
||||
return view;
|
||||
@@ -486,7 +254,7 @@ return view.extend({
|
||||
refreshHistory: function() {
|
||||
var self = this;
|
||||
|
||||
return L.resolveDefault(callHistory(GB_HISTORY_LIMIT), null).then(function(res) {
|
||||
return L.resolveDefault(common.callHistory(GB_HISTORY_LIMIT), null).then(function(res) {
|
||||
self._commits = (res && res.commits) || null;
|
||||
self._historyErr = (!res) ?
|
||||
_('Could not reach the remote repository.') :
|
||||
@@ -525,7 +293,7 @@ return view.extend({
|
||||
|
||||
buildHistoryRow: function(commit, parentSha) {
|
||||
var self = this;
|
||||
var time = gbCommitTime(commit.subject);
|
||||
var time = common.commitTime(commit.subject);
|
||||
var changed = (commit.changed || []).map(gbShortenRepoPath);
|
||||
|
||||
return E('li', { 'class': 'gitbackup-history-row' }, [
|
||||
@@ -549,19 +317,8 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
// handleViewDiff <commit> <parentSha> -- the per-commit diff (spec:
|
||||
// "Клик -> diff в ui.showModal"), i.e. what THAT backup changed versus
|
||||
// the one right before it. `history`'s commits are newest-first and
|
||||
// this branch's own push machinery (gitio.sh's gb_commit_push) always
|
||||
// commits with exactly one parent, so the previous ROW's sha (already
|
||||
// known client-side) is that parent -- no extra round trip needed to
|
||||
// find it. The oldest row on screen has no such neighbour: its actual
|
||||
// parent (if any) sits outside this page's own GB_HISTORY_LIMIT
|
||||
// window, and fabricating a diff against the wrong commit would be
|
||||
// worse than admitting this view simply does not know it.
|
||||
// The previous row is the only parent; the oldest row's is off the page.
|
||||
handleViewDiff: function(commit, parentSha, ev) {
|
||||
var self = this;
|
||||
|
||||
if (!parentSha) {
|
||||
ui.addNotification(null, E('p', {},
|
||||
[ _('No earlier backup loaded to compare against -- this is the oldest commit in this list.') ]), 'info');
|
||||
@@ -574,27 +331,18 @@ return view.extend({
|
||||
if (!res || typeof res.diff !== 'string') {
|
||||
ui.showModal(_('Diff'), [
|
||||
E('p', {}, [ _('Could not load the diff: %s').format((res && res.reason) || _('unknown error')) ]),
|
||||
E('div', { 'class': 'gitbackup-modal-actions' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, [ _('Close') ])
|
||||
])
|
||||
common.modalButtons()
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
ui.showModal(_('Diff: %s').format(gbCommitTime(commit.subject) || commit.sha.substring(0, 12)), [
|
||||
diffview.unified(res.diff),
|
||||
E('div', { 'class': 'gitbackup-modal-actions' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, [ _('Close') ])
|
||||
])
|
||||
ui.showModal(_('Diff: %s').format(common.commitTime(commit.subject) || commit.sha.substring(0, 12)), [
|
||||
common.unified(res.diff),
|
||||
common.modalButtons()
|
||||
]);
|
||||
});
|
||||
},
|
||||
|
||||
// handleRestoreClick <commit> -- step 0: fetch the one diff this whole
|
||||
// confirmation flow is built on (current branch tip -> the commit
|
||||
// about to be restored) before showing anything, so the first
|
||||
// confirmation screen (spec: "первый экран показывает список
|
||||
// перезаписываемых файлов") is never a guess.
|
||||
handleRestoreClick: function(commit, ev) {
|
||||
var self = this;
|
||||
var headSha = (self._commits && self._commits[0] && self._commits[0].sha) || commit.sha;
|
||||
@@ -610,23 +358,19 @@ return view.extend({
|
||||
});
|
||||
},
|
||||
|
||||
// showRestoreConfirm1 -- first confirmation screen (spec, verbatim
|
||||
// above): the list of files that will actually be written, and, when
|
||||
// applicable, the board-mismatch warning ahead of any second screen --
|
||||
// never after.
|
||||
showRestoreConfirm1: function(commit, plan, mismatch) {
|
||||
var self = this;
|
||||
var body = [];
|
||||
|
||||
if (mismatch) {
|
||||
body.push(E('div', { 'class': 'gitbackup-banner' }, [
|
||||
body.push(E('div', { 'class': 'alert-message error' }, [
|
||||
E('p', {}, [ _('This backup was taken on a different router model than the one it would be restored onto. Interface names and wireless radios from that hardware will not match this one -- restoring anyway may leave the network unreachable until the configuration is fixed by hand.') ])
|
||||
]));
|
||||
}
|
||||
|
||||
body.push(E('p', {},
|
||||
[ _('Restoring the backup from %s (%s) will write the following files:')
|
||||
.format(gbCommitTime(commit.subject) || commit.sha, commit.sha.substring(0, 12)) ]));
|
||||
.format(common.commitTime(commit.subject) || commit.sha, commit.sha.substring(0, 12)) ]));
|
||||
|
||||
if (plan.length) {
|
||||
body.push(E('ul', { 'class': 'gitbackup-restore-files' }, plan.map(function(p) {
|
||||
@@ -640,8 +384,7 @@ return view.extend({
|
||||
[ _('No file differs from the current commit -- restoring will not change anything on disk.') ]));
|
||||
}
|
||||
|
||||
body.push(E('div', { 'class': 'gitbackup-modal-actions' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, [ _('Cancel') ]),
|
||||
body.push(common.modalButtons(_('Cancel'), [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-negative',
|
||||
'click': ui.createHandlerFn(self, 'showRestoreConfirm2', commit, mismatch)
|
||||
@@ -651,14 +394,6 @@ return view.extend({
|
||||
ui.showModal(_('Restore backup?'), body);
|
||||
},
|
||||
|
||||
// showRestoreConfirm2 -- second confirmation screen (spec, verbatim:
|
||||
// "второй требует ввести имя устройства"): the operator has to type
|
||||
// this device's own name back, the same friction a destructive cloud
|
||||
// console action usually asks for. `status.device` (gitbackup status's
|
||||
// own `device` field, interfaces.md tickets 01/08-10) is the device
|
||||
// this router itself resolves to -- and the one `restore` is about to
|
||||
// be called for, since History only ever shows this router's own
|
||||
// branch.
|
||||
showRestoreConfirm2: function(commit, mismatch, ev) {
|
||||
var self = this;
|
||||
var device = self._status.device || '';
|
||||
@@ -683,50 +418,29 @@ return view.extend({
|
||||
ui.showModal(_('Confirm restore'), [
|
||||
E('p', {}, [ _('Type this device\'s name (%s) to confirm restoring it.').format(device || _('(device name not configured)')) ]),
|
||||
input,
|
||||
E('div', { 'class': 'gitbackup-modal-actions' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, [ _('Cancel') ]),
|
||||
confirmBtn
|
||||
])
|
||||
common.modalButtons(_('Cancel'), [ confirmBtn ])
|
||||
]);
|
||||
},
|
||||
|
||||
// handleConfirmRestore -- fires `restore` (write tier) and switches to
|
||||
// oplog.js's shared bounded live-log poller (ticket 23: "не изобретать
|
||||
// четвёртый механизм" -- the exact pattern overview.js's own
|
||||
// handleRun/handleTest use for run/test). The call itself only starts
|
||||
// the CLI in the background (gbrpc_restore always answers
|
||||
// `{started:true}` immediately) and can take a while for real (network
|
||||
// fetch of the target commit, sha256 verification of every file), so
|
||||
// this must not block the page.
|
||||
//
|
||||
// Ticket 23's own headline complaint about restore specifically:
|
||||
// the confirmation modal used to call ui.hideModal() right here and
|
||||
// let the page's own <pre> carry the live log silently underneath --
|
||||
// closing the one dialog that had the operator's attention just as an
|
||||
// irreversible, file-system-rewriting operation started, then saying
|
||||
// nothing further unless it failed to even START. It no longer hides
|
||||
// at all: showRestoreProgress below replaces this same dialog's
|
||||
// content in place, and stays open, showing the live log, until
|
||||
// showRestoreResult replaces it again with the actual outcome --
|
||||
// success, partial, or failure, spelled out in words (spec: "пусть
|
||||
// остаётся, показывает ход и в конце говорит, что восстановлено, что
|
||||
// не удалось и что делать дальше").
|
||||
//
|
||||
// `force` is passed whenever the board-mismatch warning fired on the
|
||||
// previous screen -- without it restore.sh's own
|
||||
// _gb_restore_check_board refuses outright (exit 4), which would turn
|
||||
// an operator's informed "restore anyway" into a silent no-op visible
|
||||
// only by reading the log by hand.
|
||||
// `force` goes along when the board warning was shown, otherwise
|
||||
// restore.sh refuses the mismatch outright.
|
||||
handleConfirmRestore: function(commit, mismatch, ev) {
|
||||
var self = this;
|
||||
var device = self._status.device || '';
|
||||
|
||||
self.showRestoreProgress();
|
||||
// No Cancel: restore.sh cannot be interrupted once it writes.
|
||||
ui.showModal(_('Restoring…'), [
|
||||
E('p', { 'class': 'spinning' },
|
||||
[ _('Restoring the backup -- this can take a while. Do not power off the router.') ]),
|
||||
E('pre', { 'class': 'gitbackup-log', 'id': 'gitbackup-restore-modal-log' }, [ '' ])
|
||||
]);
|
||||
|
||||
return oplog.start({
|
||||
fetch: function() { return L.resolveDefault(callLog(500), null).then(function(res) { return (res && res.text) || ''; }); },
|
||||
return oplog.run({
|
||||
call: function() {
|
||||
return callRestore(device, commit.sha, undefined, mismatch ? true : undefined);
|
||||
},
|
||||
pre: 'gitbackup-restore-modal-log',
|
||||
terminalRe: GB_RESTORE_TERMINAL_RE,
|
||||
onProgress: function(add) { self.appendRestoreProgress(add); },
|
||||
onFinish: function(line) {
|
||||
self.showRestoreResult(gbClassifyRestoreLog(line));
|
||||
self.refreshHistory();
|
||||
@@ -737,59 +451,15 @@ return view.extend({
|
||||
message: _('No result from the restore after a while -- check the log below or the syslog by hand.')
|
||||
});
|
||||
self.refreshHistory();
|
||||
},
|
||||
onFail: function(reason) {
|
||||
self.showRestoreResult({ kind: 'error', message: reason ?
|
||||
_('Could not start the restore: %s').format(reason) :
|
||||
_('Could not start the restore.') });
|
||||
}
|
||||
}).then(function() {
|
||||
return callRestore(device, commit.sha, undefined, mismatch ? true : undefined);
|
||||
}).then(function(res) {
|
||||
if (!res || res.started !== true) {
|
||||
oplog.stop();
|
||||
self.showRestoreResult({ kind: 'error', message: _('Could not start the restore.') });
|
||||
}
|
||||
}).catch(function(e) {
|
||||
oplog.stop();
|
||||
self.showRestoreResult({ kind: 'error', message: _('Could not start the restore: %s').format(e.message) });
|
||||
});
|
||||
},
|
||||
|
||||
// showRestoreProgress -- replaces the confirmation modal's own content
|
||||
// with a spinner and an empty live-log pane, IN THE SAME DIALOG (no
|
||||
// ui.hideModal() anywhere in this whole flow) -- see
|
||||
// handleConfirmRestore's own comment for why staying open matters here
|
||||
// specifically. No Close/Cancel control on purpose: restore.sh has no
|
||||
// interrupt handling once it starts writing, so a "Cancel" here could
|
||||
// only ever stop watching, never stop the restore itself, and offering
|
||||
// it would suggest otherwise.
|
||||
showRestoreProgress: function() {
|
||||
ui.showModal(_('Restoring…'), [
|
||||
E('p', { 'class': 'spinning' },
|
||||
[ _('Restoring the backup -- this can take a while. Do not power off the router.') ]),
|
||||
E('pre', { 'class': 'gitbackup-log', 'id': 'gitbackup-restore-modal-log' }, [ '' ])
|
||||
]);
|
||||
},
|
||||
|
||||
appendRestoreProgress: function(add) {
|
||||
var pre = document.getElementById('gitbackup-restore-modal-log');
|
||||
|
||||
if (!pre)
|
||||
return;
|
||||
|
||||
pre.textContent = pre.textContent ? pre.textContent + '\n' + add : add;
|
||||
pre.scrollTop = pre.scrollHeight;
|
||||
},
|
||||
|
||||
// showRestoreResult <cls> -- the final screen of the restore flow:
|
||||
// <cls> is gbClassifyRestoreLog's own {kind, message} (or the
|
||||
// onTimeout/could-not-start shapes handleConfirmRestore builds inline
|
||||
// above, both the same shape). Reads the progress pane's own
|
||||
// accumulated text (still in the DOM at this point, since this is the
|
||||
// same ui.showModal call that is about to replace it) rather than
|
||||
// keeping a second copy of it in JS state.
|
||||
//
|
||||
// Ticket 23's own explicit ask beyond "say what happened": "что делать
|
||||
// дальше" for the one outcome that actually has a next step worth
|
||||
// naming -- 'partial' (ticket 19's own headline case: content written,
|
||||
// permissions applied, one or more paths failed) points at the log
|
||||
// right below it, already showing exactly which paths and why.
|
||||
showRestoreResult: function(cls) {
|
||||
var pre = document.getElementById('gitbackup-restore-modal-log');
|
||||
var fullLog = pre ? pre.textContent : '';
|
||||
@@ -805,9 +475,7 @@ return view.extend({
|
||||
if (fullLog)
|
||||
body.push(E('pre', { 'class': 'gitbackup-log' }, [ fullLog ]));
|
||||
|
||||
body.push(E('div', { 'class': 'gitbackup-modal-actions' }, [
|
||||
E('button', { 'class': 'cbi-button', 'click': ui.hideModal }, [ _('Close') ])
|
||||
]));
|
||||
body.push(common.modalButtons());
|
||||
|
||||
ui.showModal(title, body);
|
||||
}
|
||||
|
||||
@@ -1,145 +1,124 @@
|
||||
'use strict';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
|
||||
// run/test/restore are backgrounded by rpcd ({started:true}); the only way to
|
||||
// see how they went is to tail `log` until a terminal line appears. LuCI hands
|
||||
// every view the same instance: one operation at a time per view.
|
||||
|
||||
var callLog = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
method: 'log',
|
||||
params: [ 'lines' ]
|
||||
});
|
||||
|
||||
function gbFetchLog() {
|
||||
return L.resolveDefault(callLog(500), null).then(function(res) {
|
||||
return (res && res.text) || '';
|
||||
});
|
||||
}
|
||||
|
||||
// gitbackup -- shared bounded live-log poller (ticket 23, spec "Не
|
||||
// изобретать четвёртый механизм"). Before this file existed, overview.js
|
||||
// (run/test), history.js (restore) and settings.js (test) each carried
|
||||
// their own, separately-typed copy of the exact same state machine: fetch
|
||||
// a log tail, diff it against the line count from the previous tick, feed
|
||||
// the new lines to a caller-supplied terminal-line regex, and give up
|
||||
// after 60s of no new output or a hard 5-minute ceiling regardless --
|
||||
// interfaces.md's own record of tickets 11-14 already names this bound
|
||||
// three times over ("терминальная строка лога, простой в 60 секунд,
|
||||
// жёсткий потолок в 5 минут"), copied rather than shared. Ticket 23's own
|
||||
// acceptance criterion ("механизм поллинга один на все три view, а не
|
||||
// четыре разных") asks for exactly one implementation of that shape; this
|
||||
// is it.
|
||||
//
|
||||
// LuCI's own require() resolves a module ONCE per page and hands back the
|
||||
// same already-instantiated singleton to every caller (luci.js:
|
||||
// `compileClass`'s `new _class()`, cached in `classes[name]`) -- so every
|
||||
// one of the three views above sharing this same instance is safe only
|
||||
// because each of them is its own full page load (classic multi-page LuCI
|
||||
// tears down the whole JS context on tab switch) AND never runs two such
|
||||
// pollers from itself at once (the button(s) a poller was started from
|
||||
// stay disabled for its entire run). start() below resets every piece of
|
||||
// this instance's state, so a second, later operation on the very same
|
||||
// page (e.g. overview.js's "Backup now" followed later by its own "Test
|
||||
// connection") reuses the same instance cleanly once the first one has
|
||||
// actually stopped.
|
||||
return L.Class.extend({
|
||||
// start(opts) -- begins polling opts.fetch() every opts.interval
|
||||
// seconds (default 2, matching every prior copy of this poller).
|
||||
// opts.fetch() must resolve to the log's current text tail (a
|
||||
// rejected fetch is treated as "no output this tick", same as an
|
||||
// empty string, and repeated failures alone can still trip the idle
|
||||
// bound below). Returns a promise that resolves once the FIRST fetch
|
||||
// (establishing the starting line count) has completed and polling
|
||||
// has actually started -- callers await this before firing the
|
||||
// backgrounded CLI call itself, exactly like every prior copy of this
|
||||
// poller did, so a very fast operation's own first output line is
|
||||
// never missed.
|
||||
//
|
||||
// opts.terminalRe (RegExp, required) -- tested against each newly
|
||||
// arrived line; a match stops the poller and fires opts.onFinish.
|
||||
// opts.onProgress(addedText), if given, fires on every tick that saw
|
||||
// new output at all, with only the lines new since the previous tick
|
||||
// (already newline-joined, blank lines dropped) -- never the whole
|
||||
// accumulated text, so a caller appending it to a growing <pre> never
|
||||
// duplicates a line.
|
||||
// opts.onFinish(matchedLine), if given, fires exactly once, after the
|
||||
// poller has already stopped itself, with the one line that matched
|
||||
// opts.terminalRe.
|
||||
// opts.onTimeout(), if given, fires exactly once, after the poller has
|
||||
// already stopped itself, when opts.idleLimit ticks (default 30 == 60s
|
||||
// at the default 2s interval) passed with no new output at all, when
|
||||
// opts.tickLimit ticks (default 150 == 5 minutes) is reached
|
||||
// regardless, or when opts.fetch() itself keeps rejecting for that
|
||||
// same idle span -- three different reasons this project's own
|
||||
// interfaces.md has always treated as one bound ("простой... либо
|
||||
// потолок"), not three outcomes a caller has to tell apart.
|
||||
start: function(opts) {
|
||||
callLog: callLog,
|
||||
|
||||
// Resolves only once the operation is over, so a createHandlerFn button
|
||||
// stays disabled until then. Exactly one of opts.onFinish/onTimeout/onFail
|
||||
// fires.
|
||||
run: function(opts) {
|
||||
var self = this;
|
||||
var pre = opts.pre ? document.getElementById(opts.pre) : null;
|
||||
|
||||
self._opts = opts;
|
||||
self._idle = 0;
|
||||
self._ticks = 0;
|
||||
self._lines = 0;
|
||||
self._done = false;
|
||||
|
||||
return opts.fetch().then(function(text) {
|
||||
text = text || '';
|
||||
self._lines = text ? text.split('\n').length : 0;
|
||||
if (pre) {
|
||||
pre.hidden = false;
|
||||
pre.textContent = '';
|
||||
}
|
||||
|
||||
if (!self._bound)
|
||||
self._bound = L.bind(self._tick, self);
|
||||
return new Promise(function(resolve) {
|
||||
self._opts = opts;
|
||||
self._resolve = resolve;
|
||||
|
||||
poll.remove(self._bound);
|
||||
poll.add(self._bound, opts.interval || 2);
|
||||
// Baseline before the call, or the operation's first line is missed.
|
||||
gbFetchLog().then(function(text) {
|
||||
self._lines = text ? text.split('\n').length : 0;
|
||||
|
||||
if (!self._bound)
|
||||
self._bound = L.bind(self._tick, self);
|
||||
|
||||
poll.remove(self._bound);
|
||||
poll.add(self._bound, 2);
|
||||
|
||||
return opts.call();
|
||||
}).then(function(res) {
|
||||
if (!res || res.started !== true)
|
||||
throw new Error((res && res.reason) || '');
|
||||
}).catch(function(e) {
|
||||
self._end(opts.onFail, e.message);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
// stop() -- idempotent; safe to call whether or not a poll is
|
||||
// currently running (every call site here does, on the "could not
|
||||
// even start the operation" path, whether or not start() got as far
|
||||
// as poll.add).
|
||||
stop: function() {
|
||||
if (this._bound)
|
||||
poll.remove(this._bound);
|
||||
},
|
||||
|
||||
_append: function(add) {
|
||||
var pre = this._opts.pre ? document.getElementById(this._opts.pre) : null;
|
||||
|
||||
if (!pre)
|
||||
return;
|
||||
|
||||
pre.textContent = pre.textContent ? pre.textContent + '\n' + add : add;
|
||||
pre.scrollTop = pre.scrollHeight;
|
||||
},
|
||||
|
||||
_end: function(cb, arg) {
|
||||
if (this._done)
|
||||
return;
|
||||
this._done = true;
|
||||
this.stop();
|
||||
if (cb)
|
||||
cb(arg);
|
||||
this._resolve();
|
||||
},
|
||||
|
||||
_tick: function() {
|
||||
var self = this;
|
||||
var opts = self._opts;
|
||||
var idleLimit = opts.idleLimit || 30;
|
||||
var tickLimit = opts.tickLimit || 150;
|
||||
|
||||
self._ticks++;
|
||||
|
||||
return opts.fetch().then(function(text) {
|
||||
var lines, newLines, add, finished, matchedLine, i;
|
||||
|
||||
text = text || '';
|
||||
lines = text.split('\n');
|
||||
newLines = (lines.length >= self._lines) ? lines.slice(self._lines) : lines;
|
||||
add = newLines.filter(function(l) { return l; }).join('\n');
|
||||
finished = false;
|
||||
matchedLine = null;
|
||||
return callLog(500).then(function(res) {
|
||||
var lines = ((res && res.text) || '').split('\n');
|
||||
var newLines = (lines.length >= self._lines) ? lines.slice(self._lines) : lines;
|
||||
var add = newLines.filter(function(l) { return l; }).join('\n');
|
||||
var matched = null;
|
||||
var i;
|
||||
|
||||
self._lines = lines.length;
|
||||
|
||||
if (add) {
|
||||
self._idle = 0;
|
||||
if (opts.onProgress)
|
||||
opts.onProgress(add);
|
||||
self._append(add);
|
||||
for (i = 0; i < newLines.length; i++) {
|
||||
if (opts.terminalRe.test(newLines[i])) {
|
||||
finished = true;
|
||||
matchedLine = newLines[i];
|
||||
}
|
||||
if (opts.terminalRe.test(newLines[i]))
|
||||
matched = newLines[i];
|
||||
}
|
||||
} else {
|
||||
self._idle++;
|
||||
}
|
||||
|
||||
if (finished) {
|
||||
self.stop();
|
||||
if (opts.onFinish)
|
||||
opts.onFinish(matchedLine);
|
||||
} else if (self._idle >= idleLimit || self._ticks >= tickLimit) {
|
||||
self.stop();
|
||||
if (opts.onTimeout)
|
||||
opts.onTimeout();
|
||||
}
|
||||
if (matched !== null)
|
||||
self._end(opts.onFinish, matched);
|
||||
else if (self._idle >= 30 || self._ticks >= 150)
|
||||
self._end(opts.onTimeout);
|
||||
}, function() {
|
||||
// A single failed poll must not spin forever either -- same
|
||||
// convention every prior copy of this poller used (count it
|
||||
// as a big idle jump rather than a distinct error path).
|
||||
self._idle += idleLimit;
|
||||
if (self._idle >= idleLimit) {
|
||||
self.stop();
|
||||
if (opts.onTimeout)
|
||||
opts.onTimeout();
|
||||
}
|
||||
self._end(opts.onTimeout);
|
||||
});
|
||||
}
|
||||
});
|
||||
File diff suppressed because it is too large.
Load diff
@@ -2,91 +2,12 @@
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require fs';
|
||||
'require view.gitbackup.common as common';
|
||||
/* global common */
|
||||
|
||||
// gitbackup -- Paths (ticket 13, spec "Решения по реализации -> LuCI -> Paths",
|
||||
// "Сбор и manifest.json", "Проверенные факты 25.12.4 -> sysupgrade"). Client-side
|
||||
// JS only, luci-base only -- same constraint and the same jsmin-conservative
|
||||
// style (plain ES5, `var`/`function`, no let/const/arrow/template literals,
|
||||
// every statement semicolon-terminated) as overview.js (ticket 11), read
|
||||
// before writing this file.
|
||||
//
|
||||
// The whole point of this screen (spec/brief, verbatim): "Расширение списка --
|
||||
// редактирование /etc/sysupgrade.conf, а не отдельный UCI-список. То, что
|
||||
// пользователь добавил ради бэкапа, автоматически переживёт реальный
|
||||
// sysupgrade." That file is owned entirely by package/gitbackup's paths.sh and
|
||||
// this file's own rpcd bridge (usr/libexec/rpcd/luci.gitbackup) -- both out of
|
||||
// this ticket's own zone in the original cut, but the read/write asymmetry
|
||||
// documented below was later closed in that same package/gitbackup zone, and
|
||||
// this view was updated alongside it.
|
||||
//
|
||||
// ---------------------------------------------------------------------
|
||||
// Raw vs. entries vs. effective -- the defect this file used to work
|
||||
// around instead of fixing, closed by making `list_paths` carry three
|
||||
// sets explicitly:
|
||||
//
|
||||
// - `paths` -- the RAW, unexpanded content of /etc/sysupgrade.conf
|
||||
// (gb_paths_list), comments and blank lines included.
|
||||
// Not used by this view at all (see `entries` below).
|
||||
// - `entries` -- (ticket 26) `paths`, filtered down to genuine path
|
||||
// entries: comments (sysupgrade.conf's own syntax --
|
||||
// a line starting with '#', including a commented-out
|
||||
// EXAMPLE such as "# /etc/openvpn/", which backs up
|
||||
// nothing) and blank lines are dropped by
|
||||
// gb_paths_entries server-side. This is what this view
|
||||
// lists and edits, and the only thing `set_paths` ever
|
||||
// writes back -- a directory line stays a directory
|
||||
// line across any number of edits, so anything added
|
||||
// to it later keeps being covered by a real sysupgrade
|
||||
// the same way it always was, instead of being frozen
|
||||
// into the exact file list that happened to exist on
|
||||
// the day of the last save here. Before this field
|
||||
// existed, this view built its editable list straight
|
||||
// from `paths`, so a stock router's own header
|
||||
// comments and example lines rendered as four bogus,
|
||||
// removable entries -- and the first save from this
|
||||
// view (any Add or Remove click) silently erased them,
|
||||
// because `set_paths` used to write back only what it
|
||||
// was sent and never looked at the rest of the file.
|
||||
// That erasure is now prevented server-side
|
||||
// (gb_paths_replace_entries, paths.sh) rather than by
|
||||
// this view trying to resend lines it was never shown
|
||||
// in the first place -- see that function's own header
|
||||
// comment for why the fix belongs there.
|
||||
// - `effective` -- plain `sysupgrade -l`'s own wider union (spec:
|
||||
// "(/etc/sysupgrade.conf ∪ /lib/upgrade/keep.d/*) ∪
|
||||
// изменённые conffiles"), already recursively expanded
|
||||
// to individual files/symlinks. Read-only: used only
|
||||
// to size the real backup set (the 2 MB warning has to
|
||||
// mean what `sysupgrade -b` will actually build, not
|
||||
// just the lines a human typed) and to show what is
|
||||
// already covered without anyone having added it here.
|
||||
//
|
||||
// Earlier, `list_paths` returned only the effective set under the name
|
||||
// `paths`, so this view's own edits were built on top of the ALREADY
|
||||
// EXPANDED list -- the first save after a human added a directory line
|
||||
// via the CLI would write that directory's individual files straight
|
||||
// back to sysupgrade.conf, permanently losing the one-line-per-directory
|
||||
// convenience. Editing the raw set instead removes the problem instead of
|
||||
// working around it.
|
||||
//
|
||||
// A second, related gap this same change closes: `set_paths`'s own
|
||||
// server-side check used to duplicate only two of paths.sh's
|
||||
// gb_paths_validate rules (absolute, the fixed blacklist), leaving a
|
||||
// space in the path and a nonexistent path to this view's own JS to
|
||||
// catch -- which 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` (confirmed live: every
|
||||
// attempt at fs.lstat here failed with rpcd's own -32002 "Access denied",
|
||||
// browser console, root login included -- only `fs.stat` is reachable).
|
||||
// `set_paths` now routes every entry through gb_paths_validate itself,
|
||||
// which runs ON the router and sees the real filesystem, so it gets `-e
|
||||
// || -L` right without needing that ACL grant at all. This view keeps its
|
||||
// own quick client-side checks (gbPathReject below) as a fast first pass
|
||||
// that avoids an obviously-doomed round trip, but the round trip -- and
|
||||
// its `rejected[]` response -- is the actual, authoritative gate; this
|
||||
// view no longer attempts its own existence check at all.
|
||||
// ---------------------------------------------------------------------
|
||||
// Writes back `entries`, never `effective` (sysupgrade -l): a directory line
|
||||
// must stay a directory line, or files created after the save stop being
|
||||
// backed up. Path validation lives in paths.sh only.
|
||||
|
||||
var callListPaths = rpc.declare({
|
||||
object: 'luci.gitbackup',
|
||||
@@ -104,127 +25,16 @@ var callSetPaths = rpc.declare({
|
||||
params: [ 'paths' ]
|
||||
});
|
||||
|
||||
// gb_paths_size_kb (paths.sh) rounds bytes up to a whole KB: awk's
|
||||
// `(s + 1023) / 1024` truncated to an integer is exactly `ceil(s / 1024)`.
|
||||
// No rpcd method exposes that number directly, so this view derives the
|
||||
// same total the same way paths.sh does: sum the byte size of every entry
|
||||
// in `list_paths`'s "effective" set -- the same full set `sysupgrade -l`
|
||||
// (and `run`'s own pre-flight space check) actually measures, not just
|
||||
// the raw lines a human typed -- using `fs.stat`, deliberately not
|
||||
// `fs.lstat`, and not only because of the ACL note above. gb_paths_size_kb's
|
||||
// own test is plain `[ -f "$path" ]`, which -- confirmed live against this
|
||||
// stand's busybox ash (a symlink to a regular file passes `-f`) -- FOLLOWS
|
||||
// a symlink to its target, same as `fs.stat`; only a target that is not a
|
||||
// regular file (a directory, or a dangling symlink) makes `-f` false and
|
||||
// the entry contributes 0. Using `fs.lstat` here would size the symlink's
|
||||
// own few bytes instead of its target's real content and disagree with
|
||||
// the shell-side total for the exact same set.
|
||||
var GB_PATHS_WARN_KB = 2048;
|
||||
|
||||
// gbPathReject <path> -- null when the path is worth submitting to
|
||||
// `set_paths`, otherwise one human-readable reason, worded identically to
|
||||
// package/gitbackup's paths.sh (gb_paths_validate) for the checks that
|
||||
// module owns and that do not need a filesystem round trip: absolute, no
|
||||
// space, not in the fixed blacklist. Order matches gb_paths_validate's own
|
||||
// (absolute first, everything else assumes it). This is a fast first pass
|
||||
// only -- `set_paths` re-validates every entry server-side (gb_paths_validate
|
||||
// itself, including existence) and is the actual gate; a path that slips
|
||||
// past this function is still caught there and reported through
|
||||
// commitPaths's own `rejected[]` handling below.
|
||||
function gbPathReject(path) {
|
||||
if (path.charAt(0) !== '/')
|
||||
return _('%s: not an absolute path').format(path);
|
||||
|
||||
if (path.indexOf(' ') !== -1)
|
||||
return _('%s: contains a space -- sysupgrade.conf lines become find(1) arguments and cannot quote one').format(path);
|
||||
|
||||
if (path === '/etc/gitbackup' || path.indexOf('/etc/gitbackup/') === 0)
|
||||
return _('%s: reserved for gitbackup itself').format(path);
|
||||
|
||||
if (path === '/proc' || path.indexOf('/proc/') === 0)
|
||||
return _('%s: not a real filesystem path').format(path);
|
||||
|
||||
if (path === '/sys' || path.indexOf('/sys/') === 0)
|
||||
return _('%s: not a real filesystem path').format(path);
|
||||
|
||||
if (path === '/tmp' || path.indexOf('/tmp/') === 0)
|
||||
return _('%s: cleared on every reboot').format(path);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// gbSequentialMap <items[]> <fn> -- like items.map(fn), but runs one call at
|
||||
// a time and only starts the next once the previous settles, instead of
|
||||
// firing every call at once. Confirmed necessary live on the owlab stand:
|
||||
// stock `uhttpd` there runs with `-n 3` (three concurrent ubus-bridge
|
||||
// requests, the same limit a real low-memory router ships with, not
|
||||
// something this dev stand loosened), and firing one `fs.stat` per entry of
|
||||
// even the 13-line clean-image default set at once left every later ubus
|
||||
// call queued behind it indefinitely -- confirmed by watching the network
|
||||
// panel: no request for a later call was even sent until an earlier one's
|
||||
// slot freed, and with enough of them in flight the rest of the page (this
|
||||
// view's own next list_paths/set_paths call, even an unrelated view) stayed
|
||||
// stuck on "Loading view..." until the batch drained. `Promise.all` over
|
||||
// every path was the original, incorrect implementation.
|
||||
function gbSequentialMap(items, fn) {
|
||||
var results = [];
|
||||
var i = 0;
|
||||
|
||||
function next() {
|
||||
if (i >= items.length)
|
||||
return Promise.resolve(results);
|
||||
|
||||
return fn(items[i]).then(function(res) {
|
||||
results.push(res);
|
||||
i++;
|
||||
return next();
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
}
|
||||
|
||||
// gbSumSizes <paths[]> -- { kb, unknown }. `unknown` counts entries whose
|
||||
// size could not be read at all (permission, or the entry vanished between
|
||||
// `list_paths` answering and this running) so the total is never silently
|
||||
// wrong without saying so. Called with the "effective" set, never the raw
|
||||
// one -- see the file-header comment.
|
||||
function gbSumSizes(paths) {
|
||||
return gbSequentialMap(paths, function(p) {
|
||||
return fs.stat(p).then(function(st) { return st; }, function() { return null; });
|
||||
}).then(function(stats) {
|
||||
var bytes = 0;
|
||||
var unknown = 0;
|
||||
var i;
|
||||
|
||||
for (i = 0; i < stats.length; i++) {
|
||||
if (stats[i] && stats[i].type === 'file')
|
||||
bytes += stats[i].size || 0;
|
||||
else if (!stats[i])
|
||||
unknown++;
|
||||
}
|
||||
|
||||
return { kb: Math.ceil(bytes / 1024), unknown: unknown };
|
||||
});
|
||||
}
|
||||
|
||||
// gbCoveredBy <rawEntry> <effectivePath> -- true when <effectivePath> is
|
||||
// exactly <rawEntry>, or lies underneath it as a directory. <rawEntry>'s
|
||||
// own trailing slash (if any -- sysupgrade.conf directory lines commonly
|
||||
// carry one) is normalized away first so both spellings of the same
|
||||
// directory match the same children.
|
||||
// sysupgrade.conf directory lines commonly carry a trailing slash.
|
||||
function gbCoveredBy(rawEntry, effectivePath) {
|
||||
var prefix = rawEntry.replace(/\/+$/, '') + '/';
|
||||
return effectivePath === rawEntry || effectivePath.indexOf(prefix) === 0;
|
||||
}
|
||||
|
||||
// gbAutomaticEntries <rawPaths[]> <effectivePaths[]> -- the "effective" set
|
||||
// entries not explained by anything a human put in the raw list (not equal
|
||||
// to a raw entry, not a descendant of a raw directory entry) -- i.e. only
|
||||
// there because /lib/upgrade/keep.d/* or a changed package conffile
|
||||
// already covers it. Shown read-only, separately from the editable raw
|
||||
// list, so the operator can see the difference between "I added this" and
|
||||
// "this is already covered without me doing anything".
|
||||
// Effective entries no raw line explains: kept by /lib/upgrade/keep.d or a
|
||||
// changed package conffile without the operator adding them.
|
||||
function gbAutomaticEntries(rawPaths, effectivePaths) {
|
||||
return effectivePaths.filter(function(p) {
|
||||
var i;
|
||||
@@ -264,21 +74,12 @@ function gbBuildPathsList(self, paths) {
|
||||
}));
|
||||
}
|
||||
|
||||
function gbBuildSizeBox(kb, unknown) {
|
||||
var children = [];
|
||||
var text;
|
||||
function gbBuildSizeBox(kb) {
|
||||
var children = [
|
||||
E('p', { 'class': 'gitbackup-size' }, [ _('Total size of the backup set: %d KB').format(kb) ])
|
||||
];
|
||||
|
||||
if (kb == null)
|
||||
text = _('Calculating the backup set size…');
|
||||
else
|
||||
text = _('Total size of the backup set: %d KB').format(kb);
|
||||
|
||||
if (unknown)
|
||||
text += ' ' + _('(some entries could not be measured and are left out of this total)');
|
||||
|
||||
children.push(E('p', { 'class': 'gitbackup-size' }, [ text ]));
|
||||
|
||||
if (kb != null && kb > GB_PATHS_WARN_KB) {
|
||||
if (kb > GB_PATHS_WARN_KB) {
|
||||
children.push(E('div', { 'class': 'gitbackup-warn' }, [
|
||||
E('p', {}, [ _('This is over 2 MB. A real "sysupgrade -b" builds this archive entirely in RAM, and past this size it can fail on a low-memory device. This is only a warning -- nothing here is blocked, trim the list above if that matters on this device.') ])
|
||||
]));
|
||||
@@ -330,11 +131,7 @@ function gbBuildAuditBox(self, audit) {
|
||||
}
|
||||
|
||||
var GB_CSS = [
|
||||
'.gitbackup-view { container-type: inline-size; }',
|
||||
'.gitbackup-caption { color: var(--text-color-medium, #666); }',
|
||||
'.gitbackup-actions { display: flex; flex-wrap: wrap; gap: .5em; margin: .75em 0; align-items: center; }',
|
||||
'.gitbackup-actions input[type="text"] { flex: 1 1 260px; min-width: 200px; }',
|
||||
'.gitbackup-card-hint { font-size: .9em; color: var(--text-color-medium, #666); }',
|
||||
'.gitbackup-path-list { list-style: none; margin: .5em 0; padding: 0; }',
|
||||
'.gitbackup-path-row { display: flex; align-items: center; justify-content: space-between; gap: .75em; padding: .4em .6em; border-bottom: 1px solid var(--background-color-medium, #ddd); }',
|
||||
'.gitbackup-path-row:last-child { border-bottom: none; }',
|
||||
@@ -346,9 +143,6 @@ var GB_CSS = [
|
||||
];
|
||||
|
||||
return view.extend({
|
||||
// Not a config form -- see overview.js's own identical comment. Rule 11
|
||||
// of the spec's style guide: the stock Save/Apply/Reset row is switched
|
||||
// off through this documented hook, never hidden with CSS.
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
@@ -360,25 +154,25 @@ return view.extend({
|
||||
]);
|
||||
},
|
||||
|
||||
applyData: function(data) {
|
||||
this._paths = (data[0] && data[0].entries) || [];
|
||||
this._effective = (data[0] && data[0].effective) || [];
|
||||
this._sizeKb = (data[0] && data[0].size_kb) || 0;
|
||||
this._audit = (data[1] && data[1].paths) || [];
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var view;
|
||||
|
||||
self._paths = (data[0] && data[0].entries) || [];
|
||||
self._effective = (data[0] && data[0].effective) || [];
|
||||
self._audit = (data[1] && data[1].paths) || [];
|
||||
self._sizeKb = null;
|
||||
self._sizeUnknown = 0;
|
||||
self.applyData(data);
|
||||
self._body = E('div');
|
||||
|
||||
view = E('div', { 'class': 'gitbackup-view' }, [
|
||||
E('style', { 'type': 'text/css' }, [ GB_CSS.join('\n') ]),
|
||||
E('style', { 'type': 'text/css' }, [ common.css.concat(GB_CSS).join('\n') ]),
|
||||
|
||||
E('h2', {}, [ _('Git Backup - Paths') ]),
|
||||
|
||||
// Spec, verbatim: the caption is itself a feature, not a footnote
|
||||
// -- this edits the same file the router's own firmware-upgrade
|
||||
// tool reads, so anything listed here survives a real sysupgrade
|
||||
// on its own, whether or not gitbackup itself ever runs again.
|
||||
E('p', { 'class': 'gitbackup-caption' },
|
||||
[ _('These paths also survive a firmware upgrade: they are written straight into /etc/sysupgrade.conf, the same file the router\'s own sysupgrade reads, not a separate list of this tool\'s own. A directory you add here stays a directory -- anything added to it later is covered too, exactly like a real sysupgrade would.') ]),
|
||||
|
||||
@@ -402,66 +196,34 @@ return view.extend({
|
||||
}, [ _('Add path') ])
|
||||
]),
|
||||
|
||||
E('div', { 'id': 'gitbackup-paths-body' })
|
||||
self._body
|
||||
]);
|
||||
|
||||
self.renderBody();
|
||||
self.refreshSizes();
|
||||
|
||||
return view;
|
||||
},
|
||||
|
||||
// Rebuilds the size summary, editable path list, automatic-coverage box
|
||||
// and audit box from self._paths/self._effective/self._audit -- called
|
||||
// after every load and every successful edit (commitPaths/refresh
|
||||
// below). Small N (13 keep.d entries on a clean stand plus whatever has
|
||||
// been added), so a full rebuild rather than per-row DOM patching is
|
||||
// simple and cheap enough.
|
||||
renderBody: function() {
|
||||
var self = this;
|
||||
var body = document.getElementById('gitbackup-paths-body');
|
||||
var automatic;
|
||||
|
||||
if (!body)
|
||||
return;
|
||||
|
||||
automatic = gbAutomaticEntries(self._paths, self._effective);
|
||||
// A node reference, not getElementById: render() calls this before
|
||||
// its tree is attached to the document.
|
||||
var body = self._body;
|
||||
var automatic = gbAutomaticEntries(self._paths, self._effective);
|
||||
|
||||
body.textContent = '';
|
||||
body.appendChild(gbBuildSizeBox(self._sizeKb, self._sizeUnknown));
|
||||
body.appendChild(gbBuildSizeBox(self._sizeKb));
|
||||
body.appendChild(gbBuildPathsList(self, self._paths));
|
||||
body.appendChild(gbBuildAutomaticBox(automatic));
|
||||
body.appendChild(gbBuildAuditBox(self, self._audit));
|
||||
},
|
||||
|
||||
refreshSizes: function() {
|
||||
var self = this;
|
||||
var effective = self._effective;
|
||||
|
||||
return gbSumSizes(effective).then(function(res) {
|
||||
// A slower stat batch landing after the effective set moved on
|
||||
// (another edit already completed) would show a total for a
|
||||
// set that is no longer displayed -- discard it instead.
|
||||
if (self._effective !== effective)
|
||||
return;
|
||||
self._sizeKb = res.kb;
|
||||
self._sizeUnknown = res.unknown;
|
||||
self.renderBody();
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function() {
|
||||
var self = this;
|
||||
|
||||
return Promise.all([
|
||||
L.resolveDefault(callListPaths(), null),
|
||||
L.resolveDefault(callAuditPaths(), null)
|
||||
]).then(function(data) {
|
||||
self._paths = (data[0] && data[0].entries) || [];
|
||||
self._effective = (data[0] && data[0].effective) || [];
|
||||
self._audit = (data[1] && data[1].paths) || [];
|
||||
return self.load().then(function(data) {
|
||||
self.applyData(data);
|
||||
self.renderBody();
|
||||
return self.refreshSizes();
|
||||
});
|
||||
},
|
||||
|
||||
@@ -475,14 +237,7 @@ return view.extend({
|
||||
btn.disabled = busy;
|
||||
},
|
||||
|
||||
// commitPaths <newList> [addedPath] -- the one path to `set_paths`.
|
||||
// Reports every backend-side rejection by name and reason (spec: "не
|
||||
// молча"), then always refreshes from `list_paths` regardless of outcome
|
||||
// -- the effective set on disk is the only thing worth trusting after a
|
||||
// write, never this view's own optimistic guess at what changed.
|
||||
// Resolves to true only when `addedPath` (if given) was not among the
|
||||
// rejected entries, so callers adding a single new path know whether to
|
||||
// also clear the input field.
|
||||
// Resolves false when addedPath itself was rejected.
|
||||
commitPaths: function(newList, addedPath) {
|
||||
var self = this;
|
||||
|
||||
@@ -492,8 +247,8 @@ return view.extend({
|
||||
var i;
|
||||
|
||||
for (i = 0; i < rejected.length; i++) {
|
||||
ui.addNotification(null, E('p', {},
|
||||
[ _('%s: %s').format(rejected[i].path, rejected[i].reason) ]), 'error');
|
||||
// The reason already starts with the path.
|
||||
ui.addNotification(null, E('p', {}, [ rejected[i].reason ]), 'error');
|
||||
if (addedPath && rejected[i].path === addedPath)
|
||||
ok = false;
|
||||
}
|
||||
@@ -506,18 +261,10 @@ return view.extend({
|
||||
});
|
||||
},
|
||||
|
||||
// handleAddPath -- only the fast, local checks (non-empty, not already
|
||||
// present, gbPathReject's syntax-only rules) happen before this ever
|
||||
// calls `set_paths`. Existence is deliberately NOT checked here: it
|
||||
// needs a real filesystem, `set_paths` already checks it authoritatively
|
||||
// server-side (gb_paths_validate), and a client-side attempt at the same
|
||||
// question would need `fs.lstat`, which no rpcd ACL here grants -- see
|
||||
// the file-header comment.
|
||||
handleAddPath: function(ev) {
|
||||
var self = this;
|
||||
var input = document.getElementById('gitbackup-paths-add-input');
|
||||
var path = input ? input.value.replace(/^\s+|\s+$/g, '') : '';
|
||||
var reason;
|
||||
var path = input ? input.value.trim() : '';
|
||||
|
||||
if (!path) {
|
||||
ui.addNotification(null, E('p', {}, [ _('Enter a path first.') ]), 'error');
|
||||
@@ -529,12 +276,6 @@ return view.extend({
|
||||
return;
|
||||
}
|
||||
|
||||
reason = gbPathReject(path);
|
||||
if (reason) {
|
||||
ui.addNotification(null, E('p', {}, [ reason ]), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
self.setAddBusy(true);
|
||||
|
||||
return self.commitPaths(self._paths.concat([ path ]), path).then(function(ok) {
|
||||
@@ -544,19 +285,12 @@ return view.extend({
|
||||
});
|
||||
},
|
||||
|
||||
handleAddFromAudit: function(ev, path) {
|
||||
var self = this;
|
||||
var reason = gbPathReject(path);
|
||||
|
||||
if (reason) {
|
||||
ui.addNotification(null, E('p', {}, [ reason ]), 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
return self.commitPaths(self._paths.concat([ path ]), path);
|
||||
// createHandlerFn passes bound args first, the event last.
|
||||
handleAddFromAudit: function(path, ev) {
|
||||
return this.commitPaths(this._paths.concat([ path ]), path);
|
||||
},
|
||||
|
||||
handleRemovePath: function(ev, path) {
|
||||
handleRemovePath: function(path, ev) {
|
||||
var self = this;
|
||||
var idx = self._paths.indexOf(path);
|
||||
var next;
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -4,7 +4,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-openvpn-client
|
||||
PKG_VERSION:=20250227
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
|
||||
LUCI_TITLE:=LuCI support for OpenVPN Client
|
||||
LUCI_DEPENDS:=+openvpn-openssl +kmod-tun
|
||||
|
||||
@@ -129,11 +129,11 @@ start_instance() {
|
||||
cat <<-EOF >> ${TMP_OVPN_PATH}/${cfgid}/iptables_add.sh
|
||||
${TMP_OVPN_PATH}/${cfgid}/iptables_del.sh
|
||||
#允许服务端配置静态路由表访问内网
|
||||
iptables -w -t nat -I postrouting_rule -s \$(cat ${TMP_OVPN_PATH}/${cfgid}/network)/\$(cat ${TMP_OVPN_PATH}/${cfgid}/mask) -m comment --comment "${c_dev}" -j MASQUERADE
|
||||
iptables -w -t nat -I postrouting_rule -o ${c_dev} -m comment --comment "${c_dev}" -j MASQUERADE
|
||||
iptables -w -I input_rule -s \$(cat ${TMP_OVPN_PATH}/${cfgid}/network)/\$(cat ${TMP_OVPN_PATH}/${cfgid}/mask) -m comment --comment "${c_dev}" -j ${c_allow_access_action}
|
||||
iptables -w -I forwarding_rule -i ${c_dev} -m comment --comment "${c_dev}" -j ACCEPT
|
||||
iptables -w -I forwarding_rule -o ${c_dev} -m comment --comment "${c_dev}" -j ACCEPT
|
||||
iptables -w -t nat -I POSTROUTING -s \$(cat ${TMP_OVPN_PATH}/${cfgid}/network)/\$(cat ${TMP_OVPN_PATH}/${cfgid}/mask) -m comment --comment "${c_dev}" -j MASQUERADE
|
||||
iptables -w -t nat -I POSTROUTING -o ${c_dev} -m comment --comment "${c_dev}" -j MASQUERADE
|
||||
iptables -w -I INPUT -s \$(cat ${TMP_OVPN_PATH}/${cfgid}/network)/\$(cat ${TMP_OVPN_PATH}/${cfgid}/mask) -m comment --comment "${c_dev}" -j ${c_allow_access_action}
|
||||
iptables -w -I FORWARD -i ${c_dev} -m comment --comment "${c_dev}" -j ACCEPT
|
||||
iptables -w -I FORWARD -o ${c_dev} -m comment --comment "${c_dev}" -j ACCEPT
|
||||
EOF
|
||||
|
||||
cat <<-EOF >> ${TMP_OVPN_PATH}/${cfgid}/iptables_del.sh
|
||||
@@ -143,9 +143,9 @@ start_instance() {
|
||||
\$1 -w -D \$2 \$index 2>/dev/null
|
||||
done
|
||||
}
|
||||
ipt_del "iptables -w" "input_rule"
|
||||
ipt_del "iptables -w" "forwarding_rule"
|
||||
ipt_del "iptables -w -t nat" "postrouting_rule"
|
||||
ipt_del "iptables -w" "INPUT"
|
||||
ipt_del "iptables -w" "FORWARD"
|
||||
ipt_del "iptables -w -t nat" "POSTROUTING"
|
||||
EOF
|
||||
|
||||
cat <<-EOF >> ${TMP_OVPN_PATH}/${cfgid}/ipchange.sh
|
||||
@@ -162,6 +162,7 @@ start_instance() {
|
||||
|
||||
cat <<-EOF >> /var/etc/${CONFIG}.include
|
||||
${TMP_OVPN_PATH}/${cfgid}/iptables_add.sh
|
||||
exit 0
|
||||
EOF
|
||||
|
||||
local c_routes=$(uci -q get ${CONFIG}.${cfgid}.routes)
|
||||
|
||||
@@ -5,8 +5,15 @@ uci -q batch <<-EOF >/dev/null
|
||||
set firewall.openvpn_client=include
|
||||
set firewall.openvpn_client.type=script
|
||||
set firewall.openvpn_client.path=/var/etc/luci-app-openvpn-client.include
|
||||
set firewall.openvpn_client.reload=1
|
||||
EOF
|
||||
|
||||
if [ -x "/sbin/fw3" ] && [ ! -L "/sbin/fw3" ]; then
|
||||
uci -q set firewall.openvpn_client.reload='1'
|
||||
else
|
||||
uci -q delete firewall.openvpn_client.reload
|
||||
uci -q delete firewall.openvpn_client.fw4_compatible
|
||||
fi
|
||||
uci commit firewall
|
||||
|
||||
rm -rf /tmp/luci-*cache
|
||||
exit 0
|
||||
@@ -1,8 +1,8 @@
|
||||
# SPDX-License-Identifier: GPL-2.0-only
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=1.0.7
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=1.0.9
|
||||
PKG_RELEASE:=3
|
||||
PKG_LICENSE:=GPL-2.0-only AND CC0-1.0 AND MIT AND LicenseRef-Public-Domain AND LicenseRef-fnOS-Trademark
|
||||
LUCI_MAINTAINER:=coolsnowwolf <coolsnowwolf@gmail.com>
|
||||
|
||||
|
||||
@@ -14,15 +14,19 @@ memory usage; no per-client MAC history is stored. No MAC is sent off-router.
|
||||
|
||||
Data comes from [WH-2099/macdb](https://github.com/WH-2099/macdb), which combines
|
||||
IEEE MA-L, MA-M and MA-S assignments. Longest-prefix matching supports 24/28/36
|
||||
bits. Multicast, locally administered/randomized and malformed MACs are ignored.
|
||||
Unknown vendors, absent/random MACs and brands without a bundled icon use a
|
||||
small generic desktop SVG. It is an original GPL-2.0-only asset, not a brand logo.
|
||||
bits. Locally administered unicast MACs (first octet ending in 2, 6, A or E)
|
||||
use a generic phone SVG without an OUI lookup, taking precedence over fnOS
|
||||
and vendor icons. This is a display rule for private/randomized MACs, not proof
|
||||
that the device is a phone. Multicast and malformed MACs are not looked up.
|
||||
Unknown vendors, absent/invalid MACs and brands without a bundled icon use a
|
||||
small generic desktop SVG. Both generic SVGs are original GPL-2.0-only assets,
|
||||
not brand logos.
|
||||
The icon identifies the registered MAC vendor: a PC using an Intel NIC can show
|
||||
Intel rather than the motherboard manufacturer. DHCP hostnames are not used to
|
||||
guess brands.
|
||||
|
||||
Hosts with an open TCP port 5666 or 5667 in the existing LAN web-probe cache
|
||||
show the fnOS logo instead of the OUI icon. The probe must be ready and match
|
||||
show the fnOS logo instead of the OUI icon, except for private MACs. The probe must be ready and match
|
||||
the current IP and MAC; any active IPv4 address of a merged client can qualify.
|
||||
This is a port-based display heuristic, not OS authentication. The vector logo
|
||||
was traced from the supplied `fnos.PNG`; see `NOTICE.fnos`.
|
||||
@@ -60,7 +64,7 @@ Run the regression checks with `python3 tools/test_generate.py` and
|
||||
|
||||
## OEM devices and supplemental brands
|
||||
|
||||
Midea and TCL SVGs supplement the Simple Icons subset. Their individual sources,
|
||||
Midea, TCL and Haier SVGs supplement the Simple Icons subset. Their individual sources,
|
||||
authors and hashes are recorded in `tools/supplemental.json` and the packaged
|
||||
`NOTICE.supplemental`. These are not claimed to be Simple Icons assets.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Midea, TCL and GIGABYTE are supplemental SVGs, not Simple Icons assets.
|
||||
Midea, TCL, GIGABYTE and Haier are supplemental SVGs, not Simple Icons assets.
|
||||
Sources and attribution:
|
||||
{
|
||||
"midea": {
|
||||
@@ -18,5 +18,11 @@ Sources and attribution:
|
||||
"sha256": "49693ef6522f58af4e059afe18c1fb3ddc8e6a5b15ac724d6a7493f47d84d849",
|
||||
"author": "Koman90",
|
||||
"license": "Public domain simple shape/text logo; trademark retained"
|
||||
},
|
||||
"haier": {
|
||||
"source": "https://commons.wikimedia.org/wiki/File:Haier_logo.svg",
|
||||
"sha256": "d71f652fdfb405198761e118e5c4b84fa1de76d9696bb1c13b4980c76ecb6be4",
|
||||
"author": "Haier",
|
||||
"license": "Public domain simple shape/text logo; trademark retained"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
width="824px" height="256px" viewBox="0 0 824 256" enable-background="new 0 0 824 256" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<path fill="#005BAA" d="M712.652,171.532c0.348-3.178,0.552-6.388,0.552-9.638c0-49.205-40.776-89.07-91.083-89.07
|
||||
c-50.293,0-91.063,39.865-91.063,89.07c0,49.189,40.771,89.039,91.063,89.039c38.259,0,70.963-23.021,84.481-55.729l-39.257-6.878
|
||||
c-8.696,15.914-25.151,26.634-43.96,26.634c-24.967,0-45.642-18.778-49.801-43.429H712.652z M623.386,109.79
|
||||
c21.319,0,39.528,13.729,46.96,33.163h-93.977C583.781,123.52,602.03,109.79,623.386,109.79z"/>
|
||||
<rect x="460.066" y="76.555" fill="#005BAA" width="46.512" height="171.18"/>
|
||||
<path fill="#005BAA" d="M483.339,6.758c-14.358,0-25.95,11.655-25.95,25.953c0,14.276,11.592,25.9,25.95,25.9
|
||||
c14.3,0,25.917-11.624,25.917-25.9C509.256,18.413,497.639,6.758,483.339,6.758"/>
|
||||
<polygon fill="#005BAA" points="175.389,21.671 175.389,113.822 75.133,113.822 75.133,21.671 24.291,21.671 24.291,247.582
|
||||
75.133,247.582 75.133,155.427 175.389,155.427 175.389,247.582 226.273,247.582 226.273,21.671 "/>
|
||||
<path fill="#005BAA" d="M385.318,76.634v9.185c-13.519-8.191-29.361-12.896-46.336-12.896c-49.308,0-89.3,39.84-89.3,89.006
|
||||
c0,49.156,39.992,89.006,89.3,89.006c16.534,0,31.981-4.491,45.262-12.27v8.918h45.384V76.634H385.318z M341.25,213.408
|
||||
c-28.777,0-52.04-23.027-52.04-51.459c0-28.456,23.262-51.5,52.04-51.5c28.77,0,52.071,23.044,52.071,51.5
|
||||
C393.32,190.381,370.019,213.408,341.25,213.408z"/>
|
||||
<path fill="#005BAA" d="M809.376,60.657c-42.832,7.472-75.396,44.736-75.896,89.679v97.398h44.535l0.023-99.229
|
||||
c0-22.716,17.053-41.463,39.032-44.153L809.376,60.657z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -7,12 +7,13 @@
|
||||
var base = document.currentScript.src.replace(/[^/]*$/, '');
|
||||
var database, pending, devices = Object.create(null);
|
||||
|
||||
function normalize(mac) {
|
||||
function normalize(mac, allowLocal) {
|
||||
if (typeof mac !== 'string' || !/^(?:[\da-f]{12}|(?:[\da-f]{2}:){5}[\da-f]{2}|(?:[\da-f]{2}-){5}[\da-f]{2})$/i.test(mac))
|
||||
return null;
|
||||
mac = mac.replace(/[:-]/g, '').toUpperCase();
|
||||
// Neither locally administered (randomized) nor multicast addresses have a reliable OUI.
|
||||
return (parseInt(mac.slice(0, 2), 16) & 3) ? null : mac;
|
||||
var flags = parseInt(mac.slice(0, 2), 16);
|
||||
return (flags & 1) || (!allowLocal && (flags & 2)) ? null : mac;
|
||||
}
|
||||
|
||||
function load() {
|
||||
@@ -20,7 +21,7 @@
|
||||
// One local request per page; failed requests are cached too, so polling never retries.
|
||||
pending = new Promise(function(resolve) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', base + 'vendors-451e5311befc.json', true);
|
||||
xhr.open('GET', base + 'vendors-d337209f8c00.json', true);
|
||||
xhr.timeout = 5000;
|
||||
xhr.onload = function() {
|
||||
try {
|
||||
@@ -74,13 +75,20 @@
|
||||
}
|
||||
fallback();
|
||||
node.insertBefore(icon, node.firstChild);
|
||||
var unicast = normalize(mac, true);
|
||||
if (unicast && (parseInt(unicast.slice(0, 2), 16) & 2)) {
|
||||
icon.title = 'Private MAC';
|
||||
icon.onerror = fallback;
|
||||
icon.src = base + 'phone.svg';
|
||||
return;
|
||||
}
|
||||
if (fnos === true) {
|
||||
icon.title = 'fnOS / FygoOS';
|
||||
icon.onerror = fallback;
|
||||
icon.src = base + 'fnos.svg';
|
||||
return;
|
||||
}
|
||||
// Random, malformed or absent addresses use the fallback without an OUI request.
|
||||
// Malformed or absent addresses use the fallback without an OUI request.
|
||||
if (!normalize(mac))
|
||||
return;
|
||||
function apply() {
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
<!-- SPDX-License-Identifier: GPL-2.0-only -->
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#64748b" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="1.5" width="12" height="21" rx="2.5"/><path d="M10 4h4m-3 15.5h2"/></svg>
|
||||
|
After Width: | Height: | Size: 291 B |
@@ -1 +0,0 @@
|
||||
{"vendors":[["apple","Apple"],["asus","ASUS"],["acer","Acer"],["msi","MSI"],["dell","Dell"],["hp","HP"],["lenovo","Lenovo"],["samsung","Samsung"],["huawei","Huawei"],["honor","HONOR"],["xiaomi","Xiaomi"],["oppo","OPPO"],["vivo","vivo"],["oneplus","OnePlus"],["motorola","Motorola"],["nokia","Nokia"],["sony","Sony"],["lg","LG"],["google","Google"],["microsoft","Microsoft"],["intel","Intel"],["nvidia","NVIDIA"],["broadcom","Broadcom"],["mediatek","MediaTek"],["fujitsu","Fujitsu"],["toshiba","Toshiba"],["panasonic","Panasonic"],["sharp","Sharp"],["raspberrypi","Raspberry Pi"],["framework","Framework"],["amazon","Amazon"],["tplink","TP-Link"],["ubiquiti","Ubiquiti"],["netgear","NETGEAR"],["linksys","Linksys"],["synology","Synology"],["qnap","QNAP"],["midea","Midea"],["tcl","TCL"],["gigabyte","GIGABYTE"],["vmware","VMware"]],"prefixes":{"00000E":24,"000039":25,"000095":16,"000097":4,"0000E2":2,"0000EB":26,"0000F0":7,"000124":2,"000144":4,"00014A":16,"0001E6":5,"0001E7":5,"000278":7,"0002A5":5,"0002B3":20,"0002EE":15,"000347":20,"000393":0,"0003FF":19,"00040F":1,"00041F":16,"000423":20,"00044B":21,"00047D":14,"0004EA":5,"000502":0,"000569":40,"0005B5":22,"000600":25,"00065B":4,"0007AB":7,"0007E9":20,"000802":5,"00080D":25,"000874":4,"000883":5,"0008C7":5,"000918":7,"00095B":33,"000A00":23,"000A27":0,"000A28":14,"000A57":5,"000A95":0,"000AD9":16,"000AE0":24,"000AEB":31,"000AF7":22,"000B5D":24,"000B97":26,"000BCD":5,"000BDB":4,"000BE1":15,"000C29":40,"000C43":23,"000C6E":1,"000C76":3,"000CE7":23,"000CF1":20,"000D2E":26,"000D3A":19,"000D56":4,"000D57":24,"000D61":39,"000D93":0,"000D9D":5,"000DAE":7,"000DB6":22,"000DE5":7,"000E07":16,"000E0C":20,"000E1F":38,"000E35":20,"000E7B":25,"000E7F":5,"000EA6":1,"000EB3":5,"000EC7":14,"000EED":15,"000F12":26,"000F1F":4,"000F20":5,"000F61":5,"000FB5":33,"000FBB":15,"000FDE":16,"000FEA":39,"001018":22,"00101E":26,"001055":24,"001083":5,"00108C":24,"0010B3":15,"0010DC":3,"0010E3":5,"0010FA":0,"001109":3,"00110A":5,"001111":20,"001124":0,"00112F":1,"001132":35,"001143":4,"001175":20,"001185":5,"00119F":15,"0011D8":1,"00123F":4,"001247":7,"001248":4,"00125A":19,"001262":15,"001267":26,"001279":5,"0012EE":16,"0012F0":20,"0012FB":7,"0012FE":6,"001302":20,"001315":16,"001320":20,"001321":5,"001343":26,"001370":15,"001372":4,"001377":7,"0013A9":16,"0013CE":20,"0013D3":3,"0013D4":1,"0013E8":20,"0013FD":15,"001422":4,"001451":0,"00146C":33,"001478":31,"0014A7":15,"0014C2":5,"001500":20,"001517":20,"00152A":15,"001530":4,"00155D":19,"001560":5,"00156D":32,"001599":7,"0015A0":15,"0015B7":25,"0015B9":7,"0015C1":16,"0015C5":4,"0015DE":15,"0015F2":1,"001617":3,"001620":16,"001632":7,"001635":5,"00164E":15,"00166B":7,"00166C":7,"00166F":20,"001676":20,"0016B8":16,"0016BC":15,"0016CB":0,"0016DB":7,"0016E6":39,"0016EA":20,"0016EB":20,"0016F0":4,"001708":5,"001731":1,"001735":20,"001742":24,"00174B":15,"00175C":27,"0017A4":5,"0017A5":23,"0017B0":15,"0017C9":7,"0017D5":7,"0017F2":0,"0017FA":19,"00180F":15,"001813":16,"001842":15,"00184D":33,"001871":5,"001882":8,"001885":14,"00188B":4,"00188D":15,"0018AF":7,"0018C5":15,"0018DE":20,"0018F3":1,"0018FE":5,"00192D":15,"00194F":15,"001963":16,"001979":15,"001987":26,"00198F":15,"001999":24,"0019B7":15,"0019B9":4,"0019BB":5,"0019C5":16,"0019D1":20,"0019D2":20,"0019DB":3,"0019E0":31,"0019E3":0,"001A11":18,"001A16":15,"001A4B":5,"001A4D":39,"001A75":16,"001A80":16,"001A89":15,"001A8A":7,"001A92":1,"001AA0":4,"001ADC":15,"001B21":20,"001B2F":33,"001B33":15,"001B59":16,"001B63":0,"001B77":20,"001B78":5,"001B98":7,"001BAF":15,"001BD3":26,"001BE9":22,"001BEE":15,"001BFC":1,"001C14":40,"001C23":4,"001C35":15,"001C43":7,"001C50":38,"001C62":17,"001C7E":25,"001C9A":15,"001CA4":16,"001CB3":0,"001CBF":20,"001CC0":20,"001CC4":5,"001CD4":15,"001CD6":15,"001CEE":27,"001D09":4,"001D0D":16,"001D0F":31,"001D25":7,"001D28":16,"001D3B":15,"001D40":20,"001D4F":0,"001D60":1,"001D6E":15,"001D7D":39,"001D92":3,"001D98":15,"001DBA":16,"001DD8":19,"001DE0":20,"001DE1":20,"001DE9":15,"001DF6":7,"001DFD":15,"001E0B":5,"001E10":8,"001E2A":33,"001E3A":15,"001E3B":15,"001E45":16,"001E4F":4,"001E52":0,"001E64":20,"001E65":20,"001E67":20,"001E75":17,"001E7D":7,"001E8C":1,"001EA3":15,"001EA4":15,"001EC2":0,"001EC9":4,"001EDC":16,"001EE1":7,"001EE2":7,"001F00":15,"001F01":15,"001F29":5,"001F33":33,"001F3B":20,"001F3C":20,"001F5B":0,"001F5C":15,"001F5D":15,"001F6B":17,"001F92":14,"001FA7":16,"001FC6":1,"001FCC":7,"001FCD":7,"001FCF":3,"001FD0":39,"001FDE":15,"001FDF":15,"001FE3":17,"001FE4":16,"001FF3":0,"002075":14,"00207B":20,"0020D9":26,"0020ED":39,"002108":15,"002109":15,"002119":7,"002127":31,"00214C":7,"00215A":5,"00215C":20,"00215D":20,"00216A":20,"00216B":20,"002170":4,"002185":3,"00219B":4,"00219E":16,"0021AA":15,"0021AB":15,"0021D1":7,"0021D2":7,"0021E9":0,"0021FB":17,"0021FC":15,"0021FE":15,"002215":1,"002219":4,"00223F":33,"002241":0,"002248":19,"002264":5,"002265":15,"002266":15,"002298":16,"0022A1":8,"0022A6":16,"0022A9":17,"0022DE":11,"0022F3":27,"0022FA":20,"0022FB":20,"0Line truncated
|
||||
@@ -0,0 +1 @@
|
||||
{"vendors":[["apple","Apple"],["asus","ASUS"],["acer","Acer"],["msi","MSI"],["dell","Dell"],["hp","HP"],["lenovo","Lenovo"],["samsung","Samsung"],["huawei","Huawei"],["honor","HONOR"],["xiaomi","Xiaomi"],["oppo","OPPO"],["vivo","vivo"],["oneplus","OnePlus"],["motorola","Motorola"],["nokia","Nokia"],["sony","Sony"],["lg","LG"],["google","Google"],["microsoft","Microsoft"],["intel","Intel"],["nvidia","NVIDIA"],["broadcom","Broadcom"],["mediatek","MediaTek"],["fujitsu","Fujitsu"],["toshiba","Toshiba"],["panasonic","Panasonic"],["sharp","Sharp"],["raspberrypi","Raspberry Pi"],["framework","Framework"],["amazon","Amazon"],["tplink","TP-Link"],["ubiquiti","Ubiquiti"],["netgear","NETGEAR"],["linksys","Linksys"],["synology","Synology"],["qnap","QNAP"],["midea","Midea"],["tcl","TCL"],["gigabyte","GIGABYTE"],["vmware","VMware"],["haier","Haier / \u6d77\u5c14"]],"prefixes":{"00000E":24,"000039":25,"000095":16,"000097":4,"0000E2":2,"0000EB":26,"0000F0":7,"000124":2,"000144":4,"00014A":16,"0001E6":5,"0001E7":5,"000278":7,"0002A5":5,"0002B3":20,"0002EE":15,"000347":20,"000393":0,"0003FF":19,"00040F":1,"00041F":16,"000423":20,"00044B":21,"00047D":14,"0004EA":5,"000502":0,"000569":40,"0005B5":22,"000600":25,"00065B":4,"0007A8":41,"0007AB":7,"0007E9":20,"000802":5,"00080D":25,"000874":4,"000883":5,"0008C7":5,"000918":7,"00095B":33,"000A00":23,"000A27":0,"000A28":14,"000A57":5,"000A95":0,"000AD9":16,"000AE0":24,"000AEB":31,"000AF7":22,"000B5D":24,"000B97":26,"000BCD":5,"000BDB":4,"000BE1":15,"000C29":40,"000C43":23,"000C6E":1,"000C76":3,"000CE7":23,"000CF1":20,"000D2E":26,"000D3A":19,"000D56":4,"000D57":24,"000D61":39,"000D93":0,"000D9D":5,"000DAE":7,"000DB6":22,"000DE5":7,"000E07":16,"000E0C":20,"000E1F":38,"000E35":20,"000E7B":25,"000E7F":5,"000EA6":1,"000EB3":5,"000EC7":14,"000EED":15,"000F12":26,"000F1F":4,"000F20":5,"000F61":5,"000FB5":33,"000FBB":15,"000FDE":16,"000FEA":39,"001018":22,"00101E":26,"001055":24,"001083":5,"00108C":24,"0010B3":15,"0010DC":3,"0010E3":5,"0010FA":0,"001109":3,"00110A":5,"001111":20,"001124":0,"00112F":1,"001132":35,"001143":4,"001175":20,"001185":5,"00119F":15,"0011D8":1,"00123F":4,"001247":7,"001248":4,"00125A":19,"001262":15,"001267":26,"001279":5,"0012EE":16,"0012F0":20,"0012FB":7,"0012FE":6,"001302":20,"001315":16,"001320":20,"001321":5,"001343":26,"001370":15,"001372":4,"001377":7,"0013A9":16,"0013CE":20,"0013D3":3,"0013D4":1,"0013E8":20,"0013FD":15,"001422":4,"001451":0,"00146C":33,"001478":31,"0014A7":15,"0014C2":5,"001500":20,"001517":20,"00152A":15,"001530":4,"00155D":19,"001560":5,"00156D":32,"001599":7,"0015A0":15,"0015B7":25,"0015B9":7,"0015C1":16,"0015C5":4,"0015DE":15,"0015F2":1,"001617":3,"001620":16,"001632":7,"001635":5,"00164E":15,"00166B":7,"00166C":7,"00166F":20,"001676":20,"0016B8":16,"0016BC":15,"0016CB":0,"0016DB":7,"0016E6":39,"0016EA":20,"0016EB":20,"0016F0":4,"001708":5,"001731":1,"001735":20,"001742":24,"00174B":15,"00175C":27,"0017A4":5,"0017A5":23,"0017B0":15,"0017C9":7,"0017D5":7,"0017F2":0,"0017FA":19,"00180F":15,"001813":16,"001842":15,"00184D":33,"001871":5,"001882":8,"001885":14,"00188B":4,"00188D":15,"0018AF":7,"0018C5":15,"0018DE":20,"0018F3":1,"0018FE":5,"00192D":15,"00194F":15,"001963":16,"001979":15,"001987":26,"00198F":15,"001999":24,"0019B7":15,"0019B9":4,"0019BB":5,"0019C5":16,"0019D1":20,"0019D2":20,"0019DB":3,"0019E0":31,"0019E3":0,"001A11":18,"001A16":15,"001A4B":5,"001A4D":39,"001A75":16,"001A80":16,"001A89":15,"001A8A":7,"001A92":1,"001AA0":4,"001ADC":15,"001B21":20,"001B2F":33,"001B33":15,"001B59":16,"001B63":0,"001B77":20,"001B78":5,"001B98":7,"001BAF":15,"001BD3":26,"001BE9":22,"001BEE":15,"001BFC":1,"001C14":40,"001C23":4,"001C35":15,"001C43":7,"001C50":38,"001C62":17,"001C7E":25,"001C9A":15,"001CA4":16,"001CB3":0,"001CBF":20,"001CC0":20,"001CC4":5,"001CD4":15,"001CD6":15,"001CEE":27,"001D09":4,"001D0D":16,"001D0F":31,"001D25":7,"001D28":16,"001D3B":15,"001D40":20,"001D4F":0,"001D60":1,"001D6E":15,"001D7D":39,"001D92":3,"001D98":15,"001DBA":16,"001DD8":19,"001DE0":20,"001DE1":20,"001DE9":15,"001DF6":7,"001DFD":15,"001E0B":5,"001E10":8,"001E2A":33,"001E3A":15,"001E3B":15,"001E45":16,"001E4F":4,"001E52":0,"001E64":20,"001E65":20,"001E67":20,"001E75":17,"001E7D":7,"001E8C":1,"001EA3":15,"001EA4":15,"001EC2":0,"001EC9":4,"001EDC":16,"001EE1":7,"001EE2":7,"001F00":15,"001F01":15,"001F29":5,"001F33":33,"001F3B":20,"001F3C":20,"001F5B":0,"001F5C":15,"001F5D":15,"001F6B":17,"001F92":14,"001FA7":16,"001FC6":1,"001FCC":7,"001FCD":7,"001FCF":3,"001FD0":39,"001FDE":15,"001FDF":15,"001FE3":17,"001FE4":16,"001FF3":0,"002075":14,"00207B":20,"0020D9":26,"0020ED":39,"002108":15,"002109":15,"002119":7,"002127":31,"00214C":7,"00215A":5,"00215C":20,"00215D":20,"00216A":20,"00216B":20,"002170":4,"002185":3,"00219B":4,"00219E":16,"0021AA":15,"0021AB":15,"0021D1":7,"0021D2":7,"0021E9":0,"0021FB":17,"0021FC":15,"0021FE":15,"002215":1,"002219":4,"00223F":33,"002241":0,"002248":19,"002264":5,"002265":15,"002266":15,"002298":16,"0022A1":8,"0022A6":16,"0022A9":17,"0022Line truncated
|
||||
@@ -2,10 +2,10 @@
|
||||
"simple_icons": "https://github.com/simple-icons/simple-icons/tree/11.15.0",
|
||||
"macdb": {
|
||||
"url": "https://github.com/WH-2099/macdb",
|
||||
"sha256": "dab68f6728825b5e6e894e537ffbc82d2d387eb2ba3d8dd313962dcef8076979"
|
||||
"sha256": "98c197dd8a59c30b89866de97c1e8833fcb4456c753693c447f7403c5d753eb0"
|
||||
},
|
||||
"database": "vendors-451e5311befc.json",
|
||||
"assignments": 8637,
|
||||
"database": "vendors-d337209f8c00.json",
|
||||
"assignments": 8712,
|
||||
"supplemental_icons": {
|
||||
"midea": {
|
||||
"source": "https://commons.wikimedia.org/wiki/File:Midea.svg",
|
||||
@@ -24,6 +24,12 @@
|
||||
"sha256": "49693ef6522f58af4e059afe18c1fb3ddc8e6a5b15ac724d6a7493f47d84d849",
|
||||
"author": "Koman90",
|
||||
"license": "Public domain simple shape/text logo; trademark retained"
|
||||
},
|
||||
"haier": {
|
||||
"source": "https://commons.wikimedia.org/wiki/File:Haier_logo.svg",
|
||||
"sha256": "d71f652fdfb405198761e118e5c4b84fa1de76d9696bb1c13b4980c76ecb6be4",
|
||||
"author": "Haier",
|
||||
"license": "Public domain simple shape/text logo; trademark retained"
|
||||
}
|
||||
},
|
||||
"aliases": {
|
||||
@@ -88,6 +94,18 @@
|
||||
"google": [
|
||||
"Google, Inc."
|
||||
],
|
||||
"haier": [
|
||||
"Beijing Haier IC Design Co.,Ltd",
|
||||
"Haier",
|
||||
"Haier Group Technologies Ltd",
|
||||
"Haier cloud Health Technology (Qingdao) Co., Ltd",
|
||||
"QING DAO HAIER TELECOM CO.,LTD.",
|
||||
"Qingdao Haier Intelligent Home Appliance Technology Co.,Ltd",
|
||||
"Qingdao Haier Multimedia Limited. ",
|
||||
"Qingdao Haier Technology Co.,Ltd",
|
||||
"Qingdao Haier Technology Co.Ltd",
|
||||
"Qingdao HaierTechnology Co.,Ltd"
|
||||
],
|
||||
"honor": [
|
||||
"Honor Device Co., Ltd."
|
||||
],
|
||||
|
||||
@@ -16,5 +16,11 @@
|
||||
"sha256": "49693ef6522f58af4e059afe18c1fb3ddc8e6a5b15ac724d6a7493f47d84d849",
|
||||
"author": "Koman90",
|
||||
"license": "Public domain simple shape/text logo; trademark retained"
|
||||
},
|
||||
"haier": {
|
||||
"source": "https://commons.wikimedia.org/wiki/File:Haier_logo.svg",
|
||||
"sha256": "d71f652fdfb405198761e118e5c4b84fa1de76d9696bb1c13b4980c76ecb6be4",
|
||||
"author": "Haier",
|
||||
"license": "Public domain simple shape/text logo; trademark retained"
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,17 @@ class Aliases(unittest.TestCase):
|
||||
self.assertEqual(self.brand('TCL King Electrical Appliances(Huizhou)Co.,Ltd'), 'tcl')
|
||||
self.assertEqual(self.brand('Hui Zhou Gaoshengda Technology Co.,LTD'), 'tcl')
|
||||
|
||||
def test_haier(self):
|
||||
for name in ['Qingdao Haier Technology Co.,Ltd',
|
||||
'Qingdao HaierTechnology Co.,Ltd',
|
||||
'QING DAO HAIER TELECOM CO.,LTD.', 'Haier']:
|
||||
self.assertEqual(self.brand(name), 'haier')
|
||||
self.assertIsNone(self.brand('Unrelated Haier Module Supplier'))
|
||||
data = json.loads(next((ROOT / 'htdocs/luci-static/resources/oui').glob('vendors-*.json')).read_text())
|
||||
mac = '34:29:EF:89:41:3C'.replace(':', '')
|
||||
self.assertEqual(data['vendors'][data['prefixes'][mac[:6]]][0], 'haier')
|
||||
self.assertEqual(data['vendors'][data['prefixes']['0439CB']][0], 'haier')
|
||||
|
||||
def test_vmware(self):
|
||||
self.assertEqual(self.brand('VMware, Inc.'), 'vmware')
|
||||
self.assertIsNone(self.brand('VMware Unrelated Devices'))
|
||||
|
||||
@@ -26,6 +26,18 @@ function setup(fail = false) {
|
||||
for (const mac of [null, '', '02:11:22:00:00:00', '01:11:22:00:00:00', 'FF:FF:FF:FF:FF:FF', '001122', '00:11-22:33:44:55'])
|
||||
t.oui.decorate(t.node(), mac);
|
||||
assert.equal(t.requests(), 0, 'invalid or private MACs must not fetch data');
|
||||
for (let octet = 0; octet < 256; octet++) {
|
||||
const mac = octet.toString(16).padStart(2, '0') + ':11:22:33:44:55';
|
||||
if (!(octet & 3)) continue;
|
||||
const n = t.node(); t.oui.decorate(n, mac);
|
||||
assert(n.children[0].src.endsWith((octet & 3) === 2 ? '/phone.svg' : '/computer.svg'));
|
||||
}
|
||||
for (const mac of ['a6-11-22-33-44-55', 'AE1122334455', 'BA:71:BE:00:00:01']) {
|
||||
const n = t.node(); t.oui.decorate(n, mac, true);
|
||||
assert(n.children[0].src.endsWith('/phone.svg'), 'private MAC must take precedence over fnOS');
|
||||
n.children[0].onerror(); assert(n.children[0].src.endsWith('/computer.svg'));
|
||||
}
|
||||
assert.equal(t.requests(), 0, 'phone icons must not fetch the OUI database');
|
||||
const cases = [['00:11:22:00:00:00','ASUS'],['00-11-22-35-00-00','Apple'],['001122334455','ASUS'],['00:11:22:40:00:00',null],['00:AB:CD:00:00:00',null]];
|
||||
const nodes = cases.map(([mac]) => { const n=t.node(); t.oui.decorate(n,mac); return n; });
|
||||
await Promise.resolve(); await Promise.resolve();
|
||||
@@ -42,7 +54,7 @@ function setup(fail = false) {
|
||||
assert.equal(other.children[0].title,'Unknown vendor','OEM overrides must not affect other devices in the same OUI');
|
||||
assert.equal(unsafe.children[0].title,'Unknown vendor');
|
||||
const fnos=t.node();
|
||||
t.oui.decorate(fnos,'BA:71:BE:00:00:01',true);
|
||||
t.oui.decorate(fnos,'B8:71:BE:00:00:01',true);
|
||||
assert.equal(fnos.children[0].title,'fnOS / FygoOS');
|
||||
assert(fnos.children[0].src.endsWith('/fnos.svg'));
|
||||
const overridden=t.node();
|
||||
@@ -71,7 +83,7 @@ function setup(fail = false) {
|
||||
enabled = true;
|
||||
const host = view.renderHostname('<img src=x>', '00:11:22:00:00:00');
|
||||
assert.equal(host.children[0].text, '<img src=x>');
|
||||
assert.equal(scripts[0].src, '/luci-static/resources/oui/oui.js?v=8');
|
||||
assert.equal(scripts[0].src, '/luci-static/resources/oui/oui.js?v=10');
|
||||
view.renderHostname('host2', '00:11:22:00:00:01');
|
||||
assert.equal(scripts.length, 1);
|
||||
console.log('OUI runtime and optional integration checks passed');
|
||||
|
||||
@@ -295,5 +295,13 @@
|
||||
"patterns": [
|
||||
"^vmware(?: inc)?$"
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "haier",
|
||||
"name": "Haier / \u6d77\u5c14",
|
||||
"patterns": [
|
||||
"^(?:(?:qing ?dao|beijing) )?haier(?: |technology|$)"
|
||||
],
|
||||
"supplemental": true
|
||||
}
|
||||
]
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.16
|
||||
PKG_RELEASE:=289
|
||||
PKG_RELEASE:=290
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -1470,6 +1470,10 @@ function gen_config(var)
|
||||
end
|
||||
end
|
||||
if to_node then
|
||||
local chained_tag = outbound.tag .. " -> " .. to_node[".name"] .. (to_node.remarks and ":" .. to_node.remarks or "")
|
||||
for _, o in ipairs(outbounds_table) do
|
||||
if o.tag == chained_tag then return chained_tag, last_insert_outbound end
|
||||
end
|
||||
local to_outbound
|
||||
if to_node.type ~= "sing-box" then
|
||||
local tag = to_node[".name"]
|
||||
@@ -1531,7 +1535,7 @@ function gen_config(var)
|
||||
local outbound, exist
|
||||
if node.protocol == "_urltest" then
|
||||
outbound, exist = gen_urltest_outbound(node)
|
||||
if exist then
|
||||
if exist and not node.chain_proxy then
|
||||
return outbound.tag
|
||||
end
|
||||
elseif node.protocol == "_iface" then
|
||||
@@ -1559,7 +1563,7 @@ function gen_config(var)
|
||||
end
|
||||
if outbound then
|
||||
local default_outbound_tag, last_insert_outbound = set_outbound_detour(node, outbound, outbounds)
|
||||
table.insert(outbounds, outbound)
|
||||
if not exist then table.insert(outbounds, outbound) end
|
||||
if last_insert_outbound then
|
||||
table.insert(outbounds, last_insert_outbound)
|
||||
end
|
||||
|
||||
@@ -81,7 +81,7 @@ run_ipt2socks() {
|
||||
[ -n "$tcp_tproxy" ] || _extra_param="${_extra_param} -R"
|
||||
flag="${flag}_TCP_UDP"
|
||||
_extra_param="${_extra_param} -o 60 -n 65535 -v"
|
||||
ln_run "$(first_type ipt2socks)" "ipt2socks_${flag}" $log_file -l $local_port -b 0.0.0.0 -s $socks_address -p $socks_port ${_extra_param}
|
||||
ln_run "$(first_type ipt2socks)" "ipt2socks_${flag}" $log_file -l $local_port -b 0.0.0.0 -B :: -s $socks_address -p $socks_port ${_extra_param}
|
||||
}
|
||||
|
||||
run_singbox() {
|
||||
|
||||
@@ -9,7 +9,7 @@ LUCI_TITLE:=LuCI support for SSR Mudb Server
|
||||
LUCI_DEPENDS:=+libsodium +luci-lib-jsonc +python3
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=20211211
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=4
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -49,6 +49,7 @@ gen_include() {
|
||||
iptables-restore -n <<-EOT
|
||||
$(extract_rules filter)
|
||||
EOT
|
||||
exit 0
|
||||
EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -5,9 +5,16 @@ uci -q batch <<-EOF >/dev/null
|
||||
set firewall.ssr_mudb_server=include
|
||||
set firewall.ssr_mudb_server.type=script
|
||||
set firewall.ssr_mudb_server.path=/var/etc/ssr_mudb_server.include
|
||||
set firewall.ssr_mudb_server.reload=1
|
||||
EOF
|
||||
|
||||
if [ -x "/sbin/fw3" ] && [ ! -L "/sbin/fw3" ]; then
|
||||
uci -q set firewall.ssr_mudb_server.reload='1'
|
||||
else
|
||||
uci -q delete firewall.ssr_mudb_server.reload
|
||||
uci -q delete firewall.ssr_mudb_server.fw4_compatible
|
||||
fi
|
||||
uci commit firewall
|
||||
|
||||
chmod a+x /usr/share/ssr_mudb_server/* >/dev/null 2>&1
|
||||
cp -rf /usr/bin/python3 /usr/bin/python
|
||||
rm -rf /tmp/luci-*cache
|
||||
|
||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=196
|
||||
PKG_RELEASE:=74
|
||||
PKG_RELEASE:=75
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
|
||||
@@ -12,6 +12,9 @@ local json = require "luci.jsonc"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local translate = i18n.translate
|
||||
|
||||
-- ============================================================
|
||||
-- 常量
|
||||
-- ============================================================
|
||||
local CLASH_API_PORT = "16756"
|
||||
local COMPONENT_HELPER = "/usr/share/shadowsocksr/update_components.sh"
|
||||
local SERVER_DETECT_CACHE = "/tmp/ssrplus_server_detect.json"
|
||||
@@ -30,10 +33,34 @@ local SUPPORTED_GEO_COMPONENTS = {
|
||||
{ name = "v2ray_geosite", require = nil }
|
||||
}
|
||||
|
||||
-- ============================================================
|
||||
-- 通用工具函数
|
||||
-- ============================================================
|
||||
local function trim(value)
|
||||
return tostring(value or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
|
||||
local function shell_quote(value)
|
||||
return "'" .. tostring(value or ""):gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
function uci_get_config(section, key)
|
||||
if section ~= "global" then
|
||||
return nil
|
||||
end
|
||||
local val = luci.sys.exec(string.format(
|
||||
'uci -q get shadowsocksr.@global[0].%s 2>/dev/null', key))
|
||||
val = val:gsub("%s+$", "")
|
||||
if val == "" then
|
||||
return nil
|
||||
end
|
||||
return val
|
||||
end
|
||||
|
||||
function isdirectory(dirname)
|
||||
return nixio.fs.stat(dirname, "type") == "dir"
|
||||
end
|
||||
|
||||
local function parse_nonnegative_int(value)
|
||||
local number = tonumber(value)
|
||||
if not number then
|
||||
@@ -65,6 +92,49 @@ local function normalize_client_ip(value)
|
||||
return ""
|
||||
end
|
||||
|
||||
local function urlencode(str)
|
||||
if not str then return "" end
|
||||
return tostring(str):gsub("[^%w%-_%.~]", function(c)
|
||||
return string.format("%%%02X", string.byte(c))
|
||||
end)
|
||||
end
|
||||
|
||||
local function is_ipv6_address(addr)
|
||||
addr = tostring(addr or "")
|
||||
return addr ~= "" and addr:find(":", 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function is_local_target(addr)
|
||||
addr = tostring(addr or ""):lower()
|
||||
if addr == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if addr == "localhost" or addr == "::1" or addr:match("%.local$") then
|
||||
return true
|
||||
end
|
||||
|
||||
if is_ipv6_address(addr) then
|
||||
return addr:match("^fe[89ab]") ~= nil or addr:match("^fc") ~= nil or addr:match("^fd") ~= nil
|
||||
end
|
||||
|
||||
local o1, o2 = addr:match("^(%d+)%.(%d+)%.")
|
||||
o1 = tonumber(o1)
|
||||
o2 = tonumber(o2)
|
||||
if not o1 or not o2 then
|
||||
return false
|
||||
end
|
||||
|
||||
return o1 == 10
|
||||
or o1 == 127
|
||||
or (o1 == 169 and o2 == 254)
|
||||
or (o1 == 172 and o2 >= 16 and o2 <= 31)
|
||||
or (o1 == 192 and o2 == 168)
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- CSV 处理(Clash 客户端规则)
|
||||
-- ============================================================
|
||||
local function get_clash_client_rule_csv_path(sid)
|
||||
sid = trim(sid)
|
||||
if sid == "" then
|
||||
@@ -167,6 +237,13 @@ local function write_clash_client_rules_csv(sid, rows)
|
||||
return nixio.fs.writefile(csv_path, table.concat(lines, "\n") .. "\n")
|
||||
end
|
||||
|
||||
local function read_clash_client_rules(sid)
|
||||
return read_clash_client_rules_csv(sid)
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- LAN 客户端收集
|
||||
-- ============================================================
|
||||
local function collect_lan_clients()
|
||||
local clients = {}
|
||||
local seen = {}
|
||||
@@ -191,10 +268,9 @@ local function collect_lan_clients()
|
||||
return clients
|
||||
end
|
||||
|
||||
local function read_clash_client_rules(sid)
|
||||
return read_clash_client_rules_csv(sid)
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 延迟探测
|
||||
-- ============================================================
|
||||
local function normalize_ping_ms(value, scale)
|
||||
local num = tonumber(value)
|
||||
if not num or num <= 0 then
|
||||
@@ -247,46 +323,6 @@ local function detect_tls_handshake_ms(domain, port, path, resolve_host, server_
|
||||
return nil
|
||||
end
|
||||
|
||||
local function urlencode(str)
|
||||
if not str then return "" end
|
||||
return tostring(str):gsub("[^%w%-_%.~]", function(c)
|
||||
return string.format("%%%02X", string.byte(c))
|
||||
end)
|
||||
end
|
||||
|
||||
local function is_ipv6_address(addr)
|
||||
addr = tostring(addr or "")
|
||||
return addr ~= "" and addr:find(":", 1, true) ~= nil
|
||||
end
|
||||
|
||||
local function is_local_target(addr)
|
||||
addr = tostring(addr or ""):lower()
|
||||
if addr == "" then
|
||||
return false
|
||||
end
|
||||
|
||||
if addr == "localhost" or addr == "::1" or addr:match("%.local$") then
|
||||
return true
|
||||
end
|
||||
|
||||
if is_ipv6_address(addr) then
|
||||
return addr:match("^fe[89ab]") ~= nil or addr:match("^fc") ~= nil or addr:match("^fd") ~= nil
|
||||
end
|
||||
|
||||
local o1, o2 = addr:match("^(%d+)%.(%d+)%.")
|
||||
o1 = tonumber(o1)
|
||||
o2 = tonumber(o2)
|
||||
if not o1 or not o2 then
|
||||
return false
|
||||
end
|
||||
|
||||
return o1 == 10
|
||||
or o1 == 127
|
||||
or (o1 == 169 and o2 == 254)
|
||||
or (o1 == 172 and o2 >= 16 and o2 <= 31)
|
||||
or (o1 == 192 and o2 == 168)
|
||||
end
|
||||
|
||||
local function detect_tcp_connect_ms(domain, port)
|
||||
if not domain or domain == "" or not port or port <= 0 then
|
||||
return nil
|
||||
@@ -312,6 +348,9 @@ local function detect_tcp_connect_ms(domain, port)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Clash 相关
|
||||
-- ============================================================
|
||||
local function get_clash_secret(sid)
|
||||
return sid .. "_ssrplus_clash"
|
||||
end
|
||||
@@ -561,6 +600,9 @@ local function save_detect_cache_entry(sid, data)
|
||||
end)
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 组件 / Geo 状态读取与写 JSON
|
||||
-- ============================================================
|
||||
local function read_component_state(component, action)
|
||||
if not SUPPORTED_COMPONENTS[component] then
|
||||
return nil, 400, "unsupported_component"
|
||||
@@ -632,10 +674,9 @@ local function write_geo_json(data)
|
||||
})
|
||||
end
|
||||
|
||||
local function shell_quote(value)
|
||||
return "'" .. tostring(value or ""):gsub("'", "'\\''") .. "'"
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 路由注册
|
||||
-- ============================================================
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/shadowsocksr") then
|
||||
call("act_reset")
|
||||
@@ -644,6 +685,8 @@ function index()
|
||||
page = entry({"admin", "services", "shadowsocksr"}, alias("admin", "services", "shadowsocksr", "client"), _("ShadowSocksR Plus+"), 10)
|
||||
page.dependent = true
|
||||
page.acl_depends = { "luci-app-ssr-plus" }
|
||||
|
||||
-- 页面
|
||||
entry({"admin", "services", "shadowsocksr", "client"}, cbi("shadowsocksr/client"), _("SSR Client"), 10).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "servers"}, arcombine(cbi("shadowsocksr/servers"), cbi("shadowsocksr/client-config")), _("Servers Nodes"), 20).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "control"}, cbi("shadowsocksr/control"), _("Access Control"), 30).leaf = true
|
||||
@@ -651,31 +694,41 @@ function index()
|
||||
entry({"admin", "services", "shadowsocksr", "server"}, arcombine(cbi("shadowsocksr/server"), cbi("shadowsocksr/server-config")), _("SSR Server"), 60).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "component"}, cbi("shadowsocksr/component"), _("Component Update"), 65).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "status"}, form("shadowsocksr/status"), _("Status"), 70).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "log"}, form("shadowsocksr/log"), _("Log"), 80).leaf = true
|
||||
|
||||
-- 状态 / 操作
|
||||
entry({"admin", "services", "shadowsocksr", "check"}, call("check_status"))
|
||||
entry({"admin", "services", "shadowsocksr", "refresh"}, call("refresh_data"))
|
||||
entry({"admin", "services", "shadowsocksr", "subscribe"}, call("subscribe"))
|
||||
entry({"admin", "services", "shadowsocksr", "component_local_status"}, call("component_local_status")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "component_set_mirror"}, call("component_set_mirror")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "component_status"}, call("component_status")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "component_upgrade"}, call("component_upgrade")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "geo_component"}, call("geo_component")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "geo_local_status"}, call("geo_local_status")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "geo_status"}, call("geo_status")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "geo_upgrade"}, call("geo_upgrade")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "checkport"}, call("check_port"))
|
||||
entry({"admin", "services", "shadowsocksr", "log"}, form("shadowsocksr/log"), _("Log"), 80).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "get_log"}, call("get_log")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "clear_log"}, call("clear_log")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "run"}, call("act_status"))
|
||||
entry({"admin", "services", "shadowsocksr", "ping"}, call("act_ping"))
|
||||
entry({"admin", "services", "shadowsocksr", "save_order"}, call("save_order")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "delete_node"}, call("act_delete_node")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "add_subscribe_item"}, call("add_subscribe_item")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "delete_subscribe_item"}, call("delete_subscribe_item")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled"}, call("toggle_subscribe_item_enabled")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "reset"}, call("act_reset"))
|
||||
entry({"admin", "services", "shadowsocksr", "restart"}, call("act_restart"))
|
||||
entry({"admin", "services", "shadowsocksr", "delete"}, call("act_delete"))
|
||||
entry({"admin", "services", "shadowsocksr", "save_order"}, call("save_order")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "delete_node"}, call("act_delete_node")).leaf = true
|
||||
|
||||
-- 订阅
|
||||
entry({"admin", "services", "shadowsocksr", "add_subscribe_item"}, call("add_subscribe_item")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "delete_subscribe_item"}, call("delete_subscribe_item")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "toggle_subscribe_item_enabled"}, call("toggle_subscribe_item_enabled")).leaf = true
|
||||
|
||||
-- 组件
|
||||
entry({"admin", "services", "shadowsocksr", "component_local_status"}, call("component_local_status")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "component_set_mirror"}, call("component_set_mirror")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "component_status"}, call("component_status")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "component_upgrade"}, call("component_upgrade")).leaf = true
|
||||
|
||||
-- Geo
|
||||
entry({"admin", "services", "shadowsocksr", "geo_component"}, call("geo_component")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "geo_local_status"}, call("geo_local_status")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "geo_status"}, call("geo_status")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "geo_upgrade"}, call("geo_upgrade")).leaf = true
|
||||
|
||||
-- Clash
|
||||
entry({"admin", "services", "shadowsocksr", "clash_panel"}, call("clash_panel")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "clash_groups"}, call("clash_groups")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "clash_switch"}, call("clash_switch")).leaf = true
|
||||
@@ -684,9 +737,18 @@ function index()
|
||||
entry({"admin", "services", "shadowsocksr", "clash_client_policies"}, call("clash_client_policies")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "clash_client_rule_save"}, call("clash_client_rule_save")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "clash_client_rule_clear"}, call("clash_client_rule_clear")).leaf = true
|
||||
|
||||
-- 证书
|
||||
entry({"admin", "services", "shadowsocksr", "fetch_certsha256"}, call("fetch_certsha256")).leaf = true
|
||||
entry({"admin", "services", "shadowsocksr", "fetch_certbyname"}, call("fetch_certbyname")).leaf = true
|
||||
--[[Backup]]
|
||||
|
||||
-- Dashboard
|
||||
entry({"admin", "services", "shadowsocksr", "dashboard_type"}, call("action_dashboard_type"))
|
||||
entry({"admin", "services", "shadowsocksr", "switch_dashboard"}, call("action_switch_dashboard"))
|
||||
entry({"admin", "services", "shadowsocksr", "delete_dashboard"}, call("action_delete_dashboard"))
|
||||
entry({"admin", "services", "shadowsocksr", "default_dashboard"}, call("action_default_dashboard"))
|
||||
|
||||
-- 备份
|
||||
entry({"admin", "services", "shadowsocksr", "backup"}, call("create_backup")).leaf = true
|
||||
entry({'admin', 'services', "shadowsocksr", 'ip'}, call('check_ip')) -- 获取ip情况
|
||||
end
|
||||
@@ -746,6 +808,9 @@ function check_ip()
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 订阅
|
||||
-- ============================================================
|
||||
function subscribe()
|
||||
nixio.fs.remove(SERVER_DETECT_CACHE)
|
||||
local sid = luci.http.formvalue("sid") or ""
|
||||
@@ -1004,6 +1069,175 @@ function toggle_subscribe_item_enabled()
|
||||
luci.http.write_json({ ret = 1, sid = sid, field = field, value = value })
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Dashboard 面板管理
|
||||
-- ============================================================
|
||||
function action_switch_dashboard()
|
||||
local switch_name = luci.http.formvalue("name")
|
||||
local switch_type = luci.http.formvalue("type")
|
||||
local state = luci.sys.call(string.format(
|
||||
'/usr/share/shadowsocksr/update_dashboard.sh "%s" "%s" >/dev/null 2>&1',
|
||||
switch_name, switch_type))
|
||||
|
||||
-- 写日志
|
||||
local f = io.open("/var/log/ssrplus.log", "a")
|
||||
if f then
|
||||
local result
|
||||
if tonumber(state) == 0 then
|
||||
result = os.date("%Y-%m-%d %H:%M:%S: ") ..
|
||||
string.format("Control Panel【%s - %s】Download Successful!", switch_name, switch_type)
|
||||
else
|
||||
result = os.date("%Y-%m-%d %H:%M:%S: ") ..
|
||||
string.format("Control Panel【%s - %s】Download Error!", switch_name, switch_type)
|
||||
end
|
||||
f:write(result .. "\n")
|
||||
f:close()
|
||||
end
|
||||
|
||||
if tonumber(state) == 0 then
|
||||
local target
|
||||
if switch_type == "Official" then
|
||||
target = "Official"
|
||||
else
|
||||
target = "Meta"
|
||||
end
|
||||
|
||||
if switch_name == "Dashboard" then
|
||||
luci.sys.call(string.format(
|
||||
'uci -q set shadowsocksr.@global[0].dashboard_type="%s"', target))
|
||||
elseif switch_name == "Yacd" then
|
||||
luci.sys.call(string.format(
|
||||
'uci -q set shadowsocksr.@global[0].yacd_type="%s"', target))
|
||||
else
|
||||
luci.sys.call(
|
||||
'uci -q set shadowsocksr.@global[0].dashboard_type="Official"')
|
||||
end
|
||||
luci.sys.call('uci -q commit shadowsocksr')
|
||||
end
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
download_state = state;
|
||||
})
|
||||
end
|
||||
|
||||
function action_delete_dashboard()
|
||||
local delete_name = luci.http.formvalue("name")
|
||||
local delete_path = string.format("/usr/share/shadowsocksr/ui/%s", string.lower(delete_name))
|
||||
|
||||
local panels = {
|
||||
"/usr/share/shadowsocksr/ui/dashboard",
|
||||
"/usr/share/shadowsocksr/ui/yacd",
|
||||
"/usr/share/shadowsocksr/ui/metacubexd",
|
||||
"/usr/share/shadowsocksr/ui/zashboard"
|
||||
}
|
||||
local existing_panels = 0
|
||||
for _, path in ipairs(panels) do
|
||||
if isdirectory(path) then
|
||||
existing_panels = existing_panels + 1
|
||||
end
|
||||
end
|
||||
|
||||
if existing_panels <= 1 then
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
delete_state = 0,
|
||||
error = "Cannot delete the last remaining dashboard"
|
||||
})
|
||||
return
|
||||
end
|
||||
|
||||
local state = luci.sys.call(string.format("rm -rf '%s' >/dev/null 2>&1", delete_path)) == 0 and 1 or 0
|
||||
if tonumber(state) == 1 then
|
||||
if delete_name == "Dashboard" then
|
||||
luci.sys.call('uci -q set shadowsocksr.@global[0].dashboard_type="Official"')
|
||||
luci.sys.call('uci -q commit shadowsocksr')
|
||||
elseif delete_name == "Yacd" then
|
||||
luci.sys.call('uci -q set shadowsocksr.@global[0].yacd_type="Official"')
|
||||
luci.sys.call('uci -q commit shadowsocksr')
|
||||
end
|
||||
|
||||
-- 检查 default_dashboard
|
||||
local default_dashboard = luci.sys.exec(
|
||||
'uci -q get shadowsocksr.@global[0].default_dashboard 2>/dev/null')
|
||||
default_dashboard = default_dashboard:gsub("%s+$", "")
|
||||
|
||||
if default_dashboard == string.lower(delete_name) then
|
||||
luci.sys.call('uci -q set shadowsocksr.@global[0].default_dashboard=""')
|
||||
luci.sys.call('uci -q commit shadowsocksr')
|
||||
end
|
||||
end
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
delete_state = state;
|
||||
})
|
||||
end
|
||||
|
||||
function action_dashboard_type()
|
||||
local dashboard_type = luci.sys.exec(
|
||||
'uci -q get shadowsocksr.@global[0].dashboard_type 2>/dev/null')
|
||||
dashboard_type = dashboard_type:gsub("%s+$", "")
|
||||
if dashboard_type == "" then
|
||||
dashboard_type = "Official"
|
||||
end
|
||||
|
||||
local yacd_type = luci.sys.exec(
|
||||
'uci -q get shadowsocksr.@global[0].yacd_type 2>/dev/null')
|
||||
yacd_type = yacd_type:gsub("%s+$", "")
|
||||
if yacd_type == "" then
|
||||
yacd_type = "Official"
|
||||
end
|
||||
|
||||
local default_dashboard = luci.sys.exec(
|
||||
'uci -q get shadowsocksr.@global[0].default_dashboard 2>/dev/null')
|
||||
default_dashboard = default_dashboard:gsub("%s+$", "")
|
||||
|
||||
if not isdirectory("/usr/share/shadowsocksr/ui/" .. default_dashboard) then
|
||||
default_dashboard = ""
|
||||
end
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
dashboard_type = dashboard_type,
|
||||
yacd_type = yacd_type,
|
||||
yacd = isdirectory("/usr/share/shadowsocksr/ui/yacd"),
|
||||
dashboard = isdirectory("/usr/share/shadowsocksr/ui/dashboard"),
|
||||
metacubexd = isdirectory("/usr/share/shadowsocksr/ui/metacubexd"),
|
||||
zashboard = isdirectory("/usr/share/shadowsocksr/ui/zashboard"),
|
||||
default_dashboard = default_dashboard;
|
||||
})
|
||||
end
|
||||
|
||||
function action_default_dashboard()
|
||||
local default_dashboard = luci.http.formvalue("name")
|
||||
if not default_dashboard
|
||||
or (default_dashboard ~= "Dashboard"
|
||||
and default_dashboard ~= "Yacd"
|
||||
and default_dashboard ~= "Metacubexd"
|
||||
and default_dashboard ~= "Zashboard") then
|
||||
luci.http.status(500, "Set Failed")
|
||||
return
|
||||
end
|
||||
if not isdirectory("/usr/share/shadowsocksr/ui/" .. string.lower(default_dashboard)) then
|
||||
luci.http.status(500, "Set Failed")
|
||||
return
|
||||
end
|
||||
|
||||
luci.sys.call(string.format(
|
||||
'uci -q set shadowsocksr.@global[0].default_dashboard="%s"',
|
||||
string.lower(default_dashboard)))
|
||||
luci.sys.call('uci -q commit shadowsocksr')
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({
|
||||
default_dashboard = default_dashboard;
|
||||
})
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 组件更新
|
||||
-- ============================================================
|
||||
function component_status()
|
||||
local component = luci.http.formvalue("component")
|
||||
local data, status, err = read_component_state(component, "info")
|
||||
@@ -1071,6 +1305,9 @@ function component_upgrade()
|
||||
write_component_json(data)
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Geo 更新
|
||||
-- ============================================================
|
||||
function geo_status()
|
||||
local component = luci.http.formvalue("component")
|
||||
local data, status, err = read_geo_state(component, "info")
|
||||
@@ -1127,10 +1364,13 @@ function geo_upgrade()
|
||||
data.can_upgrade = info.can_upgrade
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
write_geo_json(data)
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 状态
|
||||
-- ============================================================
|
||||
function act_status()
|
||||
local e = {}
|
||||
e.running = global_client_running()
|
||||
@@ -1141,6 +1381,88 @@ function act_status()
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
|
||||
function check_status()
|
||||
local e = {}
|
||||
local target = luci.http.formvalue("set") or ""
|
||||
e.ret = luci.sys.call("curl -m 3 -sS -o /dev/null http://www." .. target .. ".com >/dev/null 2>&1")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
|
||||
function refresh_data()
|
||||
local set = luci.http.formvalue("set")
|
||||
local retstring = loadstring("return " .. luci.sys.exec("/usr/bin/lua /usr/share/shadowsocksr/update.lua " .. set))()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(retstring)
|
||||
end
|
||||
|
||||
function check_port()
|
||||
local retstring = "<br /><br />"
|
||||
local s
|
||||
local server_name = ""
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local use_nft = use_fw4_backend()
|
||||
|
||||
uci:foreach("shadowsocksr", "servers", function(s)
|
||||
if s.type == "clash" then
|
||||
retstring = retstring .. string.format("<font><b style='color:gray'>[%s] Clash panel node.</b></font><br />", s.alias or s[".name"])
|
||||
return
|
||||
end
|
||||
if s.alias then
|
||||
server_name = s.alias
|
||||
elseif s.server and s.server_port then
|
||||
server_name = s.server .. ":" .. s.server_port
|
||||
end
|
||||
|
||||
-- 临时加入 set
|
||||
local is_ipv6 = is_ipv6_address(s.server)
|
||||
local iret = false
|
||||
if not is_ipv6 then
|
||||
if use_nft then
|
||||
iret = luci.sys.call("nft add element inet ss_spec ss_spec_wan_ac { " .. s.server .. " } 2>/dev/null") == 0
|
||||
else
|
||||
iret = luci.sys.call("ipset add ss_spec_wan_ac " .. s.server .. " 2>/dev/null") == 0
|
||||
end
|
||||
end
|
||||
|
||||
-- TCP 测试
|
||||
local socket = nixio.socket(is_ipv6 and "inet6" or "inet", "stream")
|
||||
socket:setopt("socket", "rcvtimeo", 3)
|
||||
socket:setopt("socket", "sndtimeo", 3)
|
||||
local ret = socket:connect(s.server, s.server_port)
|
||||
socket:close()
|
||||
|
||||
if ret then
|
||||
retstring = retstring .. string.format("<font><b style='color:green'>[%s] OK.</b></font><br />", server_name)
|
||||
else
|
||||
retstring = retstring .. string.format("<font><b style='color:red'>[%s] Error.</b></font><br />", server_name)
|
||||
end
|
||||
|
||||
-- 删除临时 set
|
||||
if iret then
|
||||
if use_nft then
|
||||
luci.sys.call("nft delete element inet ss_spec ss_spec_wan_ac { " .. s.server .. " } 2>/dev/null")
|
||||
else
|
||||
luci.sys.call("ipset del ss_spec_wan_ac " .. s.server)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({ret = retstring})
|
||||
end
|
||||
|
||||
function get_log()
|
||||
luci.http.write(luci.sys.exec("[ -f '/var/log/ssrplus.log' ] && cat /var/log/ssrplus.log"))
|
||||
end
|
||||
|
||||
function clear_log()
|
||||
luci.sys.call("echo '' > /var/log/ssrplus.log")
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Clash 面板
|
||||
-- ============================================================
|
||||
function clash_panel()
|
||||
local sid = luci.http.formvalue("sid")
|
||||
if uci:get("shadowsocksr", sid) ~= "servers" or uci:get("shadowsocksr", sid, "type") ~= "clash" then
|
||||
@@ -1379,6 +1701,9 @@ function clash_client_rule_clear()
|
||||
})
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 证书
|
||||
-- ============================================================
|
||||
function fetch_certsha256()
|
||||
local function fetch_cert_sha256(host, port, sni, timeout, http3)
|
||||
if not host then return "" end
|
||||
@@ -1443,16 +1768,16 @@ function fetch_certbyname()
|
||||
port = tonumber(port) or 443
|
||||
sni = sni or host
|
||||
timeout = tonumber(timeout) or 5
|
||||
|
||||
|
||||
local cmd = string.format(
|
||||
"timeout %d openssl s_client -connect %s:%d -servername %s -showcerts </dev/null 2>/dev/null " ..
|
||||
"| openssl x509 -noout -subject 2>/dev/null " ..
|
||||
"| awk '{gsub(/^.*=[[:space:]]*/, \"\"); gsub(/,.*$/, \"\"); print}'",
|
||||
timeout, host, port, sni
|
||||
)
|
||||
|
||||
|
||||
local out = trim(luci.sys.exec(cmd))
|
||||
|
||||
|
||||
if out == "" then
|
||||
return ""
|
||||
end
|
||||
@@ -1475,6 +1800,9 @@ function fetch_certbyname()
|
||||
luci.http.write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- Ping / 探测
|
||||
-- ============================================================
|
||||
function act_ping()
|
||||
local e = {}
|
||||
local domain = luci.http.formvalue("domain")
|
||||
@@ -1593,7 +1921,7 @@ function act_ping()
|
||||
end
|
||||
end
|
||||
else
|
||||
-- 3. 非 WebSocket 节点的探测逻辑 (TCP / ICMP / UDP)
|
||||
-- 非 WebSocket 节点的探测逻辑 (TCP / ICMP / UDP)
|
||||
local socket = nixio.socket("inet", "stream")
|
||||
if socket then
|
||||
socket:setopt("socket", "rcvtimeo", 3)
|
||||
@@ -1643,7 +1971,7 @@ function act_ping()
|
||||
end
|
||||
end
|
||||
|
||||
-- 4. 清理防火墙规则
|
||||
-- 清理防火墙规则
|
||||
if iret then
|
||||
if use_nft then
|
||||
luci.sys.call("nft delete element inet ss_spec ss_spec_wan_ac { " .. domain .. " } 2>/dev/null")
|
||||
@@ -1668,77 +1996,9 @@ function act_ping()
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
|
||||
function check_status()
|
||||
local e = {}
|
||||
local target = luci.http.formvalue("set") or ""
|
||||
e.ret = luci.sys.call("curl -m 3 -sS -o /dev/null http://www." .. target .. ".com >/dev/null 2>&1")
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(e)
|
||||
end
|
||||
|
||||
function refresh_data()
|
||||
local set = luci.http.formvalue("set")
|
||||
local retstring = loadstring("return " .. luci.sys.exec("/usr/bin/lua /usr/share/shadowsocksr/update.lua " .. set))()
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(retstring)
|
||||
end
|
||||
|
||||
function check_port()
|
||||
local retstring = "<br /><br />"
|
||||
local s
|
||||
local server_name = ""
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local use_nft = use_fw4_backend()
|
||||
|
||||
uci:foreach("shadowsocksr", "servers", function(s)
|
||||
if s.type == "clash" then
|
||||
retstring = retstring .. string.format("<font><b style='color:gray'>[%s] Clash panel node.</b></font><br />", s.alias or s[".name"])
|
||||
return
|
||||
end
|
||||
if s.alias then
|
||||
server_name = s.alias
|
||||
elseif s.server and s.server_port then
|
||||
server_name = s.server .. ":" .. s.server_port
|
||||
end
|
||||
|
||||
-- 临时加入 set
|
||||
local is_ipv6 = is_ipv6_address(s.server)
|
||||
local iret = false
|
||||
if not is_ipv6 then
|
||||
if use_nft then
|
||||
iret = luci.sys.call("nft add element inet ss_spec ss_spec_wan_ac { " .. s.server .. " } 2>/dev/null") == 0
|
||||
else
|
||||
iret = luci.sys.call("ipset add ss_spec_wan_ac " .. s.server .. " 2>/dev/null") == 0
|
||||
end
|
||||
end
|
||||
|
||||
-- TCP 测试
|
||||
local socket = nixio.socket(is_ipv6 and "inet6" or "inet", "stream")
|
||||
socket:setopt("socket", "rcvtimeo", 3)
|
||||
socket:setopt("socket", "sndtimeo", 3)
|
||||
local ret = socket:connect(s.server, s.server_port)
|
||||
socket:close()
|
||||
|
||||
if ret then
|
||||
retstring = retstring .. string.format("<font><b style='color:green'>[%s] OK.</b></font><br />", server_name)
|
||||
else
|
||||
retstring = retstring .. string.format("<font><b style='color:red'>[%s] Error.</b></font><br />", server_name)
|
||||
end
|
||||
|
||||
-- 删除临时 set
|
||||
if iret then
|
||||
if use_nft then
|
||||
luci.sys.call("nft delete element inet ss_spec ss_spec_wan_ac { " .. s.server .. " } 2>/dev/null")
|
||||
else
|
||||
luci.sys.call("ipset del ss_spec_wan_ac " .. s.server)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json({ret = retstring})
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 重启 / 重置 / 删除
|
||||
-- ============================================================
|
||||
function act_reset()
|
||||
luci.sys.call("/etc/init.d/shadowsocksr reset >/dev/null 2>&1")
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr"))
|
||||
@@ -1754,14 +2014,9 @@ function act_delete()
|
||||
luci.http.redirect(luci.dispatcher.build_url("admin", "services", "shadowsocksr", "servers"))
|
||||
end
|
||||
|
||||
function get_log()
|
||||
luci.http.write(luci.sys.exec("[ -f '/var/log/ssrplus.log' ] && cat /var/log/ssrplus.log"))
|
||||
end
|
||||
|
||||
function clear_log()
|
||||
luci.sys.call("echo '' > /var/log/ssrplus.log")
|
||||
end
|
||||
|
||||
-- ============================================================
|
||||
-- 备份
|
||||
-- ============================================================
|
||||
function create_backup()
|
||||
local backup_files = {
|
||||
"/etc/config/shadowsocksr",
|
||||
|
||||
@@ -2,6 +2,40 @@
|
||||
<%
|
||||
local clash_nodes = self.clash_nodes or {}
|
||||
local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
|
||||
local panel_defs = {
|
||||
{ name = "zashboard", label = "Zashboard", dir = "/usr/share/shadowsocksr/ui/zashboard" },
|
||||
{ name = "metacubexd", label = "Metacubexd", dir = "/usr/share/shadowsocksr/ui/metacubexd" },
|
||||
{ name = "yacd", label = "Yacd", dir = "/usr/share/shadowsocksr/ui/yacd" },
|
||||
{ name = "dashboard", label = "Dashboard", dir = "/usr/share/shadowsocksr/ui/dashboard" },
|
||||
}
|
||||
|
||||
local installed_panels = {}
|
||||
for _, def in ipairs(panel_defs) do
|
||||
if nixio.fs.stat(def.dir, "type") == "dir" then
|
||||
installed_panels[#installed_panels + 1] = {
|
||||
name = def.name,
|
||||
label = def.label,
|
||||
path = "/ui/" .. def.name .. "/"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local default_dashboard = luci.sys.exec(
|
||||
'uci -q get shadowsocksr.@global[0].default_dashboard 2>/dev/null'):gsub("%s+$", "")
|
||||
|
||||
local default_panel = ""
|
||||
if default_dashboard ~= "" then
|
||||
for _, p in ipairs(installed_panels) do
|
||||
if p.name == default_dashboard then
|
||||
default_panel = p.name
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
if default_panel == "" and #installed_panels > 0 then
|
||||
default_panel = installed_panels[1].name
|
||||
end
|
||||
-%>
|
||||
<div id="ssr-clash-main-panel-wrap">
|
||||
<style type="text/css">
|
||||
@@ -228,7 +262,91 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
|
||||
/* 手机端优化 */
|
||||
#ssr-panel-chooser-mask {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: var(--ssr-clash-mask-bg);
|
||||
z-index: 2100;
|
||||
}
|
||||
|
||||
#ssr-panel-chooser {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: min(480px, calc(100vw - 32px));
|
||||
max-height: calc(100vh - 48px);
|
||||
overflow: auto;
|
||||
background: var(--ssr-clash-modal-bg);
|
||||
color: var(--ssr-clash-modal-fg);
|
||||
border-radius: 10px;
|
||||
box-shadow: var(--ssr-clash-modal-shadow);
|
||||
border: 1px solid var(--ssr-clash-modal-border);
|
||||
z-index: 2101;
|
||||
padding: 14px 16px 16px;
|
||||
}
|
||||
|
||||
#ssr-panel-chooser-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
#ssr-panel-chooser-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--ssr-clash-modal-fg);
|
||||
}
|
||||
|
||||
#ssr-panel-chooser-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.ssr-panel-chooser-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid var(--ssr-clash-field-border);
|
||||
border-radius: 8px;
|
||||
background: var(--ssr-clash-field-bg);
|
||||
color: var(--ssr-clash-field-fg);
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, border-color 0.15s;
|
||||
}
|
||||
|
||||
.ssr-panel-chooser-item:hover {
|
||||
background: rgba(31, 111, 235, 0.10);
|
||||
border-color: #1f6feb;
|
||||
}
|
||||
|
||||
.ssr-panel-chooser-item .ssr-panel-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.ssr-panel-chooser-item .ssr-panel-url {
|
||||
font-size: 12px;
|
||||
color: var(--ssr-clash-modal-muted);
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.ssr-panel-chooser-item .ssr-panel-tag {
|
||||
font-size: 11px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #1f6feb;
|
||||
color: #fff;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#ssr-clash-main-buttons {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -248,6 +366,14 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#ssr-panel-chooser {
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
width: calc(100vw - 16px);
|
||||
max-height: calc(100vh - 24px);
|
||||
transform: none;
|
||||
}
|
||||
|
||||
#ssr-clash-modal-header {
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-start;
|
||||
@@ -262,7 +388,7 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
justify-content: flex-start;
|
||||
margin-left: 0;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
#ssr-clash-modal-header #ssr-clash-rules-add {
|
||||
@@ -395,6 +521,103 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
<% end -%>
|
||||
};
|
||||
|
||||
var MIHOMO_PANEL_PORT = 16756;
|
||||
var MIHOMO_PANEL_DEFAULT = '<%=default_panel%>';
|
||||
|
||||
var installedPanels = [
|
||||
<%
|
||||
for i, p in ipairs(installed_panels) do
|
||||
-%>
|
||||
{ name: '<%=p.name%>', label: '<%=p.label%>', path: '<%=p.path%>' }<%= (i < #installed_panels) and "," or "" %>
|
||||
<% end -%>
|
||||
];
|
||||
|
||||
function buildMihomoPanelUrl(path) {
|
||||
var host = window.location.hostname;
|
||||
if (host.indexOf(':') >= 0) {
|
||||
host = '[' + host + ']';
|
||||
}
|
||||
return 'http://' + host + ':' + MIHOMO_PANEL_PORT + path;
|
||||
}
|
||||
|
||||
function openPanelUrl(path) {
|
||||
if (!path) {
|
||||
return false;
|
||||
}
|
||||
window.open(buildMihomoPanelUrl(path), '_blank');
|
||||
return false;
|
||||
}
|
||||
|
||||
function showPanelChooser() {
|
||||
var mask = document.getElementById('ssr-panel-chooser-mask');
|
||||
var modal = document.getElementById('ssr-panel-chooser');
|
||||
var listEl = document.getElementById('ssr-panel-chooser-list');
|
||||
if (!mask || !modal || !listEl) {
|
||||
return false;
|
||||
}
|
||||
|
||||
listEl.innerHTML = '';
|
||||
|
||||
if (!installedPanels.length) {
|
||||
var empty = document.createElement('div');
|
||||
empty.style.color = 'var(--ssr-clash-modal-muted)';
|
||||
empty.style.padding = '12px 0';
|
||||
empty.textContent = '<%:No dashboard panel installed. Please install one first.%>';
|
||||
listEl.appendChild(empty);
|
||||
} else {
|
||||
installedPanels.forEach(function(panel) {
|
||||
var item = document.createElement('div');
|
||||
item.className = 'ssr-panel-chooser-item';
|
||||
|
||||
var info = document.createElement('div');
|
||||
info.style.flex = '1 1 auto';
|
||||
info.style.minWidth = '0';
|
||||
|
||||
var nameEl = document.createElement('div');
|
||||
nameEl.className = 'ssr-panel-name';
|
||||
nameEl.textContent = panel.label;
|
||||
|
||||
var urlEl = document.createElement('div');
|
||||
urlEl.className = 'ssr-panel-url';
|
||||
urlEl.textContent = buildMihomoPanelUrl(panel.path);
|
||||
|
||||
info.appendChild(nameEl);
|
||||
info.appendChild(urlEl);
|
||||
item.appendChild(info);
|
||||
|
||||
if (panel.name === MIHOMO_PANEL_DEFAULT) {
|
||||
var tag = document.createElement('span');
|
||||
tag.className = 'ssr-panel-tag';
|
||||
tag.textContent = '<%:Default%>';
|
||||
item.appendChild(tag);
|
||||
}
|
||||
|
||||
item.onclick = function() {
|
||||
closePanelChooser();
|
||||
openPanelUrl(panel.path);
|
||||
};
|
||||
|
||||
listEl.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
mask.style.display = 'block';
|
||||
modal.style.display = 'block';
|
||||
return false;
|
||||
}
|
||||
|
||||
function closePanelChooser() {
|
||||
var mask = document.getElementById('ssr-panel-chooser-mask');
|
||||
var modal = document.getElementById('ssr-panel-chooser');
|
||||
if (mask) mask.style.display = 'none';
|
||||
if (modal) modal.style.display = 'none';
|
||||
return false;
|
||||
}
|
||||
|
||||
window.ssrClashOpenPanelChooser = function() {
|
||||
return showPanelChooser();
|
||||
};
|
||||
|
||||
function setPanelStatus(text, color) {
|
||||
var statusEl = document.getElementById('ssr-clash-modal-status');
|
||||
if (!statusEl) {
|
||||
@@ -718,7 +941,7 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
|
||||
select.onchange = function() {
|
||||
setPanelStatus('<%:Switching...%>', '#0072c3');
|
||||
window.ssrXHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_switch")%>', {
|
||||
window.ssrXHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "clash_switch")%>', {
|
||||
sid: currentPanelSid,
|
||||
group: group.name,
|
||||
name: select.value
|
||||
@@ -919,7 +1142,7 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
var resetBtn = document.getElementById('ssr-clash-main-reset-defaults');
|
||||
var panelBtn = document.getElementById('ssr-clash-main-open');
|
||||
var btnContainer = document.getElementById('ssr-clash-main-buttons');
|
||||
if (!select || !row || !customRulesBtn || !resetBtn || !panelBtn) {
|
||||
if (!select || !row || !customRulesBtn || !resetBtn) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -928,13 +1151,13 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
row.style.display = '';
|
||||
customRulesBtn.setAttribute('data-sid', sid);
|
||||
resetBtn.setAttribute('data-sid', sid);
|
||||
panelBtn.setAttribute('data-sid', sid);
|
||||
if (panelBtn) panelBtn.setAttribute('data-sid', sid);
|
||||
if (btnContainer) btnContainer.style.display = 'flex';
|
||||
} else {
|
||||
row.style.display = 'none';
|
||||
customRulesBtn.setAttribute('data-sid', '');
|
||||
resetBtn.setAttribute('data-sid', '');
|
||||
panelBtn.setAttribute('data-sid', '');
|
||||
if (panelBtn) panelBtn.setAttribute('data-sid', '');
|
||||
if (btnContainer) btnContainer.style.display = 'none';
|
||||
}
|
||||
}
|
||||
@@ -980,9 +1203,7 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
|
||||
if (panelBtn) {
|
||||
panelBtn.onclick = function() {
|
||||
var sid = this.getAttribute('data-sid');
|
||||
if (!sid) return false;
|
||||
return openPanel(sid);
|
||||
return window.ssrClashOpenPanelChooser();
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1060,6 +1281,15 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
modalMask.onclick = closePanel;
|
||||
}
|
||||
|
||||
var chooserMask = document.getElementById('ssr-panel-chooser-mask');
|
||||
var chooserCloseBtn = document.getElementById('ssr-panel-chooser-close');
|
||||
if (chooserMask) {
|
||||
chooserMask.onclick = closePanelChooser;
|
||||
}
|
||||
if (chooserCloseBtn) {
|
||||
chooserCloseBtn.onclick = closePanelChooser;
|
||||
}
|
||||
|
||||
updateClashPanelMain();
|
||||
}, 0);
|
||||
})();
|
||||
@@ -1080,6 +1310,7 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
value="<%:Reset Default Proxies Rules%>"
|
||||
title="<%:Reset Default Proxies Rules%>"
|
||||
data-sid="<%=current_sid%>" />
|
||||
<% if #installed_panels > 0 then %>
|
||||
<input
|
||||
type="button"
|
||||
id="ssr-clash-main-open"
|
||||
@@ -1087,6 +1318,16 @@ local current_sid = self.map:get(section, "global_server") or "nil"
|
||||
value="<%:Mihomo Pannel%>"
|
||||
title="<%:Mihomo Pannel%>"
|
||||
data-sid="<%=current_sid%>" />
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div id="ssr-panel-chooser-mask"></div>
|
||||
<div id="ssr-panel-chooser" role="dialog" aria-modal="true" aria-labelledby="ssr-panel-chooser-title">
|
||||
<div id="ssr-panel-chooser-header">
|
||||
<div id="ssr-panel-chooser-title"><%:Select Dashboard Panel%></div>
|
||||
<input type="button" id="ssr-panel-chooser-close" class="btn cbi-button" value="<%:Close%>" />
|
||||
</div>
|
||||
<div id="ssr-panel-chooser-list"></div>
|
||||
</div>
|
||||
|
||||
<div id="ssr-clash-modal-mask"></div>
|
||||
|
||||
@@ -345,18 +345,110 @@
|
||||
queryLocalGeoInfo();
|
||||
}
|
||||
|
||||
function selectedGeoComponent() {
|
||||
var selector = document.getElementById('geo-selector');
|
||||
return selector ? selector.value : 'country_mmdb';
|
||||
function makeDashboardBtn(label, fn, args) {
|
||||
var argStr = args.map(function(a){ return "'" + a + "'"; }).join(', ');
|
||||
return '<input type="button" class="btn cbi-button cbi-button-apply" value="' + label + '" onclick="return ' + fn + '(this, ' + argStr + ')"/>';
|
||||
}
|
||||
|
||||
function geoHelp(component) {
|
||||
return geoMeta[component] ? geoMeta[component].help : '';
|
||||
window.switch_dashboard = window.switch_dashboard || function(btn, n, type) {
|
||||
btn.disabled = true;
|
||||
btn.value = '<%:Downloading File...%>';
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "switch_dashboard")%>', {name: n, type: type}, function(x, status) {
|
||||
if (!x || x.status != 200) return;
|
||||
if (status.download_state == "0") {
|
||||
var isUpdate = (n == "Metacubexd" || n == "Zashboard");
|
||||
var nextLabel, nextType;
|
||||
if (isUpdate) {
|
||||
nextLabel = '<%:Update Successful%> - ' + '<%:Update Version%>';
|
||||
nextType = 'Official';
|
||||
} else {
|
||||
nextLabel = '<%:Switch Successful%> - ' + (type == "Meta" ? '<%:Switch To Official Version%>' : '<%:Switch To Meta Version%>');
|
||||
nextType = type == "Meta" ? 'Official' : 'Meta';
|
||||
}
|
||||
document.getElementById('switch_dashboard_' + n).innerHTML = makeDashboardBtn(nextLabel, 'switch_dashboard', [n, nextType]);
|
||||
document.getElementById('default_dashboard_' + n).firstElementChild.disabled = false;
|
||||
document.getElementById('delete_dashboard_' + n).firstElementChild.disabled = false;
|
||||
} else {
|
||||
btn.value = status.download_state == "2" ? '<%:Unzip Error%>'
|
||||
: (n == "Metacubexd" || n == "Zashboard") ? '<%:Update Failed%>' : '<%:Switch Failed%>';
|
||||
}
|
||||
btn.disabled = false;
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
window.delete_dashboard = window.delete_dashboard || function(btn, n) {
|
||||
if (!confirm("<%:Are you sure you want to delete this panel?%>")) return false;
|
||||
btn.disabled = true;
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "delete_dashboard")%>', {name: n}, function(x, status) {
|
||||
if (!x || x.status != 200) { btn.disabled = false; return; }
|
||||
if (status.delete_state == "1") {
|
||||
var defBtn = document.getElementById('default_dashboard_' + n).firstElementChild;
|
||||
if (defBtn.disabled) defBtn.value = '<%:Set to Default%>';
|
||||
defBtn.disabled = true;
|
||||
} else btn.disabled = false;
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
window.default_dashboard = window.default_dashboard || function(btn, n) {
|
||||
btn.disabled = true;
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "default_dashboard")%>', {name: n}, function(x, status) {
|
||||
if (!x || x.status != 200) { btn.disabled = false; return; }
|
||||
btn.value = '<%:Default%>';
|
||||
document.querySelectorAll('[id^="default_dashboard_"]').forEach(function(el) {
|
||||
var b = el.firstElementChild;
|
||||
if (b && b !== btn && b.value === '<%:Default%>') {
|
||||
b.disabled = false;
|
||||
b.value = '<%:Set to Default%>';
|
||||
}
|
||||
});
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
function initDashboardPanels() {
|
||||
['Dashboard', 'Yacd', 'Metacubexd', 'Zashboard'].forEach(function(name) {
|
||||
var switchEl = document.getElementById('switch_dashboard_' + name);
|
||||
var defaultEl = document.getElementById('default_dashboard_' + name);
|
||||
var deleteEl = document.getElementById('delete_dashboard_' + name);
|
||||
|
||||
if (!switchEl || !defaultEl || !deleteEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
XHR.get('<%=luci.dispatcher.build_url("admin", "services", "shadowsocksr", "dashboard_type")%>', null, function(x, status) {
|
||||
if (!x || x.status != 200) return;
|
||||
|
||||
var switchLabel;
|
||||
if (name == "Metacubexd") {
|
||||
switchLabel = ['<%:Update Version%>', 'Official'];
|
||||
} else if (name == "Zashboard") {
|
||||
switchLabel = ['<%:Update Version%>', 'Official'];
|
||||
} else {
|
||||
var isMeta = (name == "Dashboard" ? status.dashboard_type : status.yacd_type) == "Meta";
|
||||
switchLabel = isMeta
|
||||
? ['<%:Switch To Official Version%>', 'Official']
|
||||
: ['<%:Switch To Meta Version%>', 'Meta'];
|
||||
}
|
||||
switchEl.innerHTML = makeDashboardBtn(switchLabel[0], 'switch_dashboard', [name, switchLabel[1]]);
|
||||
|
||||
if (status.default_dashboard == name.toLowerCase()) {
|
||||
defaultEl.innerHTML = makeDashboardBtn('<%:Default%>', 'default_dashboard', [name]);
|
||||
defaultEl.firstElementChild.disabled = true;
|
||||
}
|
||||
if (!status[name.toLowerCase()]) {
|
||||
defaultEl.firstElementChild.disabled = true;
|
||||
deleteEl.firstElementChild.disabled = true;
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.setTimeout(function() {
|
||||
onComponentChange();
|
||||
loadGeoComponent()
|
||||
loadGeoComponent();
|
||||
initDashboardPanels();
|
||||
}, 0);
|
||||
//]]>
|
||||
</script>
|
||||
@@ -367,6 +459,7 @@
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 10px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.component-upgrade-card {
|
||||
@@ -383,8 +476,8 @@
|
||||
}
|
||||
|
||||
.component-selector {
|
||||
min-width: 180px;
|
||||
width: 100%;
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.component-layout {
|
||||
@@ -427,6 +520,59 @@
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.dashboard-update-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 320px 1fr;
|
||||
gap: 8px 24px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-label .component-select-label strong {
|
||||
font-size: 18px;
|
||||
font-weight: normal;
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-label-first {
|
||||
min-height: 52px;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-label-first .component-select-label strong {
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-actions {
|
||||
min-height: 42px;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-actions .ssr-clash-dashboard-actions {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-actions .ssr-clash-dashboard-actions .cbi-button {
|
||||
width: 140px;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
html[data-darkmode="true"] .dashboard-update-grid-label .component-select-label {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
html[data-darkmode="true"] .dashboard-update-grid-label .component-select-label strong {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.component-layout {
|
||||
flex-direction: column;
|
||||
@@ -436,6 +582,33 @@
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dashboard-update-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-label {
|
||||
min-height: auto;
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-label-first {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-actions {
|
||||
min-height: auto;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-actions .ssr-clash-dashboard-actions .cbi-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dashboard-update-grid-actions-empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -521,3 +694,83 @@
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="cbi-section">
|
||||
<legend><%:Dashboard Update%></legend>
|
||||
<div class="cbi-value">
|
||||
<div class="cbi-value-field component-upgrade-card">
|
||||
<div class="dashboard-update-grid">
|
||||
<div class="dashboard-update-grid-label dashboard-update-grid-label-first">
|
||||
<div class="component-select-label">
|
||||
<strong><%:Panel Version Management%></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-update-grid-actions dashboard-update-grid-actions-empty"></div>
|
||||
<div class="dashboard-update-grid-label">
|
||||
<div class="component-select-label">
|
||||
<strong><%:Yacd Panel%></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-update-grid-actions">
|
||||
<div class="ssr-clash-dashboard-actions">
|
||||
<div id="switch_dashboard_Yacd"><%:Collecting data...%></div>
|
||||
<div id="delete_dashboard_Yacd">
|
||||
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Delete%>" onclick="return delete_dashboard(this, 'Yacd')"/>
|
||||
</div>
|
||||
<div id="default_dashboard_Yacd">
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Set to Default%>" onclick="return default_dashboard(this, 'Yacd')"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-update-grid-label">
|
||||
<div class="component-select-label">
|
||||
<strong><%:Dashboard Panel%></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-update-grid-actions">
|
||||
<div class="ssr-clash-dashboard-actions">
|
||||
<div id="switch_dashboard_Dashboard"><%:Collecting data...%></div>
|
||||
<div id="delete_dashboard_Dashboard">
|
||||
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Delete%>" onclick="return delete_dashboard(this, 'Dashboard')"/>
|
||||
</div>
|
||||
<div id="default_dashboard_Dashboard">
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Set to Default%>" onclick="return default_dashboard(this, 'Dashboard')"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-update-grid-label">
|
||||
<div class="component-select-label">
|
||||
<strong><%:Zashboard Panel%></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-update-grid-actions">
|
||||
<div class="ssr-clash-dashboard-actions">
|
||||
<div id="switch_dashboard_Zashboard"><%:Collecting data...%></div>
|
||||
<div id="delete_dashboard_Zashboard">
|
||||
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Delete%>" onclick="return delete_dashboard(this, 'Zashboard')"/>
|
||||
</div>
|
||||
<div id="default_dashboard_Zashboard">
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Set to Default%>" onclick="return default_dashboard(this, 'Zashboard')"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-update-grid-label">
|
||||
<div class="component-select-label">
|
||||
<strong><%:Metacubexd Panel%></strong>
|
||||
</div>
|
||||
</div>
|
||||
<div class="dashboard-update-grid-actions">
|
||||
<div class="ssr-clash-dashboard-actions">
|
||||
<div id="switch_dashboard_Metacubexd"><%:Collecting data...%></div>
|
||||
<div id="delete_dashboard_Metacubexd">
|
||||
<input type="button" class="btn cbi-button cbi-button-reset" value="<%:Delete%>" onclick="return delete_dashboard(this, 'Metacubexd')"/>
|
||||
</div>
|
||||
<div id="default_dashboard_Metacubexd">
|
||||
<input type="button" class="btn cbi-button cbi-button-apply" value="<%:Set to Default%>" onclick="return default_dashboard(this, 'Metacubexd')"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
@@ -100,16 +100,16 @@ msgstr ""
|
||||
msgid "Accept LAN Access"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:649
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:692
|
||||
msgid "Access Control"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1118
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1359
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:302
|
||||
msgid "Actions"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1098
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1339
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/server_table.htm:349
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/server_table.htm:357
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:256
|
||||
@@ -130,11 +130,11 @@ msgstr ""
|
||||
msgid "Additional Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:452
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:625
|
||||
msgid "Address"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:650
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:693
|
||||
msgid "Advanced Settings"
|
||||
msgstr ""
|
||||
|
||||
@@ -164,11 +164,11 @@ msgstr ""
|
||||
msgid "All Ports"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1046
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1267
|
||||
msgid "All client proxy rules cleared and applied."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1046
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1267
|
||||
msgid "All client proxy rules cleared."
|
||||
msgstr ""
|
||||
|
||||
@@ -242,6 +242,10 @@ msgstr ""
|
||||
msgid "Are you sure you want to delete subscribe link: %s ?"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:381
|
||||
msgid "Are you sure you want to delete this panel?"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/backup_restore.htm:133
|
||||
msgid "Are you sure you want to restore the client to default settings?"
|
||||
msgstr ""
|
||||
@@ -374,8 +378,8 @@ msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:173
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:326
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:480
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:515
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:653
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:688
|
||||
msgid "Check Update"
|
||||
msgstr ""
|
||||
|
||||
@@ -419,13 +423,13 @@ msgstr ""
|
||||
msgid "Clang.CN.CIDR"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:380
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:419
|
||||
msgid "Clash"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:112
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:893
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1096
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1116
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1337
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:3
|
||||
msgid "Clash Panel"
|
||||
msgstr ""
|
||||
@@ -446,11 +450,11 @@ msgstr ""
|
||||
msgid "Clash/Mihomo"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1101
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1342
|
||||
msgid "Clear All Rules"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1035
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1256
|
||||
msgid "Clear all custom client proxy rules and restore empty state?"
|
||||
msgstr ""
|
||||
|
||||
@@ -469,23 +473,24 @@ msgstr ""
|
||||
msgid "Click to the page"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1115
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1356
|
||||
msgid "Client"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1001
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1222
|
||||
msgid "Client rules exported to CSV file."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1025
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1246
|
||||
msgid "Client rules imported from CSV file. Click Save to apply."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1110
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1351
|
||||
msgid "Client supports manual IP or IP/CIDR input."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1107
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1328
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1348
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe.htm:251
|
||||
msgid "Close"
|
||||
msgstr ""
|
||||
@@ -500,17 +505,21 @@ msgstr ""
|
||||
msgid "Cloudflare DNS (1.1.1.1)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:477
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:512
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:650
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:685
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:716
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:732
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:748
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:764
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/status.htm:21
|
||||
msgid "Collecting data..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:464
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:637
|
||||
msgid "Component"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:652
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:695
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/component.lua:5
|
||||
msgid "Component Update"
|
||||
msgstr ""
|
||||
@@ -553,12 +562,12 @@ msgstr ""
|
||||
msgid "Copy SSR to clipboard successfully."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:443
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:616
|
||||
msgid "Core Components"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:24
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:503
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:676
|
||||
msgid "Country MMDB"
|
||||
msgstr ""
|
||||
|
||||
@@ -586,8 +595,8 @@ msgstr ""
|
||||
msgid "Custom"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1073
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1074
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1303
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1304
|
||||
msgid "Custom Client Proxies Rules"
|
||||
msgstr ""
|
||||
|
||||
@@ -623,11 +632,11 @@ msgstr ""
|
||||
msgid "Custom YAML imported successfully: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:784
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1007
|
||||
msgid "Custom client proxy rules saved and applied."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:784
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1007
|
||||
msgid "Custom client proxy rules saved."
|
||||
msgstr ""
|
||||
|
||||
@@ -658,7 +667,15 @@ msgstr ""
|
||||
msgid "DTLS 1.2"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:499
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:727
|
||||
msgid "Dashboard Panel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:699
|
||||
msgid "Dashboard Update"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:672
|
||||
msgid "Database"
|
||||
msgstr ""
|
||||
|
||||
@@ -674,6 +691,10 @@ msgstr ""
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1566
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1578
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1592
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:591
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:398
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:401
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:437
|
||||
msgid "Default"
|
||||
msgstr ""
|
||||
|
||||
@@ -702,17 +723,17 @@ msgid ""
|
||||
"proxy protocol."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:373
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:499
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:41
|
||||
msgid "Defaults Cleared!"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:373
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:499
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:41
|
||||
msgid "Defaults Restored!"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1110
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1351
|
||||
msgid ""
|
||||
"Define per-client Mihomo proxy targets. Rules are injected as SRC-IP-CIDR "
|
||||
"entries before the original Clash rules."
|
||||
@@ -722,7 +743,11 @@ msgstr ""
|
||||
msgid "Delay (ms)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:665
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:888
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:718
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:734
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:750
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:766
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/server_table.htm:319
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:196
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:328
|
||||
@@ -800,7 +825,7 @@ msgstr ""
|
||||
msgid "Downlink Capacity(Default:Mbps)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:451
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:624
|
||||
msgid "Download Source"
|
||||
msgstr ""
|
||||
|
||||
@@ -808,6 +833,10 @@ msgstr ""
|
||||
msgid "Download failed"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:355
|
||||
msgid "Downloading File..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:971
|
||||
msgid "Dual-stack Listening Socket"
|
||||
msgstr ""
|
||||
@@ -840,7 +869,7 @@ msgstr ""
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:54
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server.lua:101
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/servers.lua:676
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1114
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1355
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:184
|
||||
msgid "Enable"
|
||||
msgstr ""
|
||||
@@ -1014,7 +1043,7 @@ msgstr ""
|
||||
msgid "Expecting: %s"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1102
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1343
|
||||
msgid "Export Client Rules"
|
||||
msgstr ""
|
||||
|
||||
@@ -1026,7 +1055,7 @@ msgstr ""
|
||||
msgid "FORCE BRUTAL"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1048
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1269
|
||||
msgid "Failed to clear all client proxy rules."
|
||||
msgstr ""
|
||||
|
||||
@@ -1055,7 +1084,7 @@ msgstr ""
|
||||
msgid "Failed to query geo database information."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:786
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1009
|
||||
msgid "Failed to save custom client proxy rules."
|
||||
msgstr ""
|
||||
|
||||
@@ -1166,16 +1195,16 @@ msgstr ""
|
||||
msgid "Gecko Packet Size (min)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:491
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:664
|
||||
msgid "Geo Database Update"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:500
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:673
|
||||
msgid "Geo Resource"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/component.lua:14
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:455
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:628
|
||||
msgid "GitHub Direct"
|
||||
msgstr ""
|
||||
|
||||
@@ -1310,7 +1339,7 @@ msgstr ""
|
||||
msgid "Httpupgrade Path"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:420
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:459
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:467
|
||||
msgid "Hysteria2"
|
||||
msgstr ""
|
||||
@@ -1369,7 +1398,7 @@ msgstr ""
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1103
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1344
|
||||
msgid "Import Client Rules"
|
||||
msgstr ""
|
||||
|
||||
@@ -1572,8 +1601,8 @@ msgstr ""
|
||||
msgid "Listen only on the given interface or, if unspecified, on all"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:794
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:847
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1017
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1070
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:84
|
||||
msgid "Loading..."
|
||||
msgstr ""
|
||||
@@ -1591,7 +1620,7 @@ msgstr ""
|
||||
msgid "Local addresses"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:666
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:697
|
||||
msgid "Log"
|
||||
msgstr ""
|
||||
|
||||
@@ -1646,18 +1675,22 @@ msgstr ""
|
||||
msgid "Maximum packet size the socks5 server can receive from external"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:376
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:379
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:382
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:388
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:404
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:759
|
||||
msgid "Metacubexd Panel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:415
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:418
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:421
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:427
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:443
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:13
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:470
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:643
|
||||
msgid "Mihomo"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1087
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1088
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1318
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1319
|
||||
msgid "Mihomo Pannel"
|
||||
msgstr ""
|
||||
|
||||
@@ -1735,10 +1768,10 @@ msgstr ""
|
||||
msgid "NOT RUNNING"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:414
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:453
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:398
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:17
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:471
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:644
|
||||
msgid "NaiveProxy"
|
||||
msgstr ""
|
||||
|
||||
@@ -1790,10 +1823,14 @@ msgstr ""
|
||||
msgid "No custom YAML file was selected."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:550
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:773
|
||||
msgid "No custom client proxy rules yet."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:565
|
||||
msgid "No dashboard panel installed. Please install one first."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/refresh.htm:21
|
||||
msgid "No new data!"
|
||||
msgstr ""
|
||||
@@ -1806,7 +1843,7 @@ msgstr ""
|
||||
msgid "No subscription items found"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:691
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:914
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:32
|
||||
msgid "No switchable proxy groups found."
|
||||
msgstr ""
|
||||
@@ -1929,7 +1966,7 @@ msgid "Only when Socks5 Auth Mode is password valid, Not mandatory."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:28
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:504
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:677
|
||||
msgid "OpenClash GeoSite Database"
|
||||
msgstr ""
|
||||
|
||||
@@ -1953,7 +1990,7 @@ msgstr ""
|
||||
msgid "Package Manager"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:465
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:638
|
||||
msgid "Package Name"
|
||||
msgstr ""
|
||||
|
||||
@@ -1970,6 +2007,10 @@ msgstr ""
|
||||
msgid "Panel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:705
|
||||
msgid "Panel Version Management"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:540
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:95
|
||||
msgid "Password"
|
||||
@@ -2016,7 +2057,7 @@ msgstr ""
|
||||
msgid "Plugin Opts"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1116
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1357
|
||||
msgid "Policy"
|
||||
msgstr ""
|
||||
|
||||
@@ -2121,22 +2162,22 @@ msgstr ""
|
||||
msgid "RST Backup"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:442
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:481
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/status.htm:7
|
||||
msgid "RUNNING"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:435
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:474
|
||||
msgid "RUNNING in %s (%s) Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:439
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:478
|
||||
msgid "RUNNING in %s Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:680
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:828
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:878
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:903
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1051
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1101
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:93
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:291
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe.htm:253
|
||||
@@ -2156,7 +2197,7 @@ msgid "Realm URL"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/servers.lua:860
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:348
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:474
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:17
|
||||
msgid "Reapply"
|
||||
msgstr ""
|
||||
@@ -2206,14 +2247,14 @@ msgstr ""
|
||||
msgid "Reload YAML"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:341
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:467
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:10
|
||||
msgid "Reloading YAML..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:135
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:136
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1117
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1358
|
||||
msgid "Remarks"
|
||||
msgstr ""
|
||||
|
||||
@@ -2234,14 +2275,14 @@ msgstr ""
|
||||
msgid "Reserved bytes(optional)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1080
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1081
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1310
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1311
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:62
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:63
|
||||
msgid "Reset Default Proxies Rules"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:376
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:502
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:44
|
||||
msgid "Reset Defaults Failed!"
|
||||
msgstr ""
|
||||
@@ -2251,7 +2292,7 @@ msgstr ""
|
||||
msgid "Reset complete"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:362
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:488
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:30
|
||||
msgid "Reset saved Mihomo proxy-group selections and restore YAML defaults?"
|
||||
msgstr ""
|
||||
@@ -2260,7 +2301,7 @@ msgstr ""
|
||||
msgid "Reset to defaults"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:366
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:492
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:34
|
||||
msgid "Resetting Defaults..."
|
||||
msgstr ""
|
||||
@@ -2300,7 +2341,7 @@ msgstr ""
|
||||
msgid "Running Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:416
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:455
|
||||
msgid "SOCKS5"
|
||||
msgstr ""
|
||||
|
||||
@@ -2308,20 +2349,20 @@ msgstr ""
|
||||
msgid "SS URL base64 sstr format not recognized."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:647
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:690
|
||||
msgid "SSR Client"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:5
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:468
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:641
|
||||
msgid "SSR Plus+ Main Program"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:651
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:694
|
||||
msgid "SSR Server"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1106
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1347
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:195
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:327
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:352
|
||||
@@ -2346,7 +2387,7 @@ msgstr ""
|
||||
msgid "Save failed:"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:763
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:986
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:81
|
||||
msgid "Saving..."
|
||||
msgstr ""
|
||||
@@ -2355,6 +2396,10 @@ msgstr ""
|
||||
msgid "Second"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1327
|
||||
msgid "Select Dashboard Panel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1744
|
||||
msgid "Self-signed Certificate"
|
||||
msgstr ""
|
||||
@@ -2394,7 +2439,7 @@ msgstr ""
|
||||
msgid "Server failsafe auto swith and custom update settings"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:648
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:691
|
||||
msgid "Servers Nodes"
|
||||
msgstr ""
|
||||
|
||||
@@ -2406,6 +2451,15 @@ msgstr ""
|
||||
msgid "Session Ticket"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:387
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:403
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:721
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:737
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:753
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:769
|
||||
msgid "Set to Default"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:407
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:615
|
||||
msgid "Shadow-TLS"
|
||||
@@ -2415,15 +2469,15 @@ msgstr ""
|
||||
msgid "Shadow-TLS ChainPoxy type"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:377
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:416
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:392
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:462
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:61
|
||||
msgid "ShadowSocks"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:389
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:391
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:428
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:430
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:395
|
||||
msgid "ShadowSocks-Rust"
|
||||
msgstr ""
|
||||
@@ -2432,7 +2486,7 @@ msgstr ""
|
||||
msgid "ShadowSocks-rust Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:644
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:685
|
||||
msgid "ShadowSocksR Plus+"
|
||||
msgstr ""
|
||||
|
||||
@@ -2440,7 +2494,7 @@ msgstr ""
|
||||
msgid "ShadowSocksR Plus+ Settings"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:418
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:457
|
||||
msgid "ShadowTLS"
|
||||
msgstr ""
|
||||
|
||||
@@ -2449,7 +2503,7 @@ msgstr ""
|
||||
msgid "Shadowsocks Password"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:385
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:424
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:389
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:64
|
||||
msgid "ShadowsocksR"
|
||||
@@ -2536,7 +2590,7 @@ msgstr ""
|
||||
msgid "Starting subscription update..."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:653
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:696
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:130
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:132
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:134
|
||||
@@ -2622,21 +2676,39 @@ msgid ""
|
||||
"a maximum length of 3 bytes."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:373
|
||||
msgid "Switch Failed"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:365
|
||||
msgid "Switch Successful"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:365
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:432
|
||||
msgid "Switch To Meta Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:365
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:431
|
||||
msgid "Switch To Official Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:104
|
||||
msgid "Switch check cycly(second)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:730
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:953
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:71
|
||||
msgid "Switch failed."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:727
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:950
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:68
|
||||
msgid "Switched successfully."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:720
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:943
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:61
|
||||
msgid "Switching..."
|
||||
msgstr ""
|
||||
@@ -2690,7 +2762,7 @@ msgstr ""
|
||||
msgid "TLS is used to verify the leaf certificate name."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:383
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:422
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:404
|
||||
msgid "TUIC"
|
||||
msgstr ""
|
||||
@@ -2759,8 +2831,8 @@ msgid ""
|
||||
"GitHub release page."
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:816
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:871
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1039
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1094
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:89
|
||||
msgid "This Clash total node is not active. Apply this node first."
|
||||
msgstr ""
|
||||
@@ -2814,7 +2886,7 @@ msgstr ""
|
||||
msgid "Transport Protocol"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:412
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:451
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:461
|
||||
msgid "Trojan"
|
||||
msgstr ""
|
||||
@@ -2905,6 +2977,10 @@ msgstr ""
|
||||
msgid "Unsupported package manager"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:372
|
||||
msgid "Unzip Error"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:197
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:332
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:333
|
||||
@@ -2915,6 +2991,10 @@ msgstr ""
|
||||
msgid "Update All Subscribe Servers"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:373
|
||||
msgid "Update Failed"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/servers.lua:511
|
||||
msgid "Update Interval(min)"
|
||||
msgstr ""
|
||||
@@ -2923,6 +3003,16 @@ msgstr ""
|
||||
msgid "Update Mode"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:362
|
||||
msgid "Update Successful"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:362
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:425
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:427
|
||||
msgid "Update Version"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/servers.lua:482
|
||||
msgid "Update cycle (Day/Week)"
|
||||
msgstr ""
|
||||
@@ -2951,8 +3041,8 @@ msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:173
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:326
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:481
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:516
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:654
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:689
|
||||
msgid "Upgrade"
|
||||
msgstr ""
|
||||
|
||||
@@ -3058,12 +3148,12 @@ msgid "Using incorrect encryption mothod may causes service fail to start"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:33
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:505
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:678
|
||||
msgid "V2Ray GeoIP Databases"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:37
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:506
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:679
|
||||
msgid "V2Ray GeoSite Databases"
|
||||
msgstr ""
|
||||
|
||||
@@ -3175,7 +3265,7 @@ msgstr ""
|
||||
msgid "XTLS"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:406
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:445
|
||||
msgid "Xray"
|
||||
msgstr ""
|
||||
|
||||
@@ -3188,24 +3278,32 @@ msgid "Xray Noise Packets"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:9
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:469
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:642
|
||||
msgid "Xray-core"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:351
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:477
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:20
|
||||
msgid "YAML Reload Failed!"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:348
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:474
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:17
|
||||
msgid "YAML Reloaded!"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:711
|
||||
msgid "Yacd Panel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:273
|
||||
msgid "Yes"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:743
|
||||
msgid "Zashboard Panel"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:157
|
||||
msgid "adblock_url"
|
||||
msgstr ""
|
||||
|
||||
@@ -104,16 +104,16 @@ msgstr "一个合法的文件路径。在运行之前,此文件必须不存在
|
||||
msgid "Accept LAN Access"
|
||||
msgstr "接受局域网访问"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:649
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:692
|
||||
msgid "Access Control"
|
||||
msgstr "访问控制"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1118
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1359
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:302
|
||||
msgid "Actions"
|
||||
msgstr "操作"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1098
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1339
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/server_table.htm:349
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/server_table.htm:357
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:256
|
||||
@@ -134,11 +134,11 @@ msgstr "添加中..."
|
||||
msgid "Additional Version"
|
||||
msgstr "附加版本"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:452
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:625
|
||||
msgid "Address"
|
||||
msgstr "地址"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:650
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:693
|
||||
msgid "Advanced Settings"
|
||||
msgstr "高级设置"
|
||||
|
||||
@@ -168,11 +168,11 @@ msgstr "全部"
|
||||
msgid "All Ports"
|
||||
msgstr "所有端口(默认)"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1046
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1267
|
||||
msgid "All client proxy rules cleared and applied."
|
||||
msgstr "所有客户端代理规则已清空并生效。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1046
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1267
|
||||
msgid "All client proxy rules cleared."
|
||||
msgstr "所有客户端代理规则已清空。"
|
||||
|
||||
@@ -246,6 +246,10 @@ msgstr "定时模式"
|
||||
msgid "Are you sure you want to delete subscribe link: %s ?"
|
||||
msgstr "确定要删除此订阅链接:%s 吗?"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:381
|
||||
msgid "Are you sure you want to delete this panel?"
|
||||
msgstr "确定要删除此面板吗?"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/backup_restore.htm:133
|
||||
msgid "Are you sure you want to restore the client to default settings?"
|
||||
msgstr "是否真的要恢复客户端默认配置?"
|
||||
@@ -378,8 +382,8 @@ msgstr "切换检查重试次数"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:173
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:326
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:480
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:515
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:653
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:688
|
||||
msgid "Check Update"
|
||||
msgstr "检查更新"
|
||||
|
||||
@@ -423,13 +427,13 @@ msgstr ""
|
||||
msgid "Clang.CN.CIDR"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:380
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:419
|
||||
msgid "Clash"
|
||||
msgstr "Clash"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client.lua:112
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:893
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1096
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1116
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1337
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:3
|
||||
msgid "Clash Panel"
|
||||
msgstr "Clash 面板"
|
||||
@@ -450,11 +454,11 @@ msgstr "Clash YAML 路径"
|
||||
msgid "Clash/Mihomo"
|
||||
msgstr "Clash/Mihomo"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1101
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1342
|
||||
msgid "Clear All Rules"
|
||||
msgstr "清空所有规则"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1035
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1256
|
||||
msgid "Clear all custom client proxy rules and restore empty state?"
|
||||
msgstr "清空所有自定义客户端代理规则,并恢复为空白状态吗?"
|
||||
|
||||
@@ -473,23 +477,24 @@ msgstr "点击此处前往管理"
|
||||
msgid "Click to the page"
|
||||
msgstr "点击前往"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1115
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1356
|
||||
msgid "Client"
|
||||
msgstr "客户端"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1001
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1222
|
||||
msgid "Client rules exported to CSV file."
|
||||
msgstr "客户端规则已导出为 CSV 文件。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1025
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1246
|
||||
msgid "Client rules imported from CSV file. Click Save to apply."
|
||||
msgstr "客户端规则已从 CSV 文件导入,请点击保存生效。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1110
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1351
|
||||
msgid "Client supports manual IP or IP/CIDR input."
|
||||
msgstr "客户端支持手动输入 IP 或 IP/CIDR。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1107
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1328
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1348
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe.htm:251
|
||||
msgid "Close"
|
||||
msgstr "关闭"
|
||||
@@ -504,17 +509,21 @@ msgstr ""
|
||||
msgid "Cloudflare DNS (1.1.1.1)"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:477
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:512
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:650
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:685
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:716
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:732
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:748
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:764
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/status.htm:21
|
||||
msgid "Collecting data..."
|
||||
msgstr "正在收集数据中..."
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:464
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:637
|
||||
msgid "Component"
|
||||
msgstr "组件"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:652
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:695
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/component.lua:5
|
||||
msgid "Component Update"
|
||||
msgstr "组件升级"
|
||||
@@ -557,12 +566,12 @@ msgstr "使用订阅转换模板 URL 将订阅转换为 Clash/Mihomo YAML。"
|
||||
msgid "Copy SSR to clipboard successfully."
|
||||
msgstr "成功复制 SSR 网址到剪贴板。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:443
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:616
|
||||
msgid "Core Components"
|
||||
msgstr "核心组件"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:24
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:503
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:676
|
||||
msgid "Country MMDB"
|
||||
msgstr "Country MMDB"
|
||||
|
||||
@@ -590,8 +599,8 @@ msgstr "当前版本"
|
||||
msgid "Custom"
|
||||
msgstr "自定义"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1073
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1074
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1303
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1304
|
||||
msgid "Custom Client Proxies Rules"
|
||||
msgstr "自定义客户端代理规则"
|
||||
|
||||
@@ -628,11 +637,11 @@ msgstr "自定义端口"
|
||||
msgid "Custom YAML imported successfully: %s"
|
||||
msgstr "自定义 YAML 导入成功:%s"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:784
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1007
|
||||
msgid "Custom client proxy rules saved and applied."
|
||||
msgstr "自定义客户端代理规则已保存并生效。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:784
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1007
|
||||
msgid "Custom client proxy rules saved."
|
||||
msgstr "自定义客户端代理规则已保存。"
|
||||
|
||||
@@ -663,7 +672,15 @@ msgstr ""
|
||||
msgid "DTLS 1.2"
|
||||
msgstr "DTLS 1.2 数据包"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:499
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:727
|
||||
msgid "Dashboard Panel"
|
||||
msgstr "Dashboard 面板"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:699
|
||||
msgid "Dashboard Update"
|
||||
msgstr "Dashboard 更新"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:672
|
||||
msgid "Database"
|
||||
msgstr "数据库"
|
||||
|
||||
@@ -679,6 +696,10 @@ msgstr "解密"
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1566
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1578
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1592
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:591
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:398
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:401
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:437
|
||||
msgid "Default"
|
||||
msgstr "默认"
|
||||
|
||||
@@ -709,17 +730,17 @@ msgstr ""
|
||||
"默认值:16。填负数时,如 -1,不使用 Mux 模块承载 UDP 流量。将使用代理协议原本"
|
||||
"的 UDP 传输方式。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:373
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:499
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:41
|
||||
msgid "Defaults Cleared!"
|
||||
msgstr "已清空默认状态!"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:373
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:499
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:41
|
||||
msgid "Defaults Restored!"
|
||||
msgstr "默认值已恢复!"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1110
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1351
|
||||
msgid ""
|
||||
"Define per-client Mihomo proxy targets. Rules are injected as SRC-IP-CIDR "
|
||||
"entries before the original Clash rules."
|
||||
@@ -731,7 +752,11 @@ msgstr ""
|
||||
msgid "Delay (ms)"
|
||||
msgstr "延迟(ms)"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:665
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:888
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:718
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:734
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:750
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:766
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/server_table.htm:319
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:196
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:328
|
||||
@@ -809,7 +834,7 @@ msgstr "国内 DNS 服务器"
|
||||
msgid "Downlink Capacity(Default:Mbps)"
|
||||
msgstr "下行链路容量(默认:Mbps)"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:451
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:624
|
||||
msgid "Download Source"
|
||||
msgstr "下载源"
|
||||
|
||||
@@ -817,6 +842,10 @@ msgstr "下载源"
|
||||
msgid "Download failed"
|
||||
msgstr "下载失败"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:355
|
||||
msgid "Downloading File..."
|
||||
msgstr "文件下载中..."
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:971
|
||||
msgid "Dual-stack Listening Socket"
|
||||
msgstr "双栈 Socket 监听"
|
||||
@@ -849,7 +878,7 @@ msgstr "编辑服务器配置"
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:54
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server.lua:101
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/servers.lua:676
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1114
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1355
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:184
|
||||
msgid "Enable"
|
||||
msgstr "启用"
|
||||
@@ -1023,7 +1052,7 @@ msgstr "示例:"
|
||||
msgid "Expecting: %s"
|
||||
msgstr "应为:%s"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1102
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1343
|
||||
msgid "Export Client Rules"
|
||||
msgstr "批量导出客户端规则"
|
||||
|
||||
@@ -1035,7 +1064,7 @@ msgstr "解压失败"
|
||||
msgid "FORCE BRUTAL"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1048
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1269
|
||||
msgid "Failed to clear all client proxy rules."
|
||||
msgstr "清空所有客户端代理规则失败。"
|
||||
|
||||
@@ -1064,7 +1093,7 @@ msgstr "获取组件信息失败。"
|
||||
msgid "Failed to query geo database information."
|
||||
msgstr "获取 Geo 数据库信息失败。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:786
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1009
|
||||
msgid "Failed to save custom client proxy rules."
|
||||
msgstr "保存自定义客户端代理规则失败。"
|
||||
|
||||
@@ -1175,16 +1204,16 @@ msgstr "Gecko 包大小(最大)"
|
||||
msgid "Gecko Packet Size (min)"
|
||||
msgstr "Gecko 包大小(最小)"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:491
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:664
|
||||
msgid "Geo Database Update"
|
||||
msgstr "Geo 数据库更新"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:500
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:673
|
||||
msgid "Geo Resource"
|
||||
msgstr "Geo 资源"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/component.lua:14
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:455
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:628
|
||||
msgid "GitHub Direct"
|
||||
msgstr "GitHub 直连"
|
||||
|
||||
@@ -1319,7 +1348,7 @@ msgstr "HTTPUpgrade 主机名"
|
||||
msgid "Httpupgrade Path"
|
||||
msgstr "HTTPUpgrade 路径"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:420
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:459
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:467
|
||||
msgid "Hysteria2"
|
||||
msgstr ""
|
||||
@@ -1380,7 +1409,7 @@ msgstr "如果你使用自签证书,请选择"
|
||||
msgid "Import"
|
||||
msgstr "导入配置信息"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1103
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1344
|
||||
msgid "Import Client Rules"
|
||||
msgstr "批量导入客户端规则"
|
||||
|
||||
@@ -1583,8 +1612,8 @@ msgstr "Linux 内核 TUN 虚拟网卡需要系统支持和 root 权限。"
|
||||
msgid "Listen only on the given interface or, if unspecified, on all"
|
||||
msgstr "仅监听指定的接口,未指定则监听全部。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:794
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:847
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1017
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1070
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:84
|
||||
msgid "Loading..."
|
||||
msgstr "加载中..."
|
||||
@@ -1602,7 +1631,7 @@ msgstr "本机服务端"
|
||||
msgid "Local addresses"
|
||||
msgstr "本地地址"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:666
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:697
|
||||
msgid "Log"
|
||||
msgstr "日志"
|
||||
|
||||
@@ -1659,18 +1688,22 @@ msgstr "最大分片数"
|
||||
msgid "Maximum packet size the socks5 server can receive from external"
|
||||
msgstr "socks5 服务器可以从外部接收的最大数据包大小(单位:字节)"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:376
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:379
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:382
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:388
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:404
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:759
|
||||
msgid "Metacubexd Panel"
|
||||
msgstr "Metacubexd 面板"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:415
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:418
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:421
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:427
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:443
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:13
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:470
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:643
|
||||
msgid "Mihomo"
|
||||
msgstr "Mihomo"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1087
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1088
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1318
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1319
|
||||
msgid "Mihomo Pannel"
|
||||
msgstr "Mihomo 面板"
|
||||
|
||||
@@ -1750,10 +1783,10 @@ msgstr ""
|
||||
msgid "NOT RUNNING"
|
||||
msgstr "未运行"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:414
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:453
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:398
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:17
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:471
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:644
|
||||
msgid "NaiveProxy"
|
||||
msgstr ""
|
||||
|
||||
@@ -1805,10 +1838,14 @@ msgstr "没有可用的核心(Shadowsocks, Mihomo 或 Xray)可以导入此
|
||||
msgid "No custom YAML file was selected."
|
||||
msgstr "没有选择自定义 YAML 文件。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:550
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:773
|
||||
msgid "No custom client proxy rules yet."
|
||||
msgstr "暂无自定义客户端代理规则。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:565
|
||||
msgid "No dashboard panel installed. Please install one first."
|
||||
msgstr "未安装 Dashboard 面板,请先安装至少一个面板。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/refresh.htm:21
|
||||
msgid "No new data!"
|
||||
msgstr "你已经是最新数据,无需更新!"
|
||||
@@ -1821,7 +1858,7 @@ msgstr "没有上传证书。"
|
||||
msgid "No subscription items found"
|
||||
msgstr "没有找到任何订阅项"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:691
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:914
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:32
|
||||
msgid "No switchable proxy groups found."
|
||||
msgstr "未找到可切换的代理组。"
|
||||
@@ -1945,7 +1982,7 @@ msgid "Only when Socks5 Auth Mode is password valid, Not mandatory."
|
||||
msgstr "仅当 Socks5 认证方式为 Password 时有效,非必填。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:28
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:504
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:677
|
||||
msgid "OpenClash GeoSite Database"
|
||||
msgstr "OpenClash GeoSite 数据库"
|
||||
|
||||
@@ -1969,7 +2006,7 @@ msgstr ""
|
||||
msgid "Package Manager"
|
||||
msgstr "包管理器"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:465
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:638
|
||||
msgid "Package Name"
|
||||
msgstr "组件包名称"
|
||||
|
||||
@@ -1986,6 +2023,10 @@ msgstr "数据包 | Rand 长度"
|
||||
msgid "Panel"
|
||||
msgstr "面板"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:705
|
||||
msgid "Panel Version Management"
|
||||
msgstr "面板版本管理"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:540
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:95
|
||||
msgid "Password"
|
||||
@@ -2032,7 +2073,7 @@ msgstr "请填写 Clash 订阅 URL 或本地 YAML 路径中的至少一项。"
|
||||
msgid "Plugin Opts"
|
||||
msgstr "插件参数"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1116
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1357
|
||||
msgid "Policy"
|
||||
msgstr "策略"
|
||||
|
||||
@@ -2137,22 +2178,22 @@ msgstr ""
|
||||
msgid "RST Backup"
|
||||
msgstr "恢复备份"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:442
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:481
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/status.htm:7
|
||||
msgid "RUNNING"
|
||||
msgstr "运行中"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:435
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:474
|
||||
msgid "RUNNING in %s (%s) Mode"
|
||||
msgstr "运行中,当前为 %s(%s)模式"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:439
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:478
|
||||
msgid "RUNNING in %s Mode"
|
||||
msgstr "运行中,当前为 %s 模式"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:680
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:828
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:878
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:903
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1051
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1101
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:93
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:291
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe.htm:253
|
||||
@@ -2172,7 +2213,7 @@ msgid "Realm URL"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/servers.lua:860
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:348
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:474
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:17
|
||||
msgid "Reapply"
|
||||
msgstr "重新应用"
|
||||
@@ -2222,14 +2263,14 @@ msgstr "发布包名"
|
||||
msgid "Reload YAML"
|
||||
msgstr "重载 YAML"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:341
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:467
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:10
|
||||
msgid "Reloading YAML..."
|
||||
msgstr "正在重载 YAML..."
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:135
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:136
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1117
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1358
|
||||
msgid "Remarks"
|
||||
msgstr "备注"
|
||||
|
||||
@@ -2250,14 +2291,14 @@ msgstr "获取 TLS 证书链指纹需要 OpenSSL 支持。"
|
||||
msgid "Reserved bytes(optional)"
|
||||
msgstr "保留字节(可选)"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1080
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1081
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1310
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1311
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:62
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:63
|
||||
msgid "Reset Default Proxies Rules"
|
||||
msgstr "重置代理分组规则"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:376
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:502
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:44
|
||||
msgid "Reset Defaults Failed!"
|
||||
msgstr "重置默认值失败!"
|
||||
@@ -2267,7 +2308,7 @@ msgstr "重置默认值失败!"
|
||||
msgid "Reset complete"
|
||||
msgstr "重置完成"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:362
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:488
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:30
|
||||
msgid "Reset saved Mihomo proxy-group selections and restore YAML defaults?"
|
||||
msgstr "要清空已保存的 Mihomo 分组选择,并恢复为 YAML 默认值吗?"
|
||||
@@ -2276,7 +2317,7 @@ msgstr "要清空已保存的 Mihomo 分组选择,并恢复为 YAML 默认值
|
||||
msgid "Reset to defaults"
|
||||
msgstr "恢复出厂设置"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:366
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:492
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:34
|
||||
msgid "Resetting Defaults..."
|
||||
msgstr "正在重置默认值..."
|
||||
@@ -2316,7 +2357,7 @@ msgstr "运行中"
|
||||
msgid "Running Mode"
|
||||
msgstr "运行模式"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:416
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:455
|
||||
msgid "SOCKS5"
|
||||
msgstr "SOCKS5"
|
||||
|
||||
@@ -2324,20 +2365,20 @@ msgstr "SOCKS5"
|
||||
msgid "SS URL base64 sstr format not recognized."
|
||||
msgstr "无法识别 SS URL 的 Base64 格式。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:647
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:690
|
||||
msgid "SSR Client"
|
||||
msgstr "客户端"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:5
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:468
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:641
|
||||
msgid "SSR Plus+ Main Program"
|
||||
msgstr "SSR Plus+ 主程序"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:651
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:694
|
||||
msgid "SSR Server"
|
||||
msgstr "服务端"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1106
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1347
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:195
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:327
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:352
|
||||
@@ -2363,7 +2404,7 @@ msgstr "保存当前订阅设置"
|
||||
msgid "Save failed:"
|
||||
msgstr "保存失败:"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:763
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:986
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:81
|
||||
msgid "Saving..."
|
||||
msgstr "保存中..."
|
||||
@@ -2372,6 +2413,10 @@ msgstr "保存中..."
|
||||
msgid "Second"
|
||||
msgstr "周期"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1327
|
||||
msgid "Select Dashboard Panel"
|
||||
msgstr "选择 Dashboard 面板"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:1744
|
||||
msgid "Self-signed Certificate"
|
||||
msgstr "自签证书"
|
||||
@@ -2411,7 +2456,7 @@ msgstr "服务端类型"
|
||||
msgid "Server failsafe auto swith and custom update settings"
|
||||
msgstr "服务器节点故障自动切换/广告屏蔽/中国大陆 IP 段数据库更新设置"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:648
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:691
|
||||
msgid "Servers Nodes"
|
||||
msgstr "服务器节点"
|
||||
|
||||
@@ -2423,6 +2468,15 @@ msgstr "服务器节点订阅与管理"
|
||||
msgid "Session Ticket"
|
||||
msgstr "会话凭据"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:387
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:403
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:721
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:737
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:753
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:769
|
||||
msgid "Set to Default"
|
||||
msgstr "设为默认"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:407
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:615
|
||||
msgid "Shadow-TLS"
|
||||
@@ -2432,15 +2486,15 @@ msgstr ""
|
||||
msgid "Shadow-TLS ChainPoxy type"
|
||||
msgstr "代理链类型"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:377
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:416
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:392
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:462
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:61
|
||||
msgid "ShadowSocks"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:389
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:391
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:428
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:430
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:395
|
||||
msgid "ShadowSocks-Rust"
|
||||
msgstr ""
|
||||
@@ -2449,7 +2503,7 @@ msgstr ""
|
||||
msgid "ShadowSocks-rust Version"
|
||||
msgstr "ShadowSocks-rust 版本"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:644
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:685
|
||||
msgid "ShadowSocksR Plus+"
|
||||
msgstr ""
|
||||
|
||||
@@ -2457,7 +2511,7 @@ msgstr ""
|
||||
msgid "ShadowSocksR Plus+ Settings"
|
||||
msgstr "ShadowSocksR Plus+ 设置"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:418
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:457
|
||||
msgid "ShadowTLS"
|
||||
msgstr "ShadowTLS"
|
||||
|
||||
@@ -2466,7 +2520,7 @@ msgstr "ShadowTLS"
|
||||
msgid "Shadowsocks Password"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:385
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:424
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:389
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/server-config.lua:64
|
||||
msgid "ShadowsocksR"
|
||||
@@ -2553,7 +2607,7 @@ msgstr "开始探测"
|
||||
msgid "Starting subscription update..."
|
||||
msgstr "正在开始更新订阅..."
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:653
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:696
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:130
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:132
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:134
|
||||
@@ -2641,21 +2695,39 @@ msgstr ""
|
||||
"支持以“,”分隔的十进制数字,或者经过 Base64 编码的字符串,其最大长度为 3 个字"
|
||||
"节。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:373
|
||||
msgid "Switch Failed"
|
||||
msgstr "切换失败"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:365
|
||||
msgid "Switch Successful"
|
||||
msgstr "切换成功"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:365
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:432
|
||||
msgid "Switch To Meta Version"
|
||||
msgstr "切换至 Meta 版本"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:365
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:431
|
||||
msgid "Switch To Official Version"
|
||||
msgstr "切换至官方版本"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:104
|
||||
msgid "Switch check cycly(second)"
|
||||
msgstr "自动切换检查周期(秒)"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:730
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:953
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:71
|
||||
msgid "Switch failed."
|
||||
msgstr "切换失败。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:727
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:950
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:68
|
||||
msgid "Switched successfully."
|
||||
msgstr "切换成功。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:720
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:943
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:61
|
||||
msgid "Switching..."
|
||||
msgstr "正在切换..."
|
||||
@@ -2709,7 +2781,7 @@ msgstr "TLS 主机名"
|
||||
msgid "TLS is used to verify the leaf certificate name."
|
||||
msgstr "TLS 用于验证 leaf 证书的 name。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:383
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:422
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:404
|
||||
msgid "TUIC"
|
||||
msgstr "TUIC"
|
||||
@@ -2788,8 +2860,8 @@ msgstr ""
|
||||
"在线升级只跟踪 Mihomo 最新稳定版,并根据当前架构从官方 GitHub 发布页下载匹配"
|
||||
"的 Linux 压缩包。"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:816
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:871
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1039
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:1094
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel.htm:89
|
||||
msgid "This Clash total node is not active. Apply this node first."
|
||||
msgstr "该 Clash 总节点当前未启用,请先应用该节点。"
|
||||
@@ -2843,7 +2915,7 @@ msgstr "传输协议"
|
||||
msgid "Transport Protocol"
|
||||
msgstr "传输协议"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:412
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:451
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/client-config.lua:461
|
||||
msgid "Trojan"
|
||||
msgstr "Trojan"
|
||||
@@ -2934,6 +3006,10 @@ msgstr "不支持的组件"
|
||||
msgid "Unsupported package manager"
|
||||
msgstr "不支持的包管理器"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:372
|
||||
msgid "Unzip Error"
|
||||
msgstr "解压失败"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:197
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:332
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/subscribe_actions_footer.htm:333
|
||||
@@ -2944,6 +3020,10 @@ msgstr "更新"
|
||||
msgid "Update All Subscribe Servers"
|
||||
msgstr "更新所有订阅节点"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:373
|
||||
msgid "Update Failed"
|
||||
msgstr "更新失败"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/servers.lua:511
|
||||
msgid "Update Interval(min)"
|
||||
msgstr "更新间隔(分钟)"
|
||||
@@ -2952,6 +3032,16 @@ msgstr "更新间隔(分钟)"
|
||||
msgid "Update Mode"
|
||||
msgstr "更新模式"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:362
|
||||
msgid "Update Successful"
|
||||
msgstr "更新成功"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:362
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:425
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:427
|
||||
msgid "Update Version"
|
||||
msgstr "更新版本"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/servers.lua:482
|
||||
msgid "Update cycle (Day/Week)"
|
||||
msgstr "更新周期(天/周)"
|
||||
@@ -2980,8 +3070,8 @@ msgstr "更新中..."
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:173
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:326
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:481
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:516
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:654
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:689
|
||||
msgid "Upgrade"
|
||||
msgstr "升级"
|
||||
|
||||
@@ -3089,12 +3179,12 @@ msgid "Using incorrect encryption mothod may causes service fail to start"
|
||||
msgstr "输入不正确的参数组合可能会导致服务无法启动"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:33
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:505
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:678
|
||||
msgid "V2Ray GeoIP Databases"
|
||||
msgstr "V2Ray GeoIP 数据库"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:37
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:506
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:679
|
||||
msgid "V2Ray GeoSite Databases"
|
||||
msgstr "V2Ray GeoSite 数据库"
|
||||
|
||||
@@ -3206,7 +3296,7 @@ msgstr "XHTTP 路径"
|
||||
msgid "XTLS"
|
||||
msgstr ""
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:406
|
||||
#: applications/luci-app-ssr-plus/luasrc/controller/shadowsocksr.lua:445
|
||||
msgid "Xray"
|
||||
msgstr "Xray"
|
||||
|
||||
@@ -3219,24 +3309,32 @@ msgid "Xray Noise Packets"
|
||||
msgstr "Xray 噪声数据包"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:9
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:469
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:642
|
||||
msgid "Xray-core"
|
||||
msgstr "Xray-core"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:351
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:477
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:20
|
||||
msgid "YAML Reload Failed!"
|
||||
msgstr "YAML 重载失败!"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:348
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_main_panel.htm:474
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/clash_panel_button.htm:17
|
||||
msgid "YAML Reloaded!"
|
||||
msgstr "YAML 已重载!"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:711
|
||||
msgid "Yacd Panel"
|
||||
msgstr "Yacd 面板"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:273
|
||||
msgid "Yes"
|
||||
msgstr "是"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/view/shadowsocksr/component.htm:743
|
||||
msgid "Zashboard Panel"
|
||||
msgstr "Zashboard 面板"
|
||||
|
||||
#: applications/luci-app-ssr-plus/luasrc/model/cbi/shadowsocksr/advanced.lua:157
|
||||
msgid "adblock_url"
|
||||
msgstr "广告屏蔽更新 URL"
|
||||
|
||||
@@ -611,7 +611,9 @@ prepare_clash_runtime_config() {
|
||||
local enable_fake_ip
|
||||
local socks5_auth
|
||||
local socks5_user
|
||||
local socks5_pass
|
||||
local default_dashboard
|
||||
local yacd_type
|
||||
local dashboard_type
|
||||
|
||||
cache_file="$(get_clash_cache_file "$sid")"
|
||||
workdir="$(get_clash_workdir "$sid")"
|
||||
@@ -635,11 +637,41 @@ prepare_clash_runtime_config() {
|
||||
cp -f "$cache_file" "$raw_file"
|
||||
filter_clash_yaml_proxies "$raw_file"
|
||||
|
||||
default_dashboard=$(uci_get_by_type global default_dashboard "zashboard")
|
||||
yacd_type=$(uci_get_by_type global yacd_type "Meta")
|
||||
dashboard_type=$(uci_get_by_type global dashboard_type "Official")
|
||||
case "$default_dashboard" in
|
||||
dashboard)
|
||||
if [ "$dashboard_type" = "Official" ]; then
|
||||
EXTERNAL_UI_URL="https://codeload.github.com/ayanamist/clash-dashboard/zip/refs/heads/gh-pages"
|
||||
else
|
||||
EXTERNAL_UI_URL="https://codeload.github.com/MetaCubeX/Razord-meta/zip/refs/heads/gh-pages"
|
||||
fi
|
||||
;;
|
||||
yacd)
|
||||
if [ "$yacd_type" = "Official" ]; then
|
||||
EXTERNAL_UI_URL="https://codeload.github.com/haishanh/yacd/zip/refs/heads/gh-pages"
|
||||
else
|
||||
EXTERNAL_UI_URL="https://codeload.github.com/MetaCubeX/Yacd-meta/zip/refs/heads/gh-pages"
|
||||
fi
|
||||
;;
|
||||
metacubexd)
|
||||
EXTERNAL_UI_URL="https://codeload.github.com/MetaCubeX/metacubexd/zip/refs/heads/gh-pages"
|
||||
;;
|
||||
zashboard|*)
|
||||
EXTERNAL_UI_URL="https://codeload.github.com/Zephyruso/zashboard/zip/refs/heads/gh-pages-cdn-fonts"
|
||||
default_dashboard="zashboard"
|
||||
;;
|
||||
esac
|
||||
|
||||
cat >"$overlay_file" <<-EOF
|
||||
redir-port: $local_port
|
||||
tproxy-port: $local_port
|
||||
external-controller: 127.0.0.1:$CLASH_API_PORT
|
||||
secret: $clash_secret
|
||||
external-controller: 0.0.0.0:$CLASH_API_PORT
|
||||
secret: ""
|
||||
external-ui: /usr/share/shadowsocksr/ui
|
||||
external-ui-name: $default_dashboard
|
||||
external-ui-url: $EXTERNAL_UI_URL
|
||||
allow-lan: true
|
||||
bind-address: 0.0.0.0
|
||||
routing-mark: $CLASH_OUTBOUND_MARK
|
||||
@@ -2476,7 +2508,7 @@ Start_Run() {
|
||||
return 1
|
||||
}
|
||||
clash_workdir="$(get_clash_workdir "$GLOBAL_SERVER")"
|
||||
ln_start_bin "$mihomo_bin" ssr-retcp -d "$clash_workdir" -f "$clash_workdir/config.yaml"
|
||||
SAFE_PATHS="/usr/share/shadowsocksr/" ln_start_bin "$mihomo_bin" ssr-retcp -d "$clash_workdir" -f "$clash_workdir/config.yaml"
|
||||
echolog "Main node: $($mihomo_bin -v 2>&1 | head -1 | awk '{print $1, $3}') (Clash) Started!"
|
||||
;;
|
||||
socks5)
|
||||
@@ -2502,11 +2534,13 @@ load_config() {
|
||||
else
|
||||
GLOBAL_SERVER=$switch_server
|
||||
fi
|
||||
# 全局 socks5 注释掉后配合 advanced.lua 中取消注释 -- Server Selection 选项
|
||||
if [ "$(uci_get_by_type socks5_proxy enabled 0)" = "1" ]; then
|
||||
uci -q set "$NAME.@socks5_proxy[0].server=same"
|
||||
else
|
||||
uci -q set "$NAME.@socks5_proxy[0].server=nil"
|
||||
fi
|
||||
# 全局 3proxy 注释掉后配合 advanced.lua 中取消注释 -- Server Selection 选项
|
||||
if [ "$(uci_get_by_type http_proxy enabled 0)" = "1" ]; then
|
||||
uci -q set "$NAME.@http_proxy[0].server=same"
|
||||
else
|
||||
|
||||
@@ -92,6 +92,18 @@ if [ -s "/etc/config/shadowsocksr" ]; then
|
||||
uci -q set shadowsocksr.@global[0].component_mirror='direct'
|
||||
fi
|
||||
|
||||
if ! uci -q get shadowsocksr.@global[0].yacd_type > /dev/null; then
|
||||
uci -q set shadowsocksr.@global[0].yacd_type='Meta'
|
||||
fi
|
||||
|
||||
if ! uci -q get shadowsocksr.@global[0].dashboard_type > /dev/null; then
|
||||
uci -q set shadowsocksr.@global[0].dashboard_type='Official'
|
||||
fi
|
||||
|
||||
if ! uci -q get shadowsocksr.@global[0].default_dashboard > /dev/null; then
|
||||
uci -q set shadowsocksr.@global[0].default_dashboard='zashboard'
|
||||
fi
|
||||
|
||||
if ! uci -q get shadowsocksr.@global[0].filter_aaaa > /dev/null; then
|
||||
legacy_filter_aaaa="$(uci -q get shadowsocksr.@global[0].mosdns_ipv6)"
|
||||
uci -q set shadowsocksr.@global[0].filter_aaaa="${legacy_filter_aaaa:-1}"
|
||||
|
||||
@@ -15,6 +15,9 @@ config global
|
||||
option switch_try_count '3'
|
||||
option default_node_local_port '1234'
|
||||
option component_mirror 'direct'
|
||||
option yacd_type 'Meta'
|
||||
option dashboard_type 'Official'
|
||||
option default_dashboard 'zashboard'
|
||||
option gfwlist_url 'https://fastly.jsdelivr.net/gh/YW5vbnltb3Vz/domain-list-community@release/gfwlist.txt'
|
||||
option chnroute_url 'https://ispip.clang.cn/all_cn.txt'
|
||||
option adblock_url 'https://anti-ad.net/anti-ad-for-dnsmasq.conf'
|
||||
|
||||
Whitespace-only changes.
@@ -0,0 +1 @@
|
||||
board.zash.run.place
|
||||
@@ -0,0 +1,30 @@
|
||||
# Third-party notices
|
||||
|
||||
## Earth textures
|
||||
|
||||
The Earth day, night, bump, roughness, and cloud textures in
|
||||
`src/assets/images/earth/` are resized/combined derivatives of the Solar System
|
||||
Scope planet textures. `background.jpg` preserves the downloaded 2K stars image.
|
||||
|
||||
- Source: https://www.solarsystemscope.com/textures/
|
||||
- 2K stars source: https://www.solarsystemscope.com/textures/download/2k_stars.jpg
|
||||
- Creator: Solar System Scope
|
||||
- License: Creative Commons Attribution 4.0 International (CC BY 4.0)
|
||||
https://creativecommons.org/licenses/by/4.0/
|
||||
|
||||
The node-material treatment is adapted from the Three.js WebGPU TSL Earth
|
||||
example (Three.js, MIT License):
|
||||
https://threejs.org/examples/webgpu_tsl_earth.html
|
||||
|
||||
## DB-IP City Lite
|
||||
|
||||
The application can download `dbip-city-lite@1.0.16` on explicit user request.
|
||||
The database is not bundled with this application. It is downloaded into and
|
||||
queried from the user's browser storage.
|
||||
|
||||
- Source: https://www.npmjs.com/package/dbip-city-lite/v/1.0.16
|
||||
- Data provider: DB-IP.com, https://db-ip.com/db/lite.php
|
||||
- License: Creative Commons Attribution 4.0 International (CC BY 4.0)
|
||||
https://creativecommons.org/licenses/by/4.0/
|
||||
|
||||
IP geolocation data provided by DB-IP.com.
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.4 KiB |
+1
@@ -0,0 +1 @@
|
||||
.earth-globe-card--expanded[data-v-dede773a]{z-index:60;border-radius:0;flex-direction:column;display:flex;position:fixed;inset:0}@media (width>=769px){.home-page.sidebar-expanded{left:16rem}.home-page.sidebar-collapsed{left:4.5rem}}
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./earth-renderer-Dh_x8rMR.js","./palette-DPyxzM37.js"])))=>i.map(i=>d[i]);
|
||||
import{$ as e,A as t,B as n,D as r,E as i,F as a,G as o,H as s,I as c,J as l,K as u,L as ee,M as te,O as d,P as f,Q as p,R as m,S as ne,T as re,U as ie,V as h,W as g,X as _,Y as ae,Z as oe,a as se,at as v,b as ce,c as y,ct as b,d as le,et as ue,f as de,g as x,h as S,i as fe,it as C,j as pe,k as me,l as w,lt as T,m as E,n as he,nt as D,o as O,ot as k,p as ge,q as _e,r as ve,rt as ye,s as be,st as xe,t as Se,u as Ce,w as we,x as Te,y as Ee,z as A}from"./index-DC9m9n9t.js";import{t as De}from"./palette-DPyxzM37.js";var Oe=T(Ce(),1),j=e=>{try{return Oe.parse(e).toNormalizedString()}catch{return null}},M=e=>{let t=e.trim();if(!t)return null;let n=j(t);if(n)return n;if(t.startsWith(`[`)){let e=t.indexOf(`]`);if(e>1)return j(t.slice(1,e))}let r=t.lastIndexOf(`:`);return r>0&&/^\d+$/.test(t.slice(r+1))?j(t.slice(0,r)):null},N=e=>{let t=w(),n=[];for(let r of e){let e=M(t.destination(r)),i=t.hostname(r).trim().replace(/\.$/,``);!e&&!i||n.push({destinationIP:e,resolutionHost:i.toLowerCase(),upload:Math.max(0,r.uploadSpeed),download:Math.max(0,r.downloadSpeed),host:i||e||``,downloaded:Math.max(0,t.download(r)),direct:t.isDirect(r)})}return n},P=({latitude:e,longitude:t})=>`${e.toFixed(4)},${t.toFixed(4)}`,F=(...e)=>e.flat().sort((e,t)=>t.downloaded-e.downloaded).slice(0,5),I=e=>e!==null&&e.latitude!==null&&e.longitude!==null&&Number.isFinite(e.latitude)&&Number.isFinite(e.longitude)&&e.latitude>=-90&&e.latitude<=90&&e.longitude>=-180&&e.longitude<=180,L=(e,t,n)=>{let r,i;if(n&&I(n))r=n.latitude,i=n.longitude;else if(t&&I(t))r=t.latitude,i=t.longitude;else return null;return{ip:e,latitude:r,longitude:i,city:n?.city.trim()||t?.city||``,country:t?.country||n?.country.trim()||``}},ke=async(e,t,n,r,i,a)=>{let o=j(t);if(!o)return{routes:[],origin:null};let s=N(e),c=[...new Set(s.filter(e=>!e.destinationIP).map(e=>e.resolutionHost))],l=a&&c.length>0?await a(c):{};for(let e of s)e.destinationIP||=j(l[e.resolutionHost]??``);let u=s.filter(e=>e.destinationIP!==null),ee=new Set([o]);for(let e of u)ee.add(e.destinationIP);let te=await r([...ee],n),d=L(o,te[o],i);if(!d)return{routes:[],origin:null};let f=new Map;for(let e of u){let t=te[e.destinationIP];if(!t)continue;let n=[{...d,role:`origin`},{...t,role:`destination`}],r=n.map(e=>`${e.role}:${P(e)}`).join(`>`),i=f.get(r);i?(i.direct&&=e.direct,i.connections+=1,i.upload+=e.upload,i.download+=e.download,i.topHosts=F(i.topHosts,e.host?[{host:e.host,downloaded:e.downloaded}]:[])):f.set(r,{key:r,path:n,direct:e.direct,connections:1,upload:e.upload,download:e.download,topHosts:e.host?[{host:e.host,downloaded:e.downloaded}]:[]})}return{routes:[...f.values()],origin:d}},Ae=617e5;u(),e(),v(),f();var je={class:`flex items-center justify-between`},Me={class:`text-base-content/60 text-xs font-semibold tracking-wider uppercase`},Ne={class:`flex items-center gap-1`},Pe=[`aria-label`,`title`,`aria-pressed`],Fe=[`aria-label`,`title`,`aria-pressed`],Ie=[`aria-label`,`title`,`aria-pressed`],Le={class:`pointer-events-none absolute top-2 right-2 left-2 flex flex-wrap gap-1.5 text-xs`},Re={class:`bg-base-100/75 pointer-events-auto flex h-8 items-center gap-1.5 rounded-lg px-2 shadow backdrop-blur-md`},ze={class:`text-base-content/60`},Be={class:`font-mono`},Ve=[`aria-label`],He=[`aria-label`],Ue={class:`pointer-events-none absolute right-2 bottom-2 left-2 flex items-end justify-between gap-2`},We=[`aria-label`],Ge={class:`text-base-content/70 whitespace-nowrap`},Ke={key:0,class:`pointer-events-none absolute inset-0 flex items-center justify-center p-4`},qe={class:`bg-base-100/90 max-w-sm rounded-xl p-4 text-center text-sm shadow-xl backdrop-blur-md`},Je={class:`text-error`},Ye={key:1,class:`pointer-events-none absolute inset-0 flex items-center justify-center`},Xe={class:`bg-base-100/85 flex items-center gap-2 rounded-xl px-4 py-3 text-sm shadow-xl backdrop-blur-md`},Ze={key:2,class:`pointer-events-none absolute inset-0 flex items-center justify-center p-4`},Qe={class:`bg-base-100/90 pointer-events-auto max-w-md rounded-xl p-4 text-sm shadow-xl backdrop-blur-md`},$e={class:`font-semibold`},et={class:`text-base-content/70 mt-1.5 leading-5`},tt={key:0,class:`text-warning mt-1.5 text-xs`},nt={class:`mt-3 flex items-center gap-2`},rt={class:`link text-xs`,href:`https://db-ip.com/db/lite.php`,target:`_blank`,rel:`noopener noreferrer`},it={key:3,class:`pointer-events-none absolute inset-0 flex items-center justify-center p-4`},at={class:`bg-base-100/90 pointer-events-auto w-80 max-w-full rounded-xl p-4 text-sm shadow-xl backdrop-blur-md`},ot={class:`flex items-center justify-between gap-3`},st={class:`font-semibold`},ct={class:`text-base-content/60 text-xs`},lt=[`value`],ut={key:4,class:`pointer-events-none absolute inset-0 flex items-center justify-center p-4`},dt={class:`bg-base-100/90 pointer-events-auto max-w-sm rounded-xl p-4 text-center text-sm shadow-xl backdrop-blur-md`},ft={class:`text-error`},pt={key:5,class:`pointer-events-none absolute inset-0 flex items-center justify-center p-4`},mt={class:Line truncated
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
|
After Width: | Height: | Size: 218 KiB |
Loaded 100 of 135 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user