mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-14 20:34:19 +08:00
Compare commits
3
Commits
70d35bab31
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
26d174538f | ||
|
|
d9f7c5c117 | ||
|
|
7614ee5c63 |
@@ -392,8 +392,8 @@ jobs:
|
||||
(
|
||||
git_sparse_clone develop "https://github.com/Ysurac/openmptcprouter-feeds" \
|
||||
dsvpn glorytun-udp glorytun grpcurl ipcalc luci-app-dsvpn luci-app-glorytun-tcp luci-app-glorytun-udp luci-app-mail luci-app-mlvpn luci-app-mptcp luci-app-nginx-ha luci-app-sqm-autorate luci-app-packet-capture luci-app-iperf luci-theme-openmptcprouter sqm-autorate speedtestc mlvpn mptcp tcptraceroute tracebox tsping atinout z8102
|
||||
git_sparse_clone chawrt/25.12 "https://github.com/liudf0716/luci" applications/luci-app-aw-bpf applications/luci-app-xkcptun applications/luci-app-apfree-wifidog
|
||||
git_sparse_clone chawrt/25.12 "https://github.com/liudf0716/packages" net/aw-bpf net/xkcptun net/apfree-wifidog
|
||||
git_sparse_clone chawrt/25.12 "https://github.com/liudf0716/luci" applications/luci-app-xkcptun applications/luci-app-apfree-wifidog
|
||||
git_sparse_clone chawrt/25.12 "https://github.com/liudf0716/packages" net/xkcptun net/apfree-wifidog
|
||||
) &
|
||||
git_sparse_clone master "https://github.com/immortalwrt/packages" net/n2n net/qbittorrent \
|
||||
net/amule net/cdnspeedtest net/minieap net/ddns-go net/sysuh3c net/3proxy utils/filebrowser net/cloudreve \
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
#
|
||||
# Copyright (C) 2025 Dengfeng Liu
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=aw-bpf
|
||||
PKG_VERSION:=1.08.23
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=aw-bpf-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/$(PKG_NAME)/tar.gz/$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
||||
PKG_LICENSE:=GPL-2.0-or-later
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
|
||||
PKG_BUILD_DEPENDS:=bpf-headers
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/cmake.mk
|
||||
include $(INCLUDE_DIR)/bpf.mk
|
||||
include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
define Package/aw-bpf
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
TITLE:=ApFree eBPF programs and tools
|
||||
DEPENDS:=+libbpf +libelf +libjson-c +libmnl +libnftnl +tc-full +bpftool-minimal +ip +kmod-xdpi +kmod-sched-bpf $(BPF_DEPENDS)
|
||||
URL:=https://github.com/liudf0716/aw-bpf
|
||||
endef
|
||||
|
||||
define Package/aw-bpf/description
|
||||
ApFree eBPF provides eBPF programs for session tracking, xDPI, DNS monitoring
|
||||
and user-space tools: aw-bpfctl (control CLI) and aw-eventd (session / DNS
|
||||
consumer with independent nftables table inet awbpf).
|
||||
endef
|
||||
|
||||
|
||||
define KernelPackage/xdpi
|
||||
SECTION:=kernel
|
||||
SUBMENU:=Network Support
|
||||
TITLE:=xdpi ebpf module
|
||||
FILES:=$(PKG_BUILD_DIR)/xdpi_bpf.ko
|
||||
AUTOLOAD:=$(call AutoProbe,xdpi_bpf)
|
||||
endef
|
||||
|
||||
# Build cmake userspace first (aw-bpfctl, aw-eventd), then BPF objects and kmod.
|
||||
define Build/Compile
|
||||
$(call Build/Compile/Default)
|
||||
$(call CompileBPF,$(PKG_BUILD_DIR)/aw_bpf.c,$(if $(filter arm aarch64 x86_64 i386,$(ARCH)),-DENABLE_XDPI_FEATURE))
|
||||
$(call CompileBPF,$(PKG_BUILD_DIR)/dns_bpf.c,$(if $(filter arm aarch64 x86_64 i386,$(ARCH)),-DENABLE_XDPI_FEATURE))
|
||||
$(KERNEL_MAKE) M=$(PKG_BUILD_DIR) modules
|
||||
endef
|
||||
|
||||
define Package/aw-bpf/conffiles
|
||||
/etc/config/aw-bpf
|
||||
endef
|
||||
|
||||
define Package/aw-bpf/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/aw-bpfctl $(1)/usr/bin/aw-bpfctl
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/aw-eventd $(1)/usr/bin/aw-eventd
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/aw-bpf.init $(1)/etc/init.d/aw-bpf
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/aw-bpf.conf $(1)/etc/config/aw-bpf
|
||||
$(INSTALL_DIR) $(1)/lib/bpf
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/aw_bpf.o $(1)/lib/bpf/aw-bpf.o
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/dns_bpf.o $(1)/lib/bpf/dns-bpf.o
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,aw-bpf))
|
||||
$(eval $(call KernelPackage,xdpi))
|
||||
@@ -1,2 +0,0 @@
|
||||
config aw-bpf 'common'
|
||||
option enable_event_log '0'
|
||||
@@ -1,325 +0,0 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2025-2026 Dengfeng Liu
|
||||
# aw-bpf service - manages aw-bpf lifecycle: aw-eventd, xdpi module, BPF maps
|
||||
|
||||
START=90
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
NAME=aw-bpf
|
||||
PROG="/usr/bin/aw-eventd"
|
||||
CONF_FILE="/var/etc/eventd.conf"
|
||||
BPF_FS_ROOT="/sys/fs/bpf"
|
||||
AW_BPF_PIN_DIR="$BPF_FS_ROOT/tc/globals"
|
||||
AW_BPF_LEGACY_PIN_DIR="$BPF_FS_ROOT/xdp/globals"
|
||||
AW_BPF_MAPS="ipv4_map ipv6_map mac_map tcp_conn_map udp_conn_map xdpi_l7_map session_events_map prog_array_map dns_ringbuf dns_ringbuf_portal dns_stats_map"
|
||||
AW_BPF_PROGS="tc_ingress tc_egress"
|
||||
AW_BPF_LAN_IFACES=""
|
||||
AW_BPF_WAN_IFACES=""
|
||||
|
||||
resolve_lan_ifaces() {
|
||||
local lan_device lan_ifname
|
||||
|
||||
AW_BPF_LAN_IFACES=""
|
||||
|
||||
config_load network
|
||||
config_get lan_device lan device
|
||||
config_get lan_ifname lan ifname
|
||||
|
||||
if [ -n "$lan_device" ] && [ -e "/sys/class/net/$lan_device" ]; then
|
||||
AW_BPF_LAN_IFACES="$lan_device"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for iface in $lan_ifname; do
|
||||
if [ -n "$iface" ] && [ -e "/sys/class/net/$iface" ]; then
|
||||
AW_BPF_LAN_IFACES="$iface"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
resolve_wan_ifaces() {
|
||||
local wan_device wan_ifname
|
||||
|
||||
AW_BPF_WAN_IFACES=""
|
||||
|
||||
config_load network
|
||||
config_get wan_device wan device
|
||||
config_get wan_ifname wan ifname
|
||||
|
||||
if [ -n "$wan_device" ] && [ -e "/sys/class/net/$wan_device" ]; then
|
||||
AW_BPF_WAN_IFACES="$wan_device"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for iface in $wan_ifname; do
|
||||
if [ -n "$iface" ] && [ -e "/sys/class/net/$iface" ]; then
|
||||
AW_BPF_WAN_IFACES="$iface"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
detach_tc_programs() {
|
||||
local iface="$1"
|
||||
|
||||
[ -z "$iface" ] && return 0
|
||||
[ ! -e "/sys/class/net/$iface" ] && return 0
|
||||
|
||||
tc filter del dev "$iface" ingress 2>/dev/null || true
|
||||
tc filter del dev "$iface" egress 2>/dev/null || true
|
||||
tc qdisc del dev "$iface" clsact 2>/dev/null || true
|
||||
}
|
||||
|
||||
attach_tc_programs() {
|
||||
local iface="$1"
|
||||
local aw_bpf_file="/lib/bpf/aw-bpf.o"
|
||||
|
||||
[ -z "$iface" ] && return 0
|
||||
[ ! -e "/sys/class/net/$iface" ] && return 0
|
||||
[ ! -f "$aw_bpf_file" ] && {
|
||||
echo "aw-bpf.o not found at $aw_bpf_file" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
tc qdisc del dev "$iface" clsact 2>/dev/null || true
|
||||
tc qdisc add dev "$iface" clsact 2>/dev/null || true
|
||||
|
||||
if ! tc filter add dev "$iface" ingress prio 1 bpf da obj "$aw_bpf_file" sec tc/ingress 2>/tmp/aw-bpf.ingress.err; then
|
||||
echo "Failed to attach aw-bpf ingress to $iface: $(cat /tmp/aw-bpf.ingress.err 2>/dev/null)" >&2
|
||||
rm -f /tmp/aw-bpf.ingress.err
|
||||
return 1
|
||||
fi
|
||||
rm -f /tmp/aw-bpf.ingress.err
|
||||
|
||||
if ! tc filter add dev "$iface" egress prio 1 bpf da obj "$aw_bpf_file" sec tc/egress 2>/tmp/aw-bpf.egress.err; then
|
||||
echo "Failed to attach aw-bpf egress to $iface: $(cat /tmp/aw-bpf.egress.err 2>/dev/null)" >&2
|
||||
rm -f /tmp/aw-bpf.egress.err
|
||||
tc filter del dev "$iface" ingress 2>/dev/null || true
|
||||
tc qdisc del dev "$iface" clsact 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
rm -f /tmp/aw-bpf.egress.err
|
||||
|
||||
echo "aw-bpf attached on $iface" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
cleanup_aw_bpf_pins() {
|
||||
local entry
|
||||
|
||||
for entry in $AW_BPF_PROGS $AW_BPF_MAPS; do
|
||||
[ -e "$BPF_FS_ROOT/$entry" ] && rm -f "$BPF_FS_ROOT/$entry"
|
||||
[ -e "$AW_BPF_PIN_DIR/$entry" ] && rm -f "$AW_BPF_PIN_DIR/$entry"
|
||||
[ -e "$AW_BPF_LEGACY_PIN_DIR/$entry" ] && rm -f "$AW_BPF_LEGACY_PIN_DIR/$entry"
|
||||
done
|
||||
}
|
||||
|
||||
load_bpf_resources() {
|
||||
echo "Loading BPF resources..." >&2
|
||||
mkdir -p "$AW_BPF_PIN_DIR" 2>/dev/null || true
|
||||
|
||||
cleanup_aw_bpf_pins
|
||||
resolve_lan_ifaces
|
||||
|
||||
if [ -z "$AW_BPF_LAN_IFACES" ]; then
|
||||
echo "No valid interfaces found from network.lan" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for iface in $AW_BPF_LAN_IFACES; do
|
||||
attach_tc_programs "$iface" || return 1
|
||||
done
|
||||
|
||||
load_dns_bpf_program
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
load_xdpi() {
|
||||
if lsmod | grep -q xdpi; then
|
||||
echo "xdpi module already loaded" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
if command -v modprobe >/dev/null 2>&1; then
|
||||
if modprobe xdpi_bpf 2>/dev/null || modprobe xdpi-bpf 2>/dev/null; then
|
||||
echo "xdpi module loaded via modprobe" >&2
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
for modfile in /lib/modules/$(uname -r)/xdpi_bpf.ko /lib/modules/$(uname -r)/xdpi-bpf.ko /lib/modules/$(uname -r)/extra/xdpi_bpf.ko /lib/modules/$(uname -r)/extra/xdpi-bpf.ko /lib/xdpi_bpf.ko /lib/xdpi-bpf.ko; do
|
||||
if [ -f "$modfile" ]; then
|
||||
insmod "$modfile" 2>/dev/null && echo "xdpi module loaded via insmod $modfile" >&2 && return 0 || true
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Warning: xdpi module not found or failed to load" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
load_dns_bpf_program() {
|
||||
local dns_bpf_file="/lib/bpf/dns-bpf.o"
|
||||
local xdp_pin_base="/sys/fs/bpf/aw"
|
||||
|
||||
if [ ! -f "$dns_bpf_file" ]; then
|
||||
echo "dns-bpf.o not found at $dns_bpf_file" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
mkdir -p "$xdp_pin_base" 2>/dev/null || true
|
||||
|
||||
resolve_wan_ifaces
|
||||
if [ -z "$AW_BPF_WAN_IFACES" ]; then
|
||||
echo "No valid WAN interfaces found. Skip loading DNS XDP." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
for iface in $AW_BPF_WAN_IFACES; do
|
||||
echo "Attaching DNS XDP program to $iface..." >&2
|
||||
ip link set dev "$iface" xdp off 2>/dev/null || true
|
||||
if ! ip link set dev "$iface" xdp obj "$dns_bpf_file" sec xdp 2>/tmp/aw-bpf.xdp.err; then
|
||||
echo "Failed to attach dns xdp to $iface: $(cat /tmp/aw-bpf.xdp.err 2>/dev/null)" >&2
|
||||
rm -f /tmp/aw-bpf.xdp.err
|
||||
return 1
|
||||
fi
|
||||
rm -f /tmp/aw-bpf.xdp.err
|
||||
done
|
||||
|
||||
sleep 1
|
||||
|
||||
local xdp_prog_id=$(bpftool prog show name xdp_dns_monitor 2>/dev/null | head -n1 | cut -d':' -f1)
|
||||
[ ! -e "$xdp_pin_base/dns_ringbuf" ] && bpftool map pin name dns_ringbuf "$xdp_pin_base/dns_ringbuf" 2>/dev/null
|
||||
if [ ! -e "$xdp_pin_base/dns_ringbuf_portal" ]; then
|
||||
bpftool map pin name dns_ringbuf_por "$xdp_pin_base/dns_ringbuf_portal" 2>/dev/null || \
|
||||
bpftool map pin name dns_ringbuf_portal "$xdp_pin_base/dns_ringbuf_portal" 2>/dev/null
|
||||
fi
|
||||
[ ! -e "$xdp_pin_base/dns_stats_map" ] && bpftool map pin name dns_stats_map "$xdp_pin_base/dns_stats_map" 2>/dev/null
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
cleanup_bpf_resources() {
|
||||
echo "Cleaning up global BPF resources..." >&2
|
||||
|
||||
resolve_lan_ifaces
|
||||
for iface in $AW_BPF_LAN_IFACES; do
|
||||
detach_tc_programs "$iface"
|
||||
done
|
||||
|
||||
resolve_wan_ifaces
|
||||
for iface in $AW_BPF_WAN_IFACES; do
|
||||
ip link set dev "$iface" xdp off 2>/dev/null || true
|
||||
done
|
||||
|
||||
rm -rf /sys/fs/bpf/aw/dns_* 2>/dev/null
|
||||
cleanup_aw_bpf_pins
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
unload_xdpi() {
|
||||
if ! lsmod | grep -q xdpi; then
|
||||
echo "xdpi module not loaded" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
local rmmod_output
|
||||
rmmod_output=$(rmmod xdpi_bpf 2>&1 || rmmod xdpi-bpf 2>&1)
|
||||
local rmmod_status=$?
|
||||
|
||||
if [ $rmmod_status -ne 0 ]; then
|
||||
echo "Note: Failed to unload xdpi module (may still be in use). Error: $rmmod_output" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
if lsmod | grep -q xdpi; then
|
||||
echo "Note: Module still loaded after rmmod attempt" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "xdpi module unloaded successfully" >&2
|
||||
return 0
|
||||
}
|
||||
|
||||
generate_conf() {
|
||||
local enable_event_log
|
||||
local location_id ap_device_id ap_mac_address ap_longitude ap_latitude
|
||||
|
||||
config_load "aw-bpf"
|
||||
config_get enable_event_log "common" enable_event_log "0"
|
||||
config_get location_id "common" location_id ""
|
||||
config_get ap_device_id "common" ap_device_id ""
|
||||
config_get ap_mac_address "common" ap_mac_address ""
|
||||
config_get ap_longitude "common" ap_longitude ""
|
||||
config_get ap_latitude "common" ap_latitude ""
|
||||
|
||||
# Inherit location/device audit info from wifidogx if not explicitly set in aw-bpf
|
||||
if [ -f "/etc/config/wifidogx" ]; then
|
||||
local w_loc w_dev w_mac w_long w_lat
|
||||
config_load "wifidogx"
|
||||
config_get w_loc "common" location_id ""
|
||||
config_get w_dev "common" ap_device_id ""
|
||||
config_get w_mac "common" ap_mac_address ""
|
||||
config_get w_long "common" ap_longitude ""
|
||||
config_get w_lat "common" ap_latitude ""
|
||||
|
||||
[ -z "$location_id" ] && location_id="$w_loc"
|
||||
[ -z "$ap_device_id" ] && ap_device_id="$w_dev"
|
||||
[ -z "$ap_mac_address" ] && ap_mac_address="$w_mac"
|
||||
[ -z "$ap_longitude" ] && ap_longitude="$w_long"
|
||||
[ -z "$ap_latitude" ] && ap_latitude="$w_lat"
|
||||
fi
|
||||
|
||||
[ -z "$location_id" ] && location_id="UNKNOWN"
|
||||
[ -z "$ap_device_id" ] && ap_device_id="UNKNOWN"
|
||||
[ -z "$ap_mac_address" ] && ap_mac_address="UNKNOWN"
|
||||
[ -z "$ap_longitude" ] && ap_longitude="0.000000"
|
||||
[ -z "$ap_latitude" ] && ap_latitude="0.000000"
|
||||
|
||||
mkdir -p /var/etc
|
||||
cat > "$CONF_FILE" <<EOF
|
||||
# Generated automatically by /etc/init.d/aw-bpf from UCI
|
||||
enable_event_log=${enable_event_log}
|
||||
location_id=${location_id}
|
||||
ap_device_id=${ap_device_id}
|
||||
ap_mac_address=${ap_mac_address}
|
||||
ap_longitude=${ap_longitude}
|
||||
ap_latitude=${ap_latitude}
|
||||
EOF
|
||||
}
|
||||
|
||||
start_service() {
|
||||
load_xdpi
|
||||
load_bpf_resources
|
||||
|
||||
if ! command -v "$PROG" >/dev/null 2>&1; then
|
||||
echo "aw-eventd binary not found" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
generate_conf
|
||||
|
||||
echo "Starting aw-bpf service (aw-eventd)..." >&2
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" -c "$CONF_FILE"
|
||||
procd_set_param respawn 3600 5 0
|
||||
procd_set_param file "$CONF_FILE"
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
echo "Stopping aw-bpf service..." >&2
|
||||
cleanup_bpf_resources
|
||||
unload_xdpi
|
||||
echo "aw-bpf cleanup completed" >&2
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "aw-bpf"
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
restart
|
||||
}
|
||||
+5
-3
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gecoosac
|
||||
PKG_VERSION:=2.2.20251015
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
PKG_MAINTAINER:=Roc Lai <laipeng668@qq.com>
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
@@ -43,8 +43,8 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=Network
|
||||
TITLE:=gecoosac server (version $(PKG_VERSION))
|
||||
URL:=http://www.cnrouter.com/
|
||||
DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util
|
||||
PROVIDES:=gecoosac-files
|
||||
DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util +coreutils-timeout
|
||||
PROVIDES:=gecoosac-files gecoosac-common
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
@@ -66,11 +66,13 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/etc/gecoosac/tls
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_DIR) $(1)/usr/share/gecoosac
|
||||
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin
|
||||
$(INSTALL_BIN) ./files/etc/init.d/gecoosac $(1)/etc/init.d/gecoosac
|
||||
$(INSTALL_BIN) ./files/etc/uci-defaults/gecoosac $(1)/etc/uci-defaults/gecoosac
|
||||
$(INSTALL_CONF) ./files/etc/config/gecoosac $(1)/etc/config/gecoosac
|
||||
$(INSTALL_DATA) ./files/usr/share/gecoosac/common.sh $(1)/usr/share/gecoosac/common.sh
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
. /lib/functions.sh
|
||||
. /lib/functions/procd.sh
|
||||
. /usr/share/gecoosac/common.sh
|
||||
|
||||
USE_PROCD=1
|
||||
|
||||
@@ -10,13 +11,9 @@ START=90
|
||||
STOP=10
|
||||
|
||||
PROG=/usr/bin/gecoosac
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
DEFAULT_LANG=zh
|
||||
CERT_TIMEOUT=60
|
||||
CERT_RENEW_BEFORE=2592000
|
||||
|
||||
init_conf() {
|
||||
local section_type
|
||||
@@ -47,193 +44,6 @@ init_conf() {
|
||||
config_get "log" "config" "log" "0"
|
||||
}
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
is_secure_dir() {
|
||||
local allow_sticky="$2" owner permissions metadata
|
||||
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
||||
metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1
|
||||
set -- $metadata
|
||||
permissions="$1"
|
||||
owner="$3"
|
||||
[ "$owner" = "0" ] || return 1
|
||||
case "$permissions" in
|
||||
d?????????) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \
|
||||
[ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then
|
||||
[ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_secure_upload_dir() {
|
||||
local path current part rest
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
[ -d "$path" ] && [ ! -L "$path" ] || return 1
|
||||
current="/"
|
||||
rest="${path#/}"
|
||||
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
current="${current%/}/$part"
|
||||
case "$current" in
|
||||
/tmp) is_secure_dir "$current" 1 || return 1 ;;
|
||||
*) is_secure_dir "$current" || return 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_port() {
|
||||
case "$1" in
|
||||
""|*[!0-9]*) return 1 ;;
|
||||
@@ -242,66 +52,6 @@ is_port() {
|
||||
[ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ]
|
||||
}
|
||||
|
||||
run_with_timeout() {
|
||||
local timeout pid i
|
||||
|
||||
timeout="$1"
|
||||
shift
|
||||
|
||||
"$@" >/dev/null 2>&1 &
|
||||
pid="$!"
|
||||
i=0
|
||||
|
||||
while kill -0 "$pid" >/dev/null 2>&1; do
|
||||
if [ "$i" -ge "$timeout" ]; then
|
||||
kill "$pid" >/dev/null 2>&1
|
||||
sleep 1
|
||||
kill -9 "$pid" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
run_with_timeout_output() {
|
||||
local timeout output pid i
|
||||
|
||||
timeout="$1"
|
||||
output="$2"
|
||||
shift 2
|
||||
|
||||
"$@" >"$output" 2>/dev/null &
|
||||
pid="$!"
|
||||
i=0
|
||||
|
||||
while kill -0 "$pid" >/dev/null 2>&1; do
|
||||
if [ "$i" -ge "$timeout" ]; then
|
||||
kill "$pid" >/dev/null 2>&1
|
||||
kill -9 "$pid" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
is_readable_regular_file() {
|
||||
local path="$1" tmp_dir status
|
||||
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-file.XXXXXX)" || return 1
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/check" \
|
||||
/bin/sh -c '[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ]' \
|
||||
gecoosac-file-check "$path" </dev/null
|
||||
status="$?"
|
||||
rm -rf "$tmp_dir"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
is_ipv4() {
|
||||
local value="$1"
|
||||
local part count
|
||||
@@ -328,48 +78,55 @@ san_has_entry() {
|
||||
local san="$1"
|
||||
local entry="$2"
|
||||
|
||||
echo "$san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -F -x -q "$entry"
|
||||
printf '%s\n' "$san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -F -x -q "$entry"
|
||||
}
|
||||
|
||||
cert_matches_san() {
|
||||
local cert_file="$1" cert_host="$2" cert_ip="$3"
|
||||
local san tmp_dir
|
||||
run_with_timeout_output() {
|
||||
local limit="$1" output="$2"
|
||||
shift 2
|
||||
|
||||
is_readable_regular_file "$cert_file" || return 1
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-cert.XXXXXX)" || return 1
|
||||
if ! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/san" \
|
||||
openssl x509 -in "$cert_file" -noout -ext subjectAltName </dev/null; then
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
san="$(cat "$tmp_dir/san" 2>/dev/null)"
|
||||
rm -rf "$tmp_dir"
|
||||
[ -n "$san" ] || return 1
|
||||
san_has_entry "$san" "DNS:${cert_host}" || return 1
|
||||
[ -z "$cert_ip" ] || san_has_entry "$san" "IP Address:${cert_ip}" || return 1
|
||||
|
||||
return 0
|
||||
# coreutils-timeout waits for completion directly and also bounds commands
|
||||
# which ignore SIGTERM; no polling delay or orphaned sleep process is needed.
|
||||
timeout -k 1 "$limit" "$@" >"$output" 2>/dev/null
|
||||
}
|
||||
|
||||
cert_matches_key() {
|
||||
local cert_file="$1" key_file="$2"
|
||||
local cert_pub key_pub tmp_dir
|
||||
run_with_timeout() {
|
||||
local limit="$1"
|
||||
shift
|
||||
run_with_timeout_output "$limit" /dev/null "$@"
|
||||
}
|
||||
|
||||
cert_pair_valid() {
|
||||
local cert_file="$1" key_file="$2" cert_host="$3" cert_ip="$4"
|
||||
local min_lifetime="${5:-0}" tmp_dir cert_pub key_pub san status
|
||||
|
||||
is_readable_regular_file "$cert_file" || return 1
|
||||
is_readable_regular_file "$key_file" || return 1
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-cert.XXXXXX)" || return 1
|
||||
if ! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/cert.pub" \
|
||||
openssl x509 -in "$cert_file" -noout -pubkey </dev/null || \
|
||||
! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/key.pub" \
|
||||
openssl pkey -in "$key_file" -pubout </dev/null; then
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
status=1
|
||||
if run_with_timeout "$CERT_TIMEOUT" /bin/sh -c '
|
||||
[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ] &&
|
||||
[ -f "$2" ] && [ -r "$2" ] && [ -s "$2" ]
|
||||
' gecoosac-file-check "$cert_file" "$key_file" </dev/null && \
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/cert" \
|
||||
openssl x509 -in "$cert_file" -noout -pubkey -ext subjectAltName -checkend "$min_lifetime" </dev/null && \
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/key.pub" \
|
||||
openssl pkey -in "$key_file" -pubout </dev/null && \
|
||||
run_with_timeout "$CERT_TIMEOUT" \
|
||||
openssl verify -partial_chain -trusted "$cert_file" "$cert_file" </dev/null; then
|
||||
# Trust the configured leaf only for the local validity-period check;
|
||||
# browser trust still depends on the certificate installed by the user.
|
||||
cert_pub="$(sed -n '/^-----BEGIN PUBLIC KEY-----$/,/^-----END PUBLIC KEY-----$/p' "$tmp_dir/cert")"
|
||||
key_pub="$(cat "$tmp_dir/key.pub")"
|
||||
if [ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ]; then
|
||||
status=0
|
||||
if [ -n "$cert_host" ]; then
|
||||
san="$(cat "$tmp_dir/cert")"
|
||||
san_has_entry "$san" "DNS:${cert_host}" || status=1
|
||||
[ -z "$cert_ip" ] || san_has_entry "$san" "IP Address:${cert_ip}" || status=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
cert_pub="$(cat "$tmp_dir/cert.pub" 2>/dev/null)"
|
||||
key_pub="$(cat "$tmp_dir/key.pub" 2>/dev/null)"
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
[ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ]
|
||||
return "$status"
|
||||
}
|
||||
|
||||
generate_default_cert() {
|
||||
@@ -389,7 +146,7 @@ generate_default_cert() {
|
||||
cert_ip="$(uci -q get network.lan.ipaddr)"
|
||||
is_ipv4 "$cert_ip" || cert_ip=""
|
||||
|
||||
if is_readable_regular_file "$DEFAULT_KEY_FILE" && cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then
|
||||
if cert_pair_valid "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE" "$cert_host" "$cert_ip" "$CERT_RENEW_BEFORE"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -423,9 +180,7 @@ generate_default_cert() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -s "$tmp_crt" ] || [ ! -s "$tmp_key" ] || \
|
||||
! cert_matches_san "$tmp_crt" "$cert_host" "$cert_ip" || \
|
||||
! cert_matches_key "$tmp_crt" "$tmp_key" || \
|
||||
if ! cert_pair_valid "$tmp_crt" "$tmp_key" "$cert_host" "$cert_ip" "$CERT_RENEW_BEFORE" || \
|
||||
! chmod 600 "$tmp_key" || ! chmod 644 "$tmp_crt"; then
|
||||
rm -f "$tmp_crt" "$tmp_key"
|
||||
logger -t gecoosac "failed to validate default HTTPS certificate"
|
||||
@@ -468,10 +223,8 @@ generate_default_cert() {
|
||||
old_key=1
|
||||
fi
|
||||
|
||||
if mv "$tmp_key" "$DEFAULT_KEY_FILE" && \
|
||||
mv "$tmp_crt" "$DEFAULT_CRT_FILE" && \
|
||||
cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && \
|
||||
cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then
|
||||
# Both temporary files were validated above; rename preserves their contents.
|
||||
if mv "$tmp_key" "$DEFAULT_KEY_FILE" && mv "$tmp_crt" "$DEFAULT_CRT_FILE"; then
|
||||
rm -rf "$backup_dir"
|
||||
return 0
|
||||
fi
|
||||
@@ -601,31 +354,6 @@ ensure_secure_dir_tree() {
|
||||
done
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file|tls) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_upload_storage_root() {
|
||||
local path="$1" root
|
||||
|
||||
@@ -677,10 +405,8 @@ prepare_service() {
|
||||
if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then
|
||||
if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then
|
||||
generate_default_cert || return 1
|
||||
fi
|
||||
|
||||
if ! cert_matches_key "$crt_file" "$key_file"; then
|
||||
logger -t gecoosac "HTTPS certificate and key do not match or cannot be read"
|
||||
elif ! cert_pair_valid "$crt_file" "$key_file"; then
|
||||
logger -t gecoosac "HTTPS certificate is not currently valid, or its private key cannot be read or does not match"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /usr/share/gecoosac/common.sh
|
||||
|
||||
changed=0
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
LEGACY_UPLOAD_DIR=/etc/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
OLD_CRT_FILE=/etc/gecoosac/tls/1.crt
|
||||
OLD_KEY_FILE=/etc/gecoosac/tls/1.key
|
||||
CONFIG_COMPAT=2
|
||||
@@ -26,192 +23,6 @@ set_default() {
|
||||
changed=1
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
path_has_mount() {
|
||||
local root line mount_path
|
||||
|
||||
root="$(normalize_path "$1")" || return 2
|
||||
[ -r /proc/self/mountinfo ] || return 2
|
||||
|
||||
while IFS= read -r line; do
|
||||
mount_path="$(printf '%s\n' "$line" | cut -d ' ' -f 5)" || return 2
|
||||
[ -n "$mount_path" ] || return 2
|
||||
mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2
|
||||
mount_path="$(normalize_path "$mount_path")" || return 2
|
||||
is_path_in_dir "$mount_path" "$root" && return 0
|
||||
done < /proc/self/mountinfo
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
normalize_upload_dir() {
|
||||
local upload_dir normalized
|
||||
|
||||
@@ -229,38 +40,45 @@ normalize_upload_dir() {
|
||||
}
|
||||
|
||||
configured_path_in_legacy_upload() {
|
||||
local option path resolved
|
||||
local physical="$1" option role path
|
||||
|
||||
for option in db_dir piddir crt_file key_file; do
|
||||
path="$(uci -q get "gecoosac.config.${option}")"
|
||||
[ -n "$path" ] || continue
|
||||
path="$(normalize_path "$path")" || return 0
|
||||
is_path_in_dir "$path" "$LEGACY_UPLOAD_DIR" && return 0
|
||||
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 0
|
||||
resolved="$(normalize_path "$resolved")" || return 0
|
||||
is_path_in_dir "$resolved" "$LEGACY_UPLOAD_DIR" && return 0
|
||||
fi
|
||||
case "$option" in
|
||||
db_dir) role=db ;;
|
||||
piddir) role=pid ;;
|
||||
*) role="file" ;;
|
||||
esac
|
||||
protected_path_in_dir "$path" "$role" "$physical" "$LEGACY_UPLOAD_DIR" "$physical"
|
||||
case "$?" in
|
||||
1) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup_legacy_upload_dir() {
|
||||
local mount_state
|
||||
local physical mount_state
|
||||
|
||||
[ -e "$LEGACY_UPLOAD_DIR" ] || [ -L "$LEGACY_UPLOAD_DIR" ] || return 0
|
||||
if [ ! -d "$LEGACY_UPLOAD_DIR" ] || [ -L "$LEGACY_UPLOAD_DIR" ]; then
|
||||
logger -t gecoosac "refusing to remove unsafe legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
if configured_path_in_legacy_upload; then
|
||||
physical="$(readlink -f "$LEGACY_UPLOAD_DIR" 2>/dev/null)" || {
|
||||
logger -t gecoosac "unable to resolve legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
}
|
||||
physical="$(normalize_path "$physical")" || return 0
|
||||
if configured_path_in_legacy_upload "$physical"; then
|
||||
logger -t gecoosac "preserving legacy upload path referenced by configuration: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
path_has_mount "$LEGACY_UPLOAD_DIR"
|
||||
path_has_mount "$physical"
|
||||
mount_state="$?"
|
||||
case "$mount_state" in
|
||||
0)
|
||||
@@ -273,7 +91,13 @@ cleanup_legacy_upload_dir() {
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -rf "$LEGACY_UPLOAD_DIR" || {
|
||||
# Apply the same ownership and ancestor-symlink policy as RPC cleanup.
|
||||
# Relocated legacy directories are preserved for manual review.
|
||||
if ! is_secure_upload_dir "$LEGACY_UPLOAD_DIR" || [ "$physical" != "$LEGACY_UPLOAD_DIR" ]; then
|
||||
logger -t gecoosac "refusing to remove unsafe legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
rm -rf "$physical" || {
|
||||
logger -t gecoosac "failed to remove legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 1
|
||||
}
|
||||
@@ -310,7 +134,7 @@ is_managed_cert_path() {
|
||||
|
||||
is_regular_file_or_absent() {
|
||||
[ ! -e "$1" ] && [ ! -L "$1" ] && return 0
|
||||
[ -f "$1" ] && [ ! -L "$1" ]
|
||||
[ -f "$1" ] && [ ! -L "$1" ] && is_secure_upload_dir "${1%/*}"
|
||||
}
|
||||
|
||||
migrate_default_certificates() {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
# Shared path policy for the service, configuration migration and LuCI RPC.
|
||||
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file|tls) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
is_secure_dir() {
|
||||
local allow_sticky="$2" owner permissions metadata
|
||||
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
||||
metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1
|
||||
set -- $metadata
|
||||
permissions="$1"
|
||||
owner="$3"
|
||||
[ "$owner" = "0" ] || return 1
|
||||
case "$permissions" in
|
||||
d?????????) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \
|
||||
[ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then
|
||||
[ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_secure_upload_dir() {
|
||||
local path current part rest
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
[ -d "$path" ] && [ ! -L "$path" ] || return 1
|
||||
current="/"
|
||||
rest="${path#/}"
|
||||
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
current="${current%/}/$part"
|
||||
case "$current" in
|
||||
/tmp) is_secure_dir "$current" 1 || return 1 ;;
|
||||
*) is_secure_dir "$current" || return 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
path_has_mount() {
|
||||
local root mount_id parent_id device mount_root mount_path rest
|
||||
|
||||
root="$(normalize_path "$1")" || return 2
|
||||
[ -r /proc/self/mountinfo ] || return 2
|
||||
|
||||
while IFS=' ' read -r mount_id parent_id device mount_root mount_path rest; do
|
||||
[ -n "$mount_path" ] && [ -n "$rest" ] || return 2
|
||||
mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2
|
||||
mount_path="$(normalize_path "$mount_path")" || return 2
|
||||
is_path_in_dir "$mount_path" "$root" && return 0
|
||||
done < /proc/self/mountinfo
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# The checked root must be a resolved, validated directory. Return 0 for a
|
||||
# protected path, 1 for an unrelated path, or 2 when validation is impossible.
|
||||
# Keep the original spelling when resolving symlinks: link/../file and its
|
||||
# lexical normalization can refer to different files.
|
||||
protected_path_in_dir() {
|
||||
local raw_path="$1" role="$2" checked_root="$3"
|
||||
local live_logical="${4:-$3}" live_physical="${5:-$3}"
|
||||
local path="$1" physical real_path suffix mapped
|
||||
|
||||
[ -n "$path" ] || return 1
|
||||
|
||||
path="$(normalize_path "$path")" || return 2
|
||||
physical="$(managed_dir_path "$role" "$path")" || return 2
|
||||
checked_root="$(normalize_path "$checked_root")" || return 2
|
||||
live_logical="$(normalize_path "$live_logical")" || return 2
|
||||
live_physical="$(normalize_path "$live_physical")" || return 2
|
||||
|
||||
is_path_in_dir "$path" "$checked_root" && return 0
|
||||
is_path_in_dir "$physical" "$checked_root" && return 0
|
||||
real_path="$(readlink -f "$raw_path" 2>/dev/null)" || return 2
|
||||
[ -n "$real_path" ] || return 2
|
||||
real_path="$(normalize_path "$real_path")" || return 2
|
||||
is_path_in_dir "$real_path" "$checked_root" && return 0
|
||||
|
||||
[ "$checked_root" != "$live_physical" ] || {
|
||||
is_path_in_dir "$path" "$live_logical" && return 0
|
||||
is_path_in_dir "$physical" "$live_physical" && return 0
|
||||
if [ -n "$real_path" ] && is_path_in_dir "$real_path" "$live_physical"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
suffix=
|
||||
if is_path_in_dir "$path" "$live_logical"; then
|
||||
suffix="${path#"$live_logical"}"
|
||||
elif is_path_in_dir "$physical" "$live_physical"; then
|
||||
suffix="${physical#"$live_physical"}"
|
||||
elif [ -n "$real_path" ] && is_path_in_dir "$real_path" "$live_physical"; then
|
||||
suffix="${real_path#"$live_physical"}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
mapped="${checked_root%/}${suffix}"
|
||||
[ -e "$mapped" ] || [ -L "$mapped" ]
|
||||
}
|
||||
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,13 +0,0 @@
|
||||
# This is free software, licensed under the Apache License, Version 2.0
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI Support for aw-bpf traffic control
|
||||
LUCI_DEPENDS:=+aw-bpf +luci-lib-echarts
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -1,570 +0,0 @@
|
||||
/* Theme tokens: follow LuCI bootstrap vars, Argon, or JS-set data-aw-theme */
|
||||
.l7-view-container,
|
||||
.display-view-container {
|
||||
--aw-card-bg: var(--background-color-medium, var(--background-color, #f9f9f9));
|
||||
--aw-card-border: var(--border-color-medium, #e0e0e0);
|
||||
--aw-chart-bg: var(--background-color-high, #ffffff);
|
||||
--aw-text: var(--text-color-highest, var(--text-color-high, #333333));
|
||||
--aw-text-muted: var(--text-color-medium, #666666);
|
||||
--aw-accent: var(--primary-color-high, var(--primary, #3771c8));
|
||||
--aw-hover: var(--background-color-low, #f0f0f0);
|
||||
--aw-error-bg: #ffefef;
|
||||
--aw-error-fg: #c62828;
|
||||
--aw-dl: #28a745;
|
||||
--aw-ul: #007bff;
|
||||
--aw-vol-dl: #17a2b8;
|
||||
--aw-vol-ul: #6610f2;
|
||||
--aw-pkt: #6c757d;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.l7-view-container:not([data-aw-theme="light"]),
|
||||
.display-view-container:not([data-aw-theme="light"]) {
|
||||
--aw-card-bg: #2a2a2a;
|
||||
--aw-card-border: #3c3c3c;
|
||||
--aw-chart-bg: #252526;
|
||||
--aw-text: #cccccc;
|
||||
--aw-text-muted: #adb5bd;
|
||||
--aw-accent: #a5b2ff;
|
||||
--aw-hover: #333333;
|
||||
--aw-error-bg: #3a1f1f;
|
||||
--aw-error-fg: #ff8a80;
|
||||
--aw-dl: #5dd879;
|
||||
--aw-ul: #6eb6ff;
|
||||
--aw-vol-dl: #4ecbd8;
|
||||
--aw-vol-ul: #b794f6;
|
||||
--aw-pkt: #adb5bd;
|
||||
}
|
||||
}
|
||||
|
||||
[data-darkmode="true"] .l7-view-container,
|
||||
[data-darkmode="true"] .display-view-container,
|
||||
.l7-view-container[data-aw-theme="dark"],
|
||||
.display-view-container[data-aw-theme="dark"] {
|
||||
--aw-card-bg: #2a2a2a;
|
||||
--aw-card-border: #3c3c3c;
|
||||
--aw-chart-bg: #252526;
|
||||
--aw-text: #cccccc;
|
||||
--aw-text-muted: #adb5bd;
|
||||
--aw-accent: #a5b2ff;
|
||||
--aw-hover: #333333;
|
||||
--aw-error-bg: #3a1f1f;
|
||||
--aw-error-fg: #ff8a80;
|
||||
--aw-dl: #5dd879;
|
||||
--aw-ul: #6eb6ff;
|
||||
--aw-vol-dl: #4ecbd8;
|
||||
--aw-vol-ul: #b794f6;
|
||||
--aw-pkt: #adb5bd;
|
||||
}
|
||||
|
||||
[data-darkmode="false"] .l7-view-container,
|
||||
[data-darkmode="false"] .display-view-container,
|
||||
.l7-view-container[data-aw-theme="light"],
|
||||
.display-view-container[data-aw-theme="light"] {
|
||||
--aw-card-bg: #f9f9f9;
|
||||
--aw-card-border: #e0e0e0;
|
||||
--aw-chart-bg: #ffffff;
|
||||
--aw-text: #333333;
|
||||
--aw-text-muted: #666666;
|
||||
--aw-accent: #3771c8;
|
||||
--aw-hover: #f0f0f0;
|
||||
--aw-error-bg: #ffefef;
|
||||
--aw-error-fg: #c62828;
|
||||
--aw-dl: #28a745;
|
||||
--aw-ul: #007bff;
|
||||
--aw-vol-dl: #17a2b8;
|
||||
--aw-vol-ul: #6610f2;
|
||||
--aw-pkt: #6c757d;
|
||||
}
|
||||
|
||||
.aw-inner-tabs {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.aw-inner-tabs > .cbi-tabmenu {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.pie label {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpi ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.kpi li {
|
||||
margin: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kpi big {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.head {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.head .pie {
|
||||
/* min-width: 200px; */
|
||||
padding: 5px;
|
||||
flex: 1 1 30%;
|
||||
}
|
||||
|
||||
.cbi-tooltip .head .pie {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.head .kpi {
|
||||
padding: 5px;
|
||||
font-size: smaller;
|
||||
text-align: left;
|
||||
align-self: center;
|
||||
flex: 1 0 33%;
|
||||
min-width: 150px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.head .kpi ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.td.double > span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cbi-tooltip {
|
||||
box-shadow: 0 0 5px #000;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.td.hide-xs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span {
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span:first-child::before {
|
||||
content: "IPv4: ";
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span:last-child::before {
|
||||
content: "IPv6: ";
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
align-items: center; /* 垂直居中 */
|
||||
margin-bottom: 15px; /* 项间距 */
|
||||
gap: 10px; /* Label 和 Input 间距 */
|
||||
}
|
||||
|
||||
/* 标签固定最小宽度 + 右对齐 */
|
||||
.form-label {
|
||||
flex: 0 0 120px; /* 不伸缩、不收缩、基础宽度120px */
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: var(--aw-text, #333);
|
||||
}
|
||||
|
||||
.th-sort-asc::after {
|
||||
content: " ▲";
|
||||
}
|
||||
|
||||
.th-sort-desc::after {
|
||||
content: " ▼";
|
||||
}
|
||||
|
||||
.table .th {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table .th:hover {
|
||||
background-color: var(--aw-hover, #f0f0f0);
|
||||
}
|
||||
|
||||
/* L7 View Specific Styles */
|
||||
.l7-view-container #l7-error-message { color: var(--aw-error-fg, red); background-color: var(--aw-error-bg, #ffefef); border: 1px solid var(--aw-error-fg, red); padding: 10px; margin-bottom: 10px; display: none; }
|
||||
.l7-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; }
|
||||
.l7-view-container .line-chart-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .kpi-card { background-color: var(--aw-card-bg, #f9f9f9); border-radius: 8px; padding: 15px; text-align: center; border: 1px solid var(--aw-card-border, #e0e0e0); }
|
||||
.l7-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: var(--aw-accent, #3771c8); }
|
||||
.l7-view-container .kpi-card-label { font-size: 0.9em; color: var(--aw-text-muted, #666); }
|
||||
.l7-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .chart-card { background-color: var(--aw-chart-bg, #ffffff); border-radius: 8px; padding: 20px; border: 1px solid var(--aw-card-border, #e0e0e0); }
|
||||
.l7-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; color: var(--aw-text, inherit); }
|
||||
.l7-view-container .l7-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
color: white;
|
||||
}
|
||||
.l7-view-container .l7-controls-left, .l7-view-container .l7-controls-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Display View Specific Styles */
|
||||
.display-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; }
|
||||
.display-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }
|
||||
.display-view-container .kpi-card { background-color: var(--aw-card-bg, #f9f9f9); border-radius: 8px; padding: 15px; text-align: center; border: 1px solid var(--aw-card-border, #e0e0e0); }
|
||||
.display-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: var(--aw-accent, #3771c8); }
|
||||
.display-view-container .kpi-card-label { font-size: 0.9em; color: var(--aw-text-muted, #666); }
|
||||
.display-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||
.display-view-container .chart-card { background-color: var(--aw-chart-bg, #ffffff); border-radius: 8px; padding: 20px; border: 1px solid var(--aw-card-border, #e0e0e0); }
|
||||
.display-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; color: var(--aw-text, inherit); }
|
||||
.display-view-container .display-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Control Groups */
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
font-size: 1.2em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.control-input {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
padding: 6px 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.control-input:focus {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-group {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.last-updated-text {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Enhanced Select Styling */
|
||||
.cbi-input-select {
|
||||
padding: 6px 30px 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23667eea' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 12px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cbi-input-select:hover {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.cbi-input-select:focus {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Button Enhancements for Controls */
|
||||
.l7-controls .cbi-button,
|
||||
.display-controls .cbi-button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.cbi-button-action {
|
||||
background: rgba(255, 193, 7, 0.9);
|
||||
color: #333;
|
||||
border-color: rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.cbi-button-action:hover {
|
||||
background: #ffc107;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
|
||||
.cbi-button-positive {
|
||||
background: rgba(40, 167, 69, 0.9);
|
||||
color: white;
|
||||
border-color: rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.cbi-button-positive:hover {
|
||||
background: #28a745;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
|
||||
/* Responsive Controls */
|
||||
@media screen and (max-width: 768px) {
|
||||
.l7-controls,
|
||||
.display-controls {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.l7-controls-left,
|
||||
.l7-controls-right,
|
||||
.control-buttons {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table Cell Icon Styles */
|
||||
.activity-indicator {
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.icon, .btn-icon, .th-icon {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.th-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Cell Specific Styles */
|
||||
.sid-cell, .id-cell, .host-cell, .hostname-cell, .protocol-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.speed-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.speed-cell.download {
|
||||
color: var(--aw-dl, #28a745);
|
||||
}
|
||||
|
||||
.speed-cell.upload {
|
||||
color: var(--aw-ul, #007bff);
|
||||
}
|
||||
|
||||
.volume-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.volume-cell.download {
|
||||
color: var(--aw-vol-dl, #17a2b8);
|
||||
}
|
||||
|
||||
.volume-cell.upload {
|
||||
color: var(--aw-vol-ul, #6610f2);
|
||||
}
|
||||
|
||||
.packet-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
color: var(--aw-pkt, #6c757d);
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* Data value with monospace font for better alignment */
|
||||
.data-value {
|
||||
font-family: 'Courier New', Consolas, monospace;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Table header enhancements */
|
||||
.table .th {
|
||||
font-weight: 600;
|
||||
padding: 10px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table .td {
|
||||
padding: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.protocol-icon.l7::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #5470c6;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.protocol-icon.domain::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #91cc75;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Button Enhancements */
|
||||
.cbi-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cbi-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.cbi-button-edit {
|
||||
background-color: #ffc107;
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.cbi-button-edit:hover {
|
||||
background-color: #e0a800;
|
||||
border-color: #e0a800;
|
||||
}
|
||||
|
||||
.cbi-button-remove {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cbi-button-remove:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
}
|
||||
|
||||
.cbi-button-add {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cbi-button-add:hover:not(:disabled) {
|
||||
background-color: #218838;
|
||||
border-color: #1e7e34;
|
||||
}
|
||||
|
||||
.cbi-button-add:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Table Row Hover Effects */
|
||||
.table .tr:not(.table-titles):not(.placeholder):hover {
|
||||
background-color: var(--aw-hover, #f8f9fa);
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Active/Inactive Status Styles */
|
||||
.activity-indicator[title*="Active"] {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Responsive Icon Handling */
|
||||
@media screen and (max-width: 768px) {
|
||||
.icon, .btn-icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-icon + span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.th-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,787 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require dom';
|
||||
'require uci';
|
||||
|
||||
// Global variables from original display.js
|
||||
var chartRegistry = {};
|
||||
var hostNames = {}; // mac => hostname
|
||||
var hostInfo = {}; // ip => mac
|
||||
var hostNameMacSectionId = "";
|
||||
var isPaused = false;
|
||||
var lastUpdated = null;
|
||||
|
||||
// Line chart variables (from l7.js)
|
||||
var downloadLineChart = {}, uploadLineChart = {};
|
||||
var lineCategories = { ipv4: [], ipv6: [], mac: [] };
|
||||
var downloadSeriesData = { ipv4: {}, ipv6: {}, mac: {} };
|
||||
var uploadSeriesData = { ipv4: {}, ipv6: {}, mac: {} };
|
||||
|
||||
// Color palette for chart series
|
||||
var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
|
||||
|
||||
var resizeListenerAdded = false;
|
||||
|
||||
// Pre-fill with 60 empty points for a smooth start
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
for (var i = 0; i < 60; i++) {
|
||||
lineCategories[type].push('');
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to convert hex to rgba (from l7.js)
|
||||
function hexToRgba(hex, opacity) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ?
|
||||
'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' :
|
||||
null;
|
||||
};
|
||||
|
||||
function isDarkMode() {
|
||||
var attr = document.documentElement.getAttribute('data-darkmode');
|
||||
if (attr === 'true')
|
||||
return true;
|
||||
if (attr === 'false')
|
||||
return false;
|
||||
|
||||
var bg = getComputedStyle(document.body).backgroundColor;
|
||||
var m = bg && bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (m) {
|
||||
var lum = (0.299 * m[1] + 0.587 * m[2] + 0.114 * m[3]) / 255;
|
||||
return lum < 0.5;
|
||||
}
|
||||
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
function getChartColors() {
|
||||
var dark = isDarkMode();
|
||||
return {
|
||||
background: 'transparent',
|
||||
text: dark ? '#cccccc' : '#333333',
|
||||
muted: dark ? '#adb5bd' : '#666666',
|
||||
axis: dark ? 'rgba(255,255,255,0.28)' : 'rgba(0,0,0,0.25)',
|
||||
split: dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
|
||||
pieBorder: dark ? '#252526' : '#ffffff',
|
||||
tooltipBg: dark ? 'rgba(32,32,32,0.94)' : 'rgba(255,255,255,0.95)',
|
||||
tooltipBorder: dark ? '#555555' : '#cccccc',
|
||||
tooltipText: dark ? '#eeeeee' : '#333333'
|
||||
};
|
||||
}
|
||||
|
||||
function applyViewTheme() {
|
||||
var theme = isDarkMode() ? 'dark' : 'light';
|
||||
document.querySelectorAll('.l7-view-container, .display-view-container').forEach(function(el) {
|
||||
el.setAttribute('data-aw-theme', theme);
|
||||
});
|
||||
return theme;
|
||||
}
|
||||
|
||||
function observeChartEl(chart, el) {
|
||||
if (!chart || !el || !window.ResizeObserver || el._awRo)
|
||||
return;
|
||||
el._awRo = new ResizeObserver(function() {
|
||||
chart.resize();
|
||||
});
|
||||
el._awRo.observe(el);
|
||||
}
|
||||
|
||||
function chartAxisTheme(colors) {
|
||||
return {
|
||||
backgroundColor: colors.background,
|
||||
textStyle: { color: colors.text },
|
||||
legend: { textStyle: { color: colors.text } },
|
||||
tooltip: {
|
||||
backgroundColor: colors.tooltipBg,
|
||||
borderColor: colors.tooltipBorder,
|
||||
textStyle: { color: colors.tooltipText }
|
||||
},
|
||||
xAxis: {
|
||||
axisLine: { lineStyle: { color: colors.axis } },
|
||||
axisLabel: { color: colors.muted },
|
||||
splitLine: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
axisLine: { lineStyle: { color: colors.axis } },
|
||||
axisLabel: { color: colors.muted },
|
||||
splitLine: { lineStyle: { color: colors.split } }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
// --- Core Data Logic from display.js ---
|
||||
|
||||
loadHostNames: async function() {
|
||||
try {
|
||||
await uci.sections('hostnames', "hostname", function (params) {
|
||||
hostNameMacSectionId = params['.name'];
|
||||
for (var key in params) {
|
||||
if (key.startsWith('.')) continue;
|
||||
var macAddr = key.split('_').join(':');
|
||||
hostNames[macAddr] = params[key];
|
||||
}
|
||||
});
|
||||
|
||||
const dhcpLeases = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $2, $3, $4}', '/tmp/dhcp.leases'], 'text');
|
||||
dhcpLeases.split('\n').forEach(function(line) {
|
||||
if (line === '') return;
|
||||
const [mac, ip, hostname] = line.split(' ');
|
||||
if (!hostNames.hasOwnProperty(mac)) {
|
||||
hostNames[mac] = hostname;
|
||||
}
|
||||
});
|
||||
|
||||
const arp = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $1, $4}', '/proc/net/arp'], 'text');
|
||||
arp.split('\n').forEach(function(line, i) {
|
||||
if (i === 0 || line === '') return;
|
||||
const [ip, mac] = line.split(' ');
|
||||
hostInfo[ip] = mac;
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error getting host names:', e);
|
||||
}
|
||||
},
|
||||
|
||||
loadHostSpeedData: async function() {
|
||||
var self = this;
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv4', 'json'], 'json'),
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv6', 'json'], 'json'),
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['mac', 'json'], 'json')
|
||||
]);
|
||||
|
||||
const defaultData = {status: "success", data: []};
|
||||
const ipv4Data = results[0] || defaultData;
|
||||
const ipv6Data = results[1] || defaultData;
|
||||
const macData = results[2] || defaultData;
|
||||
|
||||
ipv4Data.data.forEach(function(item) {
|
||||
const mac = hostInfo[item.ip];
|
||||
if (mac) {
|
||||
item.mac = mac;
|
||||
item.hostname = hostNames[mac];
|
||||
}
|
||||
});
|
||||
macData.data.forEach(function(item) {
|
||||
const mac = item.mac;
|
||||
if (mac) {
|
||||
item.hostname = hostNames[mac];
|
||||
}
|
||||
});
|
||||
|
||||
self.renderHostSpeed(ipv4Data, "ipv4");
|
||||
self.renderHostSpeed(ipv6Data, "ipv6");
|
||||
self.renderHostSpeed(macData, "mac");
|
||||
|
||||
lastUpdated = new Date();
|
||||
var timestampEl = document.getElementById('display-last-updated');
|
||||
if (timestampEl) {
|
||||
timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString());
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error polling data:', e);
|
||||
}
|
||||
},
|
||||
|
||||
pollData: function() {
|
||||
poll.add(L.bind(async function() {
|
||||
if (isPaused) return;
|
||||
await this.loadHostNames();
|
||||
await this.loadHostSpeedData();
|
||||
}, this), 5);
|
||||
},
|
||||
|
||||
resizeAllCharts: function() {
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
if (downloadLineChart[type])
|
||||
downloadLineChart[type].resize();
|
||||
if (uploadLineChart[type])
|
||||
uploadLineChart[type].resize();
|
||||
});
|
||||
Object.keys(chartRegistry).forEach(function(chartId) {
|
||||
if (chartRegistry[chartId])
|
||||
chartRegistry[chartId].resize();
|
||||
});
|
||||
},
|
||||
|
||||
bindTabChartResize: function(root) {
|
||||
var self = this;
|
||||
if (!root)
|
||||
return;
|
||||
var host = root.parentNode || root;
|
||||
if (host._awTabResizeBound)
|
||||
return;
|
||||
host._awTabResizeBound = true;
|
||||
|
||||
var schedule = function() {
|
||||
setTimeout(function() { self.resizeAllCharts(); }, 80);
|
||||
};
|
||||
|
||||
host.addEventListener('click', function(ev) {
|
||||
if (ev.target.closest && ev.target.closest('ul.cbi-tabmenu'))
|
||||
schedule();
|
||||
});
|
||||
root.querySelectorAll('[data-tab]').forEach(function(pane) {
|
||||
pane.addEventListener('cbi-tab-active', schedule);
|
||||
});
|
||||
},
|
||||
|
||||
// --- UI Rendering and Interaction (New structure based on l7.js) ---
|
||||
|
||||
pie: function(id, data, valueFormatter) {
|
||||
var total = data.reduce(function(n, d) { return n + d.value; }, 0);
|
||||
data.sort(function(a, b) { return b.value - a.value; });
|
||||
|
||||
if (total === 0) {
|
||||
data = [{ value: 1, color: '#cccccc', name: _('no traffic') }];
|
||||
}
|
||||
|
||||
data.forEach(function(d, i) {
|
||||
if (!d.color) {
|
||||
var hue = (i * 137.508) % 360;
|
||||
d.color = 'hsl(' + hue + ', 75%, 55%)';
|
||||
}
|
||||
});
|
||||
|
||||
var colors = getChartColors();
|
||||
var option = {
|
||||
backgroundColor: colors.background,
|
||||
textStyle: { color: colors.text },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: colors.tooltipBg,
|
||||
borderColor: colors.tooltipBorder,
|
||||
textStyle: { color: colors.tooltipText },
|
||||
formatter: function(params) {
|
||||
if (valueFormatter) {
|
||||
// 将 ECharts params 对象转换为自定义格式
|
||||
return valueFormatter({
|
||||
name: params.name,
|
||||
value: params.value,
|
||||
percent: params.percent.toFixed(2)
|
||||
});
|
||||
}
|
||||
return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)';
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['25%', '80%'],
|
||||
avoidLabelOverlap: false,
|
||||
padAngle: 10,
|
||||
itemStyle: { borderRadius: 10, borderColor: colors.pieBorder, borderWidth: 2 },
|
||||
label: { show: false, position: 'center', color: colors.text },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold', color: colors.text } },
|
||||
labelLine: { show: false },
|
||||
data: data.map(function(d) {
|
||||
return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } };
|
||||
})
|
||||
}]
|
||||
};
|
||||
|
||||
var dom = typeof id === 'string' ? document.getElementById(id) : id;
|
||||
if (!chartRegistry[id]) {
|
||||
chartRegistry[id] = echarts.init(dom);
|
||||
observeChartEl(chartRegistry[id], dom);
|
||||
}
|
||||
chartRegistry[id].setOption(option, true);
|
||||
return chartRegistry[id];
|
||||
},
|
||||
|
||||
updateStackedLineCharts: function(type, perHostDownload, perHostUpload) {
|
||||
var now = new Date().toLocaleTimeString();
|
||||
lineCategories[type].push(now);
|
||||
lineCategories[type].shift();
|
||||
|
||||
var processChartData = function(seriesData, perHostData) {
|
||||
var allHosts = Object.keys(seriesData);
|
||||
Object.keys(perHostData).forEach(function(host) {
|
||||
if (allHosts.indexOf(host) === -1) {
|
||||
allHosts.push(host);
|
||||
}
|
||||
});
|
||||
|
||||
allHosts.forEach(function(host) {
|
||||
if (!seriesData[host]) {
|
||||
seriesData[host] = Array(59).fill(0);
|
||||
}
|
||||
var rate = perHostData[host] || 0;
|
||||
seriesData[host].push(rate);
|
||||
seriesData[host].shift();
|
||||
});
|
||||
|
||||
return Object.keys(seriesData).map(function(host, index) {
|
||||
var color = colorPalette[index % colorPalette.length];
|
||||
return {
|
||||
name: host,
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: { width: 1, color: color },
|
||||
showSymbol: false,
|
||||
itemStyle: { color: color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: hexToRgba(color, 0.5) },
|
||||
{ offset: 1, color: hexToRgba(color, 0) }
|
||||
])
|
||||
},
|
||||
data: seriesData[host]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var downloadChartSeries = processChartData(downloadSeriesData[type], perHostDownload);
|
||||
var uploadChartSeries = processChartData(uploadSeriesData[type], perHostUpload);
|
||||
|
||||
var legendData = downloadChartSeries.map(function(s) { return s.name; });
|
||||
|
||||
var colors = getChartColors();
|
||||
if (downloadLineChart[type]) {
|
||||
downloadLineChart[type].setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center', textStyle: { color: colors.text } },
|
||||
series: downloadChartSeries,
|
||||
xAxis: { data: lineCategories[type] }
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadLineChart[type]) {
|
||||
uploadLineChart[type].setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center', textStyle: { color: colors.text } },
|
||||
series: uploadChartSeries,
|
||||
xAxis: { data: lineCategories[type] }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderHostSpeed: function(data, type) {
|
||||
if (!data || data.status !== "success" || !Array.isArray(data.data)) return;
|
||||
|
||||
var rows = [];
|
||||
var txRateData = [], rxRateData = [];
|
||||
var txVolumeData = [], rxVolumeData = [];
|
||||
var tx_rate_total = 0, rx_rate_total = 0;
|
||||
var tx_bytes_total = 0, rx_bytes_total = 0;
|
||||
var perHostTxRate = {};
|
||||
var perHostRxRate = {};
|
||||
|
||||
data.data.forEach(item => {
|
||||
if (!item || !item.incoming || !item.outgoing) return;
|
||||
|
||||
var host = item.ip || item.mac || '';
|
||||
var hostname = item.hostname || hostNames[item.mac] || '';
|
||||
var displayName = hostname || host;
|
||||
|
||||
// 判断连接是否活跃
|
||||
var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0;
|
||||
var activityIcon = isActive ? '🟢' : '⚪';
|
||||
|
||||
rows.push([
|
||||
E('span', { 'class': 'host-cell' }, [
|
||||
E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon),
|
||||
E('span', {}, ' ' + host)
|
||||
]),
|
||||
E('span', { 'class': 'hostname-cell' }, [
|
||||
E('span', { 'class': 'icon' }, hostname ? '👤' : '❓'),
|
||||
E('span', {}, ' ' + (hostname || _('Unknown')))
|
||||
]),
|
||||
E('span', { 'class': 'speed-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.incoming.rate))
|
||||
]),
|
||||
E('span', { 'class': 'volume-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes))
|
||||
]),
|
||||
E('span', { 'class': 'packet-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets))
|
||||
]),
|
||||
E('span', { 'class': 'speed-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.outgoing.rate))
|
||||
]),
|
||||
E('span', { 'class': 'volume-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes))
|
||||
]),
|
||||
E('span', { 'class': 'packet-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets))
|
||||
]),
|
||||
E('div', { 'class': 'button-container' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-edit',
|
||||
'style': 'margin-right: 5px;',
|
||||
'click': ui.createHandlerFn(this, () => this.handleEditSpeed(host, item.mac, hostname, type))
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '✏️'),
|
||||
E('span', {}, ' ' + _('Edit'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-remove',
|
||||
'click': ui.createHandlerFn(this, () => this.handleDeleteHost(host, type))
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '🗑️'),
|
||||
E('span', {}, ' ' + _('Delete'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
rx_rate_total += item.outgoing.rate;
|
||||
tx_rate_total += item.incoming.rate;
|
||||
rx_bytes_total += item.outgoing.total_bytes;
|
||||
tx_bytes_total += item.incoming.total_bytes;
|
||||
|
||||
rxRateData.push({ value: item.outgoing.rate, label: displayName });
|
||||
txRateData.push({ value: item.incoming.rate, label: displayName });
|
||||
rxVolumeData.push({ value: item.outgoing.total_bytes, label: displayName });
|
||||
txVolumeData.push({ value: item.incoming.total_bytes, label: displayName });
|
||||
|
||||
perHostTxRate[displayName] = (perHostTxRate[displayName] || 0) + item.incoming.rate;
|
||||
perHostRxRate[displayName] = (perHostRxRate[displayName] || 0) + item.outgoing.rate;
|
||||
});
|
||||
|
||||
this.updateStackedLineCharts(type, perHostTxRate, perHostRxRate);
|
||||
|
||||
var table = document.getElementById(type + '-speed-data');
|
||||
cbi_update_table(table, rows, E('em', _('No data recorded yet.')));
|
||||
|
||||
this.pie(type + '-tx-rate-pie', txRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-rx-rate-pie', rxRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-tx-volume-pie', txVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-rx-volume-pie', rxVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`);
|
||||
|
||||
var hostEl = document.getElementById(type + '-host-val');
|
||||
if (hostEl) hostEl.textContent = data.data.length;
|
||||
|
||||
var txRateEl = document.getElementById(type + '-tx-rate-val');
|
||||
if (txRateEl) txRateEl.textContent = '%1024.2mBps'.format(tx_rate_total);
|
||||
|
||||
var rxRateEl = document.getElementById(type + '-rx-rate-val');
|
||||
if (rxRateEl) rxRateEl.textContent = '%1024.2mBps'.format(rx_rate_total);
|
||||
|
||||
var txVolEl = document.getElementById(type + '-tx-volume-val');
|
||||
if (txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total);
|
||||
|
||||
var rxVolEl = document.getElementById(type + '-rx-volume-val');
|
||||
if (rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total);
|
||||
},
|
||||
|
||||
// --- Interaction Handlers from display.js ---
|
||||
|
||||
handleDeleteHost: function(host, type) {
|
||||
ui.showModal(_('Delete Host'), [
|
||||
E('p', _('Are you sure you want to delete this host?')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button-negative', 'click': ui.createHandlerFn(this, async () => {
|
||||
try {
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'del', host], 'text');
|
||||
this.loadHostSpeedData();
|
||||
ui.hideModal();
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
ui.hideModal();
|
||||
}
|
||||
})}, _('Delete'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleEditSpeed: function(host, mac, hostname, type) {
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'json'], 'json').then(L.bind(res => {
|
||||
let rate_limit_dl = 0, rate_limit_ul = 0;
|
||||
if (res && res.status === 'success' && Array.isArray(res.data)) {
|
||||
const item = res.data.find(d => (d.ip === host || d.mac === host));
|
||||
if (item) {
|
||||
rate_limit_dl = (item.incoming.incoming_rate_limit || 0) / 1024 / 1024;
|
||||
rate_limit_ul = (item.outgoing.outgoing_rate_limit || 0) / 1024 / 1024;
|
||||
}
|
||||
}
|
||||
this.displaySpeedLimitDialog(host, mac, hostname, type, rate_limit_dl, rate_limit_ul);
|
||||
}, this)).catch(e => {
|
||||
console.error('Error getting speed limit:', e);
|
||||
this.displaySpeedLimitDialog(host, mac, hostname, type, 0, 0);
|
||||
});
|
||||
},
|
||||
|
||||
displaySpeedLimitDialog: function(host, mac, hostname, type, dl, ul) {
|
||||
const inputDom = E('input', { type: 'text', id: 'host-name', class: 'cbi-input-text', value: hostname, disabled: !mac });
|
||||
|
||||
ui.showModal(_('Edit Speed Limit'), [
|
||||
E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Host')), E('span',{}, host) ]),
|
||||
E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Hostname')), inputDom ]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', { 'class': 'form-label' }, _('Download Limit')),
|
||||
E('input', { type: 'number', id: 'dl-rate', class: 'cbi-input-number', min: '0', value: dl }),
|
||||
E('span',{}, " Mbps")
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', { 'class': 'form-label' }, _('Upload Limit')),
|
||||
E('input', { type: 'number', id: 'ul-rate', class: 'cbi-input-number', min: '0', value: ul }),
|
||||
E('span',{}, " Mbps")
|
||||
]),
|
||||
E('div', { 'class': 'cbi-page-actions right' }, [
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'click': ui.hideModal }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': ui.createHandlerFn(this, async ev => {
|
||||
const dl_val = document.getElementById('dl-rate').value;
|
||||
const ul_val = document.getElementById('ul-rate').value;
|
||||
const newName = document.getElementById('host-name').value;
|
||||
try {
|
||||
if (mac && newName !== hostname) {
|
||||
hostNames[mac] = newName;
|
||||
await uci.set('hostnames', hostNameMacSectionId, mac.split(':').join('_'), newName);
|
||||
await uci.save('hostnames');
|
||||
await uci.apply('hostnames');
|
||||
}
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'update', host, "downrate", dl_val*1024*1024 || '0', "uprate", ul_val*1024*1024 || '0']);
|
||||
this.loadHostSpeedData();
|
||||
ui.addNotification(null, E('p',_('Speed limit updated')));
|
||||
ui.hideModal();
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
}
|
||||
})}, _('Save'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
validateData: function(value, type) {
|
||||
if (typeof value !== 'string') return false;
|
||||
const ipv4Regex = /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})$/i;
|
||||
const macRegex = /^([0-9A-Fa-f]{2}([-:]))([0-9A-Fa-f]{2}\2){4}[0-9A-Fa-f]{2}$|^([0-9A-Fa-f]{12})$/i;
|
||||
return (type === 'ipv4') ? ipv4Regex.test(value) : (type === 'ipv6') ? ipv6Regex.test(value) : macRegex.test(value);
|
||||
},
|
||||
|
||||
createAddControls: function(type, placeholder) {
|
||||
const input = E('input', {
|
||||
type: 'text',
|
||||
class: 'cbi-input-text control-input',
|
||||
style: (type === 'ipv6') ? 'width:320px' : 'width:180px',
|
||||
placeholder: _(placeholder)
|
||||
});
|
||||
const addBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-add',
|
||||
disabled: true
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '➕'),
|
||||
E('span', {}, ' ' + _('Add'))
|
||||
]);
|
||||
const refreshBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
click: () => this.loadHostSpeedData()
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '🔄'),
|
||||
E('span', {}, ' ' + _('Refresh'))
|
||||
]);
|
||||
|
||||
input.addEventListener('input', () => { addBtn.disabled = (input.value.trim() === ''); });
|
||||
addBtn.addEventListener('click', ui.createHandlerFn(this, async () => {
|
||||
const value = input.value.trim();
|
||||
if (!this.validateData(value, type)) {
|
||||
return ui.addNotification(null, E('p', _('Data format error')));
|
||||
}
|
||||
try {
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'add', value]);
|
||||
this.loadHostSpeedData();
|
||||
ui.addNotification(null, E('p',_('Updated successfully!')));
|
||||
input.value = '';
|
||||
addBtn.disabled = true;
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
}
|
||||
}));
|
||||
|
||||
return E('div', { 'class': 'display-controls' }, [
|
||||
E('div', { 'class': 'control-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '🖥️'),
|
||||
E('label', { 'class': 'control-label' }, _('Add Host:')),
|
||||
input
|
||||
]),
|
||||
E('div', { 'class': 'control-buttons' }, [
|
||||
addBtn,
|
||||
refreshBtn,
|
||||
E('div', { 'class': 'control-group status-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '🕐'),
|
||||
E('span', { 'id': 'display-last-updated', 'class': 'last-updated-text' }, _('Ready'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
initializeUI: function() {
|
||||
applyViewTheme();
|
||||
if (window.echarts) {
|
||||
var self = this;
|
||||
var colors = getChartColors();
|
||||
var axisTheme = chartAxisTheme(colors);
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
var dlChartEl = document.getElementById(type + '-download-speed-line-chart');
|
||||
var ulChartEl = document.getElementById(type + '-upload-speed-line-chart');
|
||||
if (!dlChartEl || !ulChartEl) return;
|
||||
|
||||
var baseChartOption = {
|
||||
backgroundColor: axisTheme.backgroundColor,
|
||||
textStyle: axisTheme.textStyle,
|
||||
legend: axisTheme.legend,
|
||||
tooltip: Object.assign({
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
if (!params || params.length === 0) {
|
||||
return null;
|
||||
}
|
||||
var tooltipContent = params[0].axisValueLabel + '<br/>';
|
||||
params.sort(function(a, b) { return b.value - a.value; });
|
||||
params.forEach(function(item) {
|
||||
if (item.value > 0) {
|
||||
tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mBps'.format(item.value) + '<br/>';
|
||||
}
|
||||
});
|
||||
return tooltipContent;
|
||||
}
|
||||
}, axisTheme.tooltip),
|
||||
grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: lineCategories[type],
|
||||
axisLine: axisTheme.xAxis.axisLine,
|
||||
axisLabel: axisTheme.xAxis.axisLabel,
|
||||
splitLine: axisTheme.xAxis.splitLine
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: axisTheme.yAxis.axisLine,
|
||||
splitLine: axisTheme.yAxis.splitLine,
|
||||
axisLabel: { formatter: function(val) { return '%1024.2mBps'.format(val); }, color: colors.muted }
|
||||
},
|
||||
series: []
|
||||
};
|
||||
|
||||
downloadLineChart[type] = echarts.init(dlChartEl);
|
||||
downloadLineChart[type].setOption(baseChartOption);
|
||||
observeChartEl(downloadLineChart[type], dlChartEl);
|
||||
|
||||
uploadLineChart[type] = echarts.init(ulChartEl);
|
||||
uploadLineChart[type].setOption(baseChartOption);
|
||||
observeChartEl(uploadLineChart[type], ulChartEl);
|
||||
});
|
||||
|
||||
// 添加窗口大小变化监听器,使图表能够响应式调整
|
||||
if (!resizeListenerAdded) {
|
||||
var resizeTimer = null;
|
||||
var resizeHandler = function() {
|
||||
// 使用防抖,避免频繁触发 resize
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
}
|
||||
resizeTimer = setTimeout(function() {
|
||||
self.resizeAllCharts();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
resizeListenerAdded = true;
|
||||
}
|
||||
|
||||
this.pollData();
|
||||
} else {
|
||||
setTimeout(this.initializeUI.bind(this), 50);
|
||||
}
|
||||
},
|
||||
|
||||
// --- Main Render Function (New) ---
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
const createTab = (type, title, placeholder) => {
|
||||
var innerTabs = E('div', { 'class': 'aw-inner-tabs' }, [
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': type + '-trend', 'data-tab-title': _('Speed Trend') }, [
|
||||
E('div', { 'class': 'dashboard-container' }, [
|
||||
E('div', { 'class': 'kpi-row' }, [
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-host-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Hosts')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ])
|
||||
]),
|
||||
E('div', { 'class': 'line-chart-row' }, [
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Download Speed')]),
|
||||
E('div', { id: type + '-download-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Upload Speed')]),
|
||||
E('div', { id: type + '-upload-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': type + '-share', 'data-tab-title': _('Traffic Share') }, [
|
||||
E('div', { 'class': 'dashboard-container' }, [
|
||||
E('div', { 'class': 'chart-grid' }, [
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Speed / Host')]), E('div', { id: type + '-tx-rate-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Speed / Host')]), E('div', { id: type + '-rx-rate-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Total')]), E('div', { id: type + '-tx-volume-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Total')]), E('div', { id: type + '-rx-volume-pie', style: 'width:100%; height:300px;' }) ])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': type + '-hosts', 'data-tab-title': _('Host List') }, [
|
||||
E('table', { 'class': 'table', 'id': type + '-speed-data' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🖥️'), ' ', _('Host') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '👤'), ' ', _('Hostname') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download Total') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download Packets') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload Total') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload Packets') ]),
|
||||
E('th', { 'class': 'th center' }, [ E('span', { 'class': 'th-icon' }, '⚙️'), ' ', _('Actions') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr placeholder' }, [ E('td', { 'class': 'td', 'colspan': '9' }, [ E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) ]) ])
|
||||
]),
|
||||
self.createAddControls(type, placeholder)
|
||||
])
|
||||
]);
|
||||
|
||||
return E('div', { 'class': 'cbi-section', 'data-tab': type, 'data-tab-title': _(title) }, [
|
||||
innerTabs
|
||||
]);
|
||||
};
|
||||
|
||||
var tabContainer = E('div', {}, [
|
||||
createTab('ipv4', 'IPv4', 'Please enter a valid IPv4 address'),
|
||||
createTab('ipv6', 'IPv6', 'Please enter a valid IPv6 address'),
|
||||
createTab('mac', 'MAC', 'Please enter a valid MAC address')
|
||||
]);
|
||||
|
||||
var node = E([], [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/aw-bpf.css') }),
|
||||
E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }),
|
||||
E('div', { 'class': 'l7-view-container', 'data-aw-theme': isDarkMode() ? 'dark' : 'light' }, [
|
||||
E('h2', [ _('Host Speed Monitor') ]),
|
||||
tabContainer
|
||||
])
|
||||
]);
|
||||
|
||||
tabContainer.querySelectorAll('.aw-inner-tabs').forEach(function(inner) {
|
||||
ui.tabs.initTabGroup(inner.childNodes);
|
||||
});
|
||||
ui.tabs.initTabGroup(tabContainer.childNodes);
|
||||
this.bindTabChartResize(tabContainer);
|
||||
|
||||
setTimeout(() => this.initializeUI(), 0);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -1,996 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require dom';
|
||||
|
||||
var chartRegistry = {};
|
||||
var downloadLineChart, uploadLineChart;
|
||||
|
||||
// Data structures for stacked line charts
|
||||
var lineCategories = [];
|
||||
var downloadSeriesData = {};
|
||||
var uploadSeriesData = {};
|
||||
|
||||
// Color palette for chart series
|
||||
var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
|
||||
|
||||
var currentSortInfo = {
|
||||
table: null,
|
||||
column: null,
|
||||
reverse: false
|
||||
};
|
||||
var sidLookupTable = {};
|
||||
var isPaused = false;
|
||||
var lastUpdated = null;
|
||||
var pollActive = false;
|
||||
var lastSIDData = null;
|
||||
var lastL7ProtoData = null;
|
||||
var resizeListenerAdded = false;
|
||||
var resizeTimer = null;
|
||||
|
||||
// Pre-fill with 60 empty points for a smooth start
|
||||
for (var i = 0; i < 60; i++) {
|
||||
lineCategories.push('');
|
||||
}
|
||||
|
||||
// Helper to convert hex to rgba
|
||||
function hexToRgba(hex, opacity) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ?
|
||||
'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' :
|
||||
null;
|
||||
};
|
||||
|
||||
function isDarkMode() {
|
||||
var attr = document.documentElement.getAttribute('data-darkmode');
|
||||
if (attr === 'true')
|
||||
return true;
|
||||
if (attr === 'false')
|
||||
return false;
|
||||
|
||||
var bg = getComputedStyle(document.body).backgroundColor;
|
||||
var m = bg && bg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||
if (m) {
|
||||
var lum = (0.299 * m[1] + 0.587 * m[2] + 0.114 * m[3]) / 255;
|
||||
return lum < 0.5;
|
||||
}
|
||||
|
||||
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
}
|
||||
|
||||
function getChartColors() {
|
||||
var dark = isDarkMode();
|
||||
return {
|
||||
background: 'transparent',
|
||||
text: dark ? '#cccccc' : '#333333',
|
||||
muted: dark ? '#adb5bd' : '#666666',
|
||||
axis: dark ? 'rgba(255,255,255,0.28)' : 'rgba(0,0,0,0.25)',
|
||||
split: dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.08)',
|
||||
pieBorder: dark ? '#252526' : '#ffffff',
|
||||
tooltipBg: dark ? 'rgba(32,32,32,0.94)' : 'rgba(255,255,255,0.95)',
|
||||
tooltipBorder: dark ? '#555555' : '#cccccc',
|
||||
tooltipText: dark ? '#eeeeee' : '#333333'
|
||||
};
|
||||
}
|
||||
|
||||
function applyViewTheme() {
|
||||
var theme = isDarkMode() ? 'dark' : 'light';
|
||||
document.querySelectorAll('.l7-view-container, .display-view-container').forEach(function(el) {
|
||||
el.setAttribute('data-aw-theme', theme);
|
||||
});
|
||||
return theme;
|
||||
}
|
||||
|
||||
function observeChartEl(chart, el) {
|
||||
if (!chart || !el || !window.ResizeObserver || el._awRo)
|
||||
return;
|
||||
el._awRo = new ResizeObserver(function() {
|
||||
chart.resize();
|
||||
});
|
||||
el._awRo.observe(el);
|
||||
}
|
||||
|
||||
function chartAxisTheme(colors) {
|
||||
return {
|
||||
backgroundColor: colors.background,
|
||||
textStyle: { color: colors.text },
|
||||
legend: { textStyle: { color: colors.text } },
|
||||
tooltip: {
|
||||
backgroundColor: colors.tooltipBg,
|
||||
borderColor: colors.tooltipBorder,
|
||||
textStyle: { color: colors.tooltipText }
|
||||
},
|
||||
xAxis: {
|
||||
axisLine: { lineStyle: { color: colors.axis } },
|
||||
axisLabel: { color: colors.muted },
|
||||
splitLine: { show: false }
|
||||
},
|
||||
yAxis: {
|
||||
axisLine: { lineStyle: { color: colors.axis } },
|
||||
axisLabel: { color: colors.muted },
|
||||
splitLine: { lineStyle: { color: colors.split } }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return view.extend({
|
||||
hasXdns: false,
|
||||
xdnsDomains: {},
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
this.loadSIDData(),
|
||||
this.loadL7ProtoData(),
|
||||
this.checkXdnsStatus()
|
||||
]);
|
||||
},
|
||||
|
||||
checkXdnsStatus: function() {
|
||||
var self = this;
|
||||
return fs.stat('/usr/bin/xdns-ctl').then(function(stat) {
|
||||
if (stat && stat.type === 'file') {
|
||||
self.hasXdns = true;
|
||||
return fs.read_direct('/etc/xdns/whitelist.txt').then(function(content) {
|
||||
var domains = {};
|
||||
if (content) {
|
||||
content.split('\n').forEach(function(line) {
|
||||
line = line.trim();
|
||||
if (!line || line.charAt(0) === '#') return;
|
||||
if (line.indexOf('*.') === 0) line = line.substring(2);
|
||||
if (line.charAt(0) === '.') line = line.substring(1);
|
||||
domains[line.toLowerCase()] = true;
|
||||
});
|
||||
}
|
||||
self.xdnsDomains = domains;
|
||||
return true;
|
||||
}).catch(function() {
|
||||
self.xdnsDomains = {};
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
self.hasXdns = false;
|
||||
return false;
|
||||
}
|
||||
}).catch(function() {
|
||||
self.hasXdns = false;
|
||||
return false;
|
||||
});
|
||||
},
|
||||
|
||||
isDomainProxied: function(dName) {
|
||||
if (!dName || !this.xdnsDomains) return false;
|
||||
dName = dName.toLowerCase();
|
||||
if (this.xdnsDomains[dName]) return true;
|
||||
var parts = dName.split('.');
|
||||
for (var i = 1; i < parts.length - 1; i++) {
|
||||
var parent = parts.slice(i).join('.');
|
||||
if (this.xdnsDomains[parent]) return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
handleAddXdnsDomain: function(domain, btn) {
|
||||
var self = this;
|
||||
if (!domain) return;
|
||||
btn.disabled = true;
|
||||
var origText = btn.textContent;
|
||||
btn.textContent = _('添加中...');
|
||||
|
||||
fs.exec_direct('/usr/bin/xdns-ctl', ['add-domain', domain]).then(function() {
|
||||
self.xdnsDomains[domain.toLowerCase()] = true;
|
||||
ui.addNotification(null, E('p', _('域名「%s」已成功加入 xdns-bpf 代理名单并即刻生效!').format(domain)), 'info');
|
||||
if (lastL7ProtoData) {
|
||||
self.renderL7ProtoData(lastL7ProtoData);
|
||||
}
|
||||
}).catch(function(err) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = origText;
|
||||
ui.addNotification(null, E('p', _('加入代理名单失败: %s').format(err.message || err)), 'error');
|
||||
});
|
||||
},
|
||||
|
||||
showError: function(message) {
|
||||
var errorEl = document.getElementById('l7-error-message');
|
||||
if (errorEl) {
|
||||
errorEl.textContent = message;
|
||||
errorEl.style.display = 'block';
|
||||
}
|
||||
},
|
||||
|
||||
hideError: function() {
|
||||
var errorEl = document.getElementById('l7-error-message');
|
||||
if (errorEl) {
|
||||
errorEl.style.display = 'none';
|
||||
}
|
||||
},
|
||||
|
||||
loadSIDData: function() {
|
||||
var self = this;
|
||||
return fs.exec_direct('/usr/bin/aw-bpfctl', ['sid', 'json'], 'json').then(function(result) {
|
||||
self.hideError();
|
||||
lastSIDData = result;
|
||||
return result;
|
||||
}).catch(function(error) {
|
||||
console.error('Error loading SID data:', error);
|
||||
self.showError(_('Error loading SID data: %s').format(error.message));
|
||||
return { status: 'error', data: [] };
|
||||
});
|
||||
},
|
||||
|
||||
loadL7ProtoData: function() {
|
||||
var self = this;
|
||||
return fs.exec_direct('/usr/bin/aw-bpfctl', ['l7', 'json'], 'json').then(function(result) {
|
||||
self.hideError();
|
||||
return result;
|
||||
}).catch(function(error) {
|
||||
console.error('Error loading L7 protocol data:', error);
|
||||
self.showError(_('Error loading L7 protocol data: %s').format(error.message));
|
||||
return { status: 'error', data: [] };
|
||||
});
|
||||
},
|
||||
|
||||
updateStackedLineCharts: function(perServiceDownload, perServiceUpload) {
|
||||
var now = new Date().toLocaleTimeString();
|
||||
lineCategories.push(now);
|
||||
lineCategories.shift();
|
||||
|
||||
var processChartData = function(seriesData, perServiceData) {
|
||||
var allServices = Object.keys(seriesData);
|
||||
Object.keys(perServiceData).forEach(function(service) {
|
||||
if (allServices.indexOf(service) === -1) {
|
||||
allServices.push(service);
|
||||
}
|
||||
});
|
||||
|
||||
allServices.forEach(function(service) {
|
||||
if (!seriesData[service]) {
|
||||
seriesData[service] = Array(59).fill(0);
|
||||
}
|
||||
var rate = perServiceData[service] || 0;
|
||||
seriesData[service].push(rate);
|
||||
seriesData[service].shift();
|
||||
});
|
||||
|
||||
return Object.keys(seriesData).map(function(service, index) {
|
||||
var color = colorPalette[index % colorPalette.length];
|
||||
return {
|
||||
name: service,
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: { width: 1, color: color },
|
||||
showSymbol: false,
|
||||
itemStyle: { color: color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: hexToRgba(color, 0.5) },
|
||||
{ offset: 1, color: hexToRgba(color, 0) }
|
||||
])
|
||||
},
|
||||
data: seriesData[service]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var downloadChartSeries = processChartData(downloadSeriesData, perServiceDownload);
|
||||
var uploadChartSeries = processChartData(uploadSeriesData, perServiceUpload);
|
||||
|
||||
var legendData = downloadChartSeries.map(function(s) { return s.name; });
|
||||
var colors = getChartColors();
|
||||
|
||||
if (downloadLineChart) {
|
||||
downloadLineChart.setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center', textStyle: { color: colors.text } },
|
||||
series: downloadChartSeries,
|
||||
xAxis: { data: lineCategories }
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadLineChart) {
|
||||
uploadLineChart.setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center', textStyle: { color: colors.text } },
|
||||
series: uploadChartSeries,
|
||||
xAxis: { data: lineCategories }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
pie: function(id, data, valueFormatter) {
|
||||
var total = data.reduce(function(n, d) { return n + d.value; }, 0);
|
||||
|
||||
data.sort(function(a, b) { return b.value - a.value; });
|
||||
|
||||
if (total === 0) {
|
||||
data = [{ value: 1, color: '#cccccc', name: _('no traffic') }];
|
||||
}
|
||||
|
||||
data.forEach(function(d, i) {
|
||||
if (!d.color) {
|
||||
var hue = (i * 137.508) % 360;
|
||||
d.color = 'hsl(' + hue + ', 75%, 55%)';
|
||||
}
|
||||
});
|
||||
|
||||
var colors = getChartColors();
|
||||
var option = {
|
||||
backgroundColor: colors.background,
|
||||
textStyle: { color: colors.text },
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
backgroundColor: colors.tooltipBg,
|
||||
borderColor: colors.tooltipBorder,
|
||||
textStyle: { color: colors.tooltipText },
|
||||
formatter: function(params) {
|
||||
if (valueFormatter) {
|
||||
// 将 ECharts params 对象转换为自定义格式
|
||||
return valueFormatter({
|
||||
name: params.name,
|
||||
value: params.value,
|
||||
percent: params.percent.toFixed(2)
|
||||
});
|
||||
}
|
||||
return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)';
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['25%', '80%'],
|
||||
avoidLabelOverlap: false,
|
||||
padAngle: 10,
|
||||
itemStyle: { borderRadius: 10, borderColor: colors.pieBorder, borderWidth: 2 },
|
||||
label: { show: false, position: 'center', color: colors.text },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold', color: colors.text } },
|
||||
labelLine: { show: false },
|
||||
data: data.map(function(d) {
|
||||
return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } };
|
||||
})
|
||||
}]
|
||||
};
|
||||
|
||||
var dom = typeof id === 'string' ? document.getElementById(id) : id;
|
||||
|
||||
if (!chartRegistry[id]) {
|
||||
chartRegistry[id] = echarts.init(dom);
|
||||
observeChartEl(chartRegistry[id], dom);
|
||||
}
|
||||
|
||||
chartRegistry[id].setOption(option, true);
|
||||
|
||||
return chartRegistry[id];
|
||||
},
|
||||
|
||||
sortTable: function(table, column) {
|
||||
var tbody = table.querySelector('tbody');
|
||||
if (!tbody) return;
|
||||
var rows = Array.from(tbody.querySelectorAll('tr:not(.table-titles):not(.placeholder)'));
|
||||
var reverse = (currentSortInfo.table === table && currentSortInfo.column === column) ? !currentSortInfo.reverse : false;
|
||||
|
||||
table.querySelectorAll('th').forEach(function(th) {
|
||||
th.classList.remove('th-sort-asc', 'th-sort-desc');
|
||||
});
|
||||
|
||||
var th = table.querySelector('th:nth-child(' + (column + 1) + ')');
|
||||
th.classList.add(reverse ? 'th-sort-desc' : 'th-sort-asc');
|
||||
|
||||
rows.sort(function(row1, row2) {
|
||||
var a = row1.cells[column].getAttribute('data-value') || row1.cells[column].textContent;
|
||||
var b = row2.cells[column].getAttribute('data-value') || row2.cells[column].textContent;
|
||||
|
||||
if (!isNaN(a) && !isNaN(b)) { a = Number(a); b = Number(b); }
|
||||
|
||||
if (a < b) return reverse ? 1 : -1;
|
||||
if (a > b) return reverse ? -1 : 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
currentSortInfo.table = table;
|
||||
currentSortInfo.column = column;
|
||||
currentSortInfo.reverse = reverse;
|
||||
|
||||
rows.forEach(function(row) { tbody.removeChild(row); });
|
||||
rows.forEach(function(row) { tbody.appendChild(row); });
|
||||
},
|
||||
|
||||
formatMbps: function(bits) {
|
||||
if (typeof bits !== 'number') return '0.00 Mbps';
|
||||
return (bits / 1024 / 1024).toFixed(2) + ' Mbps';
|
||||
},
|
||||
|
||||
formatMB: function(bytes) {
|
||||
if (typeof bytes !== 'number') return '0.00 MB';
|
||||
return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
||||
},
|
||||
|
||||
renderSIDData: function(data) {
|
||||
var rows = [];
|
||||
var txRateData = [], rxRateData = [];
|
||||
var txVolumeData = [], rxVolumeData = [];
|
||||
var tx_rate_total = 0, rx_rate_total = 0;
|
||||
var tx_bytes_total = 0, rx_bytes_total = 0;
|
||||
var perServiceTxRate = {};
|
||||
var perServiceRxRate = {};
|
||||
var self = this;
|
||||
var allItems = [];
|
||||
|
||||
if (data && data.status === 'success' && Array.isArray(data.data)) {
|
||||
allItems = data.data;
|
||||
var listSizeEl = document.getElementById('sid-size-select');
|
||||
var listSize = listSizeEl ? parseInt(listSizeEl.value, 10) : 10;
|
||||
|
||||
var activeConnections = allItems.filter(function(item) { return item.incoming.rate > 0 || item.outgoing.rate > 0; });
|
||||
var inactiveConnections = allItems.filter(function(item) { return item.incoming.rate === 0 && item.outgoing.rate === 0; });
|
||||
|
||||
activeConnections.sort(function(a, b) { return (b.incoming.rate + b.outgoing.rate) - (a.incoming.rate + a.outgoing.rate); });
|
||||
inactiveConnections.sort(function(a, b) { return b.incoming.total_bytes - a.incoming.total_bytes; });
|
||||
|
||||
var displayData = activeConnections;
|
||||
if (displayData.length < listSize) {
|
||||
displayData = displayData.concat(inactiveConnections.slice(0, listSize - displayData.length));
|
||||
}
|
||||
|
||||
if (displayData.length > listSize) {
|
||||
displayData = displayData.slice(0, listSize);
|
||||
}
|
||||
|
||||
displayData.forEach(function(item) {
|
||||
var domainOrL7Proto = 'unknown';
|
||||
var lookupInfo = sidLookupTable[item.sid];
|
||||
|
||||
if (lookupInfo) {
|
||||
domainOrL7Proto = lookupInfo.name;
|
||||
} else if (item.sid_type === 'Domain' && item.domain && item.domain !== 'unknown') {
|
||||
domainOrL7Proto = item.domain;
|
||||
} else if (item.sid_type === 'L7' && item.l7_proto_desc && item.l7_proto_desc !== 'unknown') {
|
||||
domainOrL7Proto = item.l7_proto_desc;
|
||||
}
|
||||
|
||||
// 判断连接是否活跃
|
||||
var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0;
|
||||
var activityIcon = isActive ? '🟢' : '⚪';
|
||||
|
||||
rows.push([
|
||||
E('span', { 'class': 'sid-cell' }, [
|
||||
E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon),
|
||||
E('span', {}, ' ' + item.sid)
|
||||
]),
|
||||
E('span', { 'class': 'protocol-cell' }, [
|
||||
E('span', { 'class': 'protocol-icon' }, '🌐'),
|
||||
E('span', {}, ' ' + domainOrL7Proto)
|
||||
]),
|
||||
[ item.incoming.rate, E('span', { 'class': 'speed-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.incoming.rate))
|
||||
])],
|
||||
[ item.incoming.total_bytes, E('span', { 'class': 'volume-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes))
|
||||
])],
|
||||
[ item.incoming.total_packets, E('span', { 'class': 'packet-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets))
|
||||
])],
|
||||
[ item.outgoing.rate, E('span', { 'class': 'speed-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mbps'.format(item.outgoing.rate))
|
||||
])],
|
||||
[ item.outgoing.total_bytes, E('span', { 'class': 'volume-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes))
|
||||
])],
|
||||
[ item.outgoing.total_packets, E('span', { 'class': 'packet-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets))
|
||||
])]
|
||||
]);
|
||||
|
||||
txRateData.push({ value: item.incoming.rate, label: domainOrL7Proto });
|
||||
rxRateData.push({ value: item.outgoing.rate, label: domainOrL7Proto });
|
||||
txVolumeData.push({ value: item.incoming.total_bytes, label: domainOrL7Proto });
|
||||
rxVolumeData.push({ value: item.outgoing.total_bytes, label: domainOrL7Proto });
|
||||
|
||||
perServiceTxRate[domainOrL7Proto] = (perServiceTxRate[domainOrL7Proto] || 0) + item.incoming.rate;
|
||||
perServiceRxRate[domainOrL7Proto] = (perServiceRxRate[domainOrL7Proto] || 0) + item.outgoing.rate;
|
||||
});
|
||||
|
||||
allItems.forEach(function(item) {
|
||||
tx_rate_total += item.incoming.rate;
|
||||
rx_rate_total += item.outgoing.rate;
|
||||
tx_bytes_total += item.incoming.total_bytes;
|
||||
rx_bytes_total += item.outgoing.total_bytes;
|
||||
});
|
||||
}
|
||||
|
||||
this.updateStackedLineCharts(perServiceTxRate, perServiceRxRate);
|
||||
|
||||
var table = document.getElementById('sid-data');
|
||||
cbi_update_table(table, rows, E('em', _('No data recorded yet.')));
|
||||
|
||||
var headers = table.querySelectorAll('th');
|
||||
|
||||
if (!table.hasAttribute('data-sort-initialized')) {
|
||||
headers.forEach(function(header, index) {
|
||||
header.style.cursor = 'pointer';
|
||||
header.addEventListener('click', function() { self.sortTable(table, index); });
|
||||
});
|
||||
table.setAttribute('data-sort-initialized', 'true');
|
||||
}
|
||||
|
||||
table.querySelectorAll('tr:not(.table-titles):not(.placeholder)').forEach(function(row, rowIndex) {
|
||||
if (!rows[rowIndex]) return;
|
||||
Array.from(row.cells).forEach(function(cell, cellIndex) {
|
||||
if (Array.isArray(rows[rowIndex][cellIndex])) {
|
||||
cell.setAttribute('data-value', rows[rowIndex][cellIndex][0]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.pie('sid-tx-rate-pie', txRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; });
|
||||
this.pie('sid-rx-rate-pie', rxRateData, function(p) { return p.name + ': ' + self.formatMbps(p.value) + ' (' + p.percent + '%)'; });
|
||||
this.pie('sid-tx-volume-pie', txVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; });
|
||||
this.pie('sid-rx-volume-pie', rxVolumeData, function(p) { return p.name + ': ' + self.formatMB(p.value) + ' (' + p.percent + '%)'; });
|
||||
|
||||
var sidTotalEl = document.getElementById('sid-total-val');
|
||||
if(sidTotalEl) sidTotalEl.textContent = allItems.length;
|
||||
|
||||
var txRateEl = document.getElementById('sid-tx-rate-val');
|
||||
if(txRateEl) txRateEl.textContent = '%1024.2mbps'.format(tx_rate_total);
|
||||
|
||||
var rxRateEl = document.getElementById('sid-rx-rate-val');
|
||||
if(rxRateEl) rxRateEl.textContent = '%1024.2mbps'.format(rx_rate_total);
|
||||
|
||||
var txVolEl = document.getElementById('sid-tx-volume-val');
|
||||
if(txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total);
|
||||
|
||||
var rxVolEl = document.getElementById('sid-rx-volume-val');
|
||||
if(rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total);
|
||||
|
||||
lastUpdated = new Date();
|
||||
var timestampEl = document.getElementById('last-updated');
|
||||
if (timestampEl) {
|
||||
timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString());
|
||||
}
|
||||
},
|
||||
|
||||
fillSortableTable: function(tableId, rows) {
|
||||
var table = document.getElementById(tableId);
|
||||
var self = this;
|
||||
if (!table)
|
||||
return;
|
||||
|
||||
if (!table.hasAttribute('data-sort-initialized')) {
|
||||
table.querySelectorAll('th').forEach(function(header, index) {
|
||||
header.style.cursor = 'pointer';
|
||||
header.addEventListener('click', function() { self.sortTable(table, index); });
|
||||
});
|
||||
table.setAttribute('data-sort-initialized', 'true');
|
||||
}
|
||||
|
||||
cbi_update_table(table, rows, E('em', _('No data recorded yet.')));
|
||||
|
||||
table.querySelectorAll('tr:not(.table-titles):not(.placeholder)').forEach(function(row, rowIndex) {
|
||||
if (!rows[rowIndex])
|
||||
return;
|
||||
Array.from(row.cells).forEach(function(cell, cellIndex) {
|
||||
if (Array.isArray(rows[rowIndex][cellIndex]))
|
||||
cell.setAttribute('data-value', rows[rowIndex][cellIndex][0]);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
renderL7ProtoData: function(data) {
|
||||
var self = this;
|
||||
var protoRows = [];
|
||||
var domainRows = [];
|
||||
|
||||
lastL7ProtoData = data;
|
||||
sidLookupTable = {};
|
||||
|
||||
if (data && data.status === 'success' && data.data) {
|
||||
if (Array.isArray(data.data.protocols)) {
|
||||
data.data.protocols.forEach(function(item) {
|
||||
sidLookupTable[item.sid] = { type: 'protocol', name: item.protocol };
|
||||
protoRows.push([
|
||||
[ item.id, E('span', { 'class': 'id-cell' }, item.id) ],
|
||||
E('span', { 'class': 'protocol-cell' }, [
|
||||
E('span', { 'class': 'protocol-icon l7' }, '🔌'),
|
||||
E('span', {}, ' ' + item.protocol)
|
||||
]),
|
||||
[ item.sid, E('span', { 'class': 'sid-cell' }, item.sid) ]
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
if (Array.isArray(data.data.domains)) {
|
||||
var domains = data.data.domains.slice().sort(function(a, b) {
|
||||
var ac = (b.access_count || 0) - (a.access_count || 0);
|
||||
if (ac !== 0)
|
||||
return ac;
|
||||
return (b.last_access || 0) - (a.last_access || 0);
|
||||
});
|
||||
|
||||
domains.forEach(function(item) {
|
||||
sidLookupTable[item.sid] = { type: 'domain', name: item.domain };
|
||||
var row = [
|
||||
[ item.id, E('span', { 'class': 'id-cell' }, item.id) ],
|
||||
E('span', { 'class': 'protocol-cell' }, [
|
||||
E('span', { 'class': 'protocol-icon domain' }, '🌍'),
|
||||
E('span', {}, ' ' + item.domain)
|
||||
]),
|
||||
[ item.sid, E('span', { 'class': 'sid-cell' }, item.sid) ],
|
||||
[ item.access_count || 0, E('span', { 'class': 'data-value' }, item.access_count || 0) ],
|
||||
item.first_seen_str || '-',
|
||||
item.last_access_str || '-'
|
||||
];
|
||||
|
||||
if (self.hasXdns) {
|
||||
var isProxied = self.isDomainProxied(item.domain);
|
||||
if (isProxied) {
|
||||
row.push(E('span', {
|
||||
'class': 'badge success',
|
||||
'style': 'color: #2ecc71; background: rgba(46,204,113,0.12); border: 1px solid rgba(46,204,113,0.3); padding: 2px 8px; border-radius: 4px; font-size: 85%; white-space: nowrap;'
|
||||
}, [ '✔ ', _('已代理') ]));
|
||||
} else {
|
||||
row.push(E('button', {
|
||||
'class': 'btn cbi-button cbi-button-action',
|
||||
'style': 'padding: 2px 8px; font-size: 85%; white-space: nowrap;',
|
||||
'click': function(ev) {
|
||||
var b = ev.target.closest('button');
|
||||
self.handleAddXdnsDomain(item.domain, b);
|
||||
}
|
||||
}, [ '➕ ', _('加入代理') ]));
|
||||
}
|
||||
}
|
||||
|
||||
domainRows.push(row);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this.fillSortableTable('l7-protocol-data', protoRows);
|
||||
this.fillSortableTable('l7-domain-data', domainRows);
|
||||
|
||||
var protoCountEl = document.getElementById('l7-protocol-count');
|
||||
if (protoCountEl)
|
||||
protoCountEl.textContent = protoRows.length;
|
||||
var domainCountEl = document.getElementById('l7-domain-count');
|
||||
if (domainCountEl)
|
||||
domainCountEl.textContent = domainRows.length;
|
||||
},
|
||||
|
||||
pollL7Data: function() {
|
||||
if (pollActive) return;
|
||||
|
||||
var self = this;
|
||||
pollActive = true;
|
||||
|
||||
self.loadL7ProtoData().then(function(l7data) {
|
||||
self.renderL7ProtoData(l7data);
|
||||
return self.loadSIDData();
|
||||
}).then(function(sidData){
|
||||
self.renderSIDData(sidData);
|
||||
});
|
||||
|
||||
poll.add(function() {
|
||||
if (isPaused) return Promise.resolve();
|
||||
|
||||
return self.loadL7ProtoData().then(function(data) {
|
||||
self.renderL7ProtoData(data);
|
||||
}).then(function() {
|
||||
return self.loadSIDData().then(function(data) {
|
||||
self.renderSIDData(data);
|
||||
});
|
||||
});
|
||||
}, 5);
|
||||
},
|
||||
|
||||
initializeUI: function() {
|
||||
applyViewTheme();
|
||||
if (window.echarts) {
|
||||
var self = this;
|
||||
var dlChartEl = document.getElementById('download-speed-line-chart');
|
||||
var ulChartEl = document.getElementById('upload-speed-line-chart');
|
||||
if (!dlChartEl || !ulChartEl) return;
|
||||
|
||||
var colors = getChartColors();
|
||||
var axisTheme = chartAxisTheme(colors);
|
||||
var baseChartOption = {
|
||||
backgroundColor: axisTheme.backgroundColor,
|
||||
textStyle: axisTheme.textStyle,
|
||||
legend: axisTheme.legend,
|
||||
tooltip: Object.assign({
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
if (!params || params.length === 0) {
|
||||
return null;
|
||||
}
|
||||
var tooltipContent = params[0].axisValueLabel + '<br/>';
|
||||
params.sort(function(a, b) { return b.value - a.value; });
|
||||
params.forEach(function(item) {
|
||||
if (item.value > 0) {
|
||||
tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mbps'.format(item.value) + '<br/>';
|
||||
}
|
||||
});
|
||||
return tooltipContent;
|
||||
}
|
||||
}, axisTheme.tooltip),
|
||||
grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true },
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
boundaryGap: false,
|
||||
data: lineCategories,
|
||||
axisLine: axisTheme.xAxis.axisLine,
|
||||
axisLabel: axisTheme.xAxis.axisLabel,
|
||||
splitLine: axisTheme.xAxis.splitLine
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLine: axisTheme.yAxis.axisLine,
|
||||
splitLine: axisTheme.yAxis.splitLine,
|
||||
axisLabel: { formatter: function(val) { return '%1024.2mbps'.format(val); }, color: colors.muted }
|
||||
},
|
||||
series: []
|
||||
};
|
||||
|
||||
|
||||
downloadLineChart = echarts.init(dlChartEl);
|
||||
downloadLineChart.setOption(baseChartOption);
|
||||
observeChartEl(downloadLineChart, dlChartEl);
|
||||
|
||||
uploadLineChart = echarts.init(ulChartEl);
|
||||
uploadLineChart.setOption(baseChartOption);
|
||||
observeChartEl(uploadLineChart, ulChartEl);
|
||||
|
||||
// 添加窗口大小变化监听器,使图表能够响应式调整
|
||||
if (!resizeListenerAdded) {
|
||||
var resizeTimer = null;
|
||||
var resizeHandler = function() {
|
||||
// 使用防抖,避免频繁触发 resize
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
}
|
||||
resizeTimer = setTimeout(function() {
|
||||
self.resizeAllCharts();
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
resizeListenerAdded = true;
|
||||
}
|
||||
|
||||
this.pollL7Data();
|
||||
} else {
|
||||
setTimeout(this.initializeUI.bind(this), 50);
|
||||
}
|
||||
},
|
||||
|
||||
resizeAllCharts: function() {
|
||||
if (downloadLineChart)
|
||||
downloadLineChart.resize();
|
||||
if (uploadLineChart)
|
||||
uploadLineChart.resize();
|
||||
Object.keys(chartRegistry).forEach(function(chartId) {
|
||||
if (chartRegistry[chartId])
|
||||
chartRegistry[chartId].resize();
|
||||
});
|
||||
},
|
||||
|
||||
bindTabChartResize: function(root) {
|
||||
var self = this;
|
||||
if (!root)
|
||||
return;
|
||||
var host = root.parentNode || root;
|
||||
if (host._awTabResizeBound)
|
||||
return;
|
||||
host._awTabResizeBound = true;
|
||||
|
||||
var schedule = function() {
|
||||
setTimeout(function() { self.resizeAllCharts(); }, 80);
|
||||
};
|
||||
|
||||
host.addEventListener('click', function(ev) {
|
||||
if (ev.target.closest && ev.target.closest('ul.cbi-tabmenu'))
|
||||
schedule();
|
||||
});
|
||||
root.querySelectorAll('[data-tab]').forEach(function(pane) {
|
||||
pane.addEventListener('cbi-tab-active', schedule);
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
var controls = E('div', { 'class': 'l7-controls' }, [
|
||||
E('div', { 'class': 'l7-controls-left' }, [
|
||||
E('div', { 'class': 'control-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '📊'),
|
||||
E('label', { 'for': 'sid-size-select', 'class': 'control-label' }, _('Show entries:')),
|
||||
E('select', {
|
||||
'id': 'sid-size-select',
|
||||
'class': 'cbi-input-select',
|
||||
'change': ui.createHandlerFn(this, function() {
|
||||
if (lastSIDData) {
|
||||
self.renderSIDData(lastSIDData);
|
||||
}
|
||||
})
|
||||
}, [
|
||||
E('option', { 'value': '10' }, '10'),
|
||||
E('option', { 'value': '15' }, '15'),
|
||||
E('option', { 'value': '20' }, '20'),
|
||||
E('option', { 'value': '25' }, '25'),
|
||||
E('option', { 'value': '50' }, '50')
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'l7-controls-right' }, [
|
||||
E('div', { 'class': 'control-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '🕐'),
|
||||
E('span', { 'id': 'last-updated', 'class': 'last-updated-text' }, _('Last updated: never'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action',
|
||||
'id': 'pause-resume-btn',
|
||||
'click': function(ev) {
|
||||
isPaused = !isPaused;
|
||||
var btn = ev.target;
|
||||
if (isPaused) {
|
||||
btn.innerHTML = '<span class="btn-icon">▶️</span> ' + _('Resume');
|
||||
btn.classList.remove('cbi-button-action');
|
||||
btn.classList.add('cbi-button-positive');
|
||||
} else {
|
||||
btn.innerHTML = '<span class="btn-icon">⏸️</span> ' + _('Pause');
|
||||
btn.classList.remove('cbi-button-positive');
|
||||
btn.classList.add('cbi-button-action');
|
||||
}
|
||||
}
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '⏸️'),
|
||||
E('span', {}, ' ' + _('Pause'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
var sidInnerTabs = E('div', { 'class': 'aw-inner-tabs' }, [
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'sid-trend', 'data-tab-title': _('Speed Trend') }, [
|
||||
E('div', { 'class': 'dashboard-container' }, [
|
||||
E('div', { 'class': 'kpi-row' }, [
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-total-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('L7 Protocol Data')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: 'sid-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ])
|
||||
]),
|
||||
E('div', { 'class': 'line-chart-row' }, [
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Download Speed')]),
|
||||
E('div', { id: 'download-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Upload Speed')]),
|
||||
E('div', { id: 'upload-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'sid-share', 'data-tab-title': _('Traffic Share') }, [
|
||||
E('div', { 'class': 'dashboard-container' }, [
|
||||
E('div', { 'class': 'chart-grid' }, [
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Download Speed / SID')]),
|
||||
E('div', { id: 'sid-tx-rate-pie', style: 'width: 100%; height: 300px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Upload Speed / SID')]),
|
||||
E('div', { id: 'sid-rx-rate-pie', style: 'width: 100%; height: 300px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Download Total')]),
|
||||
E('div', { id: 'sid-tx-volume-pie', style: 'width: 100%; height: 300px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Upload Total')]),
|
||||
E('div', { id: 'sid-rx-volume-pie', style: 'width: 100%; height: 300px;' })
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'sid-list', 'data-tab-title': _('SID List') }, [
|
||||
E('table', { 'class': 'table', 'id': 'sid-data' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🆔'), ' ', _('SID') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌐'), ' ', _('Domain&L7Protocol') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed (Bit/s)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download (Bytes)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download (Packets)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed (Bit/s)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload (Bytes)') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload (Packets)') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr placeholder' }, [
|
||||
E('td', { 'class': 'td', 'colspan': '8' }, [
|
||||
E('em', { 'class': 'spinning' }, [ _('Collecting data...') ])
|
||||
])
|
||||
])
|
||||
]),
|
||||
controls
|
||||
])
|
||||
]);
|
||||
|
||||
var tabContainer = E('div', {}, [
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'sid', 'data-tab-title': _('L7 SID Data') }, [
|
||||
sidInnerTabs
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'l7proto', 'data-tab-title': _('L7 Protocol Data') }, [
|
||||
E('div', { 'class': 'aw-inner-tabs' }, [
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'l7-protocols', 'data-tab-title': _('Protocol Library') }, [
|
||||
E('p', { 'class': 'cbi-section-descr' }, [
|
||||
_('Built-in L7 protocol signatures from aw-bpf.'),
|
||||
' ',
|
||||
_('Entries:'),
|
||||
' ',
|
||||
E('strong', { 'id': 'l7-protocol-count' }, '0')
|
||||
]),
|
||||
E('table', { 'class': 'table', 'id': 'l7-protocol-data' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '#️⃣'), ' ', _('ID') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🔌'), ' ', _('Protocol') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '🔑'), ' ', _('SID') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr placeholder' }, [
|
||||
E('td', { 'class': 'td', 'colspan': '3' }, [
|
||||
E('em', { 'class': 'spinning' }, [ _('Collecting data...') ])
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'cbi-section', 'data-tab': 'l7-domains', 'data-tab-title': _('常用域名') }, [
|
||||
E('p', { 'class': 'cbi-section-descr' }, [
|
||||
_('Frequently accessed domains discovered by xDPI, sorted by access count.'),
|
||||
' ',
|
||||
_('Entries:'),
|
||||
' ',
|
||||
E('strong', { 'id': 'l7-domain-count' }, '0')
|
||||
]),
|
||||
E('table', { 'class': 'table', 'id': 'l7-domain-data' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '#️⃣'), ' ', _('ID') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🌍'), ' ', _('Domain') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '🔑'), ' ', _('SID') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📊'), ' ', _('Access Count') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🕒'), ' ', _('First Seen') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🕒'), ' ', _('Last Access') ]),
|
||||
this.hasXdns ? E('th', { 'class': 'th center' }, [ E('span', { 'class': 'th-icon' }, '⚡'), ' ', _('xdns代理') ]) : null
|
||||
].filter(Boolean)),
|
||||
E('tr', { 'class': 'tr placeholder' }, [
|
||||
E('td', { 'class': 'td', 'colspan': this.hasXdns ? '7' : '6' }, [
|
||||
E('em', { 'class': 'spinning' }, [ _('Collecting data...') ])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
var node = E([], [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/aw-bpf.css') }),
|
||||
E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }),
|
||||
|
||||
E('div', { 'class': 'l7-view-container', 'data-aw-theme': isDarkMode() ? 'dark' : 'light' }, [
|
||||
E('h2', [ _('L7 Data Monitor') ]),
|
||||
E('div', { 'id': 'l7-error-message' }),
|
||||
tabContainer
|
||||
])
|
||||
]);
|
||||
|
||||
tabContainer.querySelectorAll('.aw-inner-tabs').forEach(function(inner) {
|
||||
ui.tabs.initTabGroup(inner.childNodes);
|
||||
});
|
||||
ui.tabs.initTabGroup(tabContainer.childNodes);
|
||||
this.bindTabChartResize(tabContainer);
|
||||
|
||||
setTimeout(this.initializeUI.bind(this), 0);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require form';
|
||||
'require uci';
|
||||
|
||||
return view.extend({
|
||||
load: function() {
|
||||
return uci.load('aw-bpf');
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var m, s, o;
|
||||
|
||||
m = new form.Map('aw-bpf', _('eBPF Traffic Control & DPI'),
|
||||
_('eBPF kernel-level bandwidth control, session audit logging and xDPI L7 application/domain recognition.'));
|
||||
|
||||
s = m.section(form.TypedSection, 'aw-bpf', _('General Settings'));
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
o = s.option(form.Flag, 'enable_event_log', _('Enable Session Event Logging'),
|
||||
_('Record TCP/UDP session connection events as structured JSON to system log / syslog. (DNS learning and xDPI protocol recognition are always active in background).'));
|
||||
o.rmempty = false;
|
||||
o.default = '0';
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
zh_Hans
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +0,0 @@
|
||||
#!/bin/sh
|
||||
[ -f "/etc/config/hostnames" ] || {
|
||||
echo 'config hostname' > /etc/config/hostnames
|
||||
}
|
||||
|
||||
exit 0
|
||||
@@ -1,46 +0,0 @@
|
||||
{
|
||||
"admin/qos": {
|
||||
"title": "QoS",
|
||||
"order": 60,
|
||||
"action": {
|
||||
"type": "firstchild",
|
||||
"recurse": true
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-aw-bpf" ]
|
||||
}
|
||||
},
|
||||
"admin/qos/hosts": {
|
||||
"title": "Hosts",
|
||||
"order": 10,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "aw-bpf/display"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-aw-bpf" ]
|
||||
}
|
||||
},
|
||||
"admin/qos/l7": {
|
||||
"title": "L7 application",
|
||||
"order": 20,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "aw-bpf/l7"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-aw-bpf" ]
|
||||
}
|
||||
},
|
||||
"admin/qos/settings": {
|
||||
"title": "Settings",
|
||||
"order": 30,
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "aw-bpf/settings"
|
||||
},
|
||||
"depends": {
|
||||
"acl": [ "luci-app-aw-bpf" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"luci-app-aw-bpf": {
|
||||
"description": "Grant access to LuCI app aw-bpf",
|
||||
"read": {
|
||||
"file": {
|
||||
"/usr/bin/aw-bpfctl": [ "exec" ],
|
||||
"/usr/bin/xdns-ctl": [ "exec" ],
|
||||
"/usr/bin/awk": [ "exec" ],
|
||||
"/tmp/dhcp.leases": [ "read" ],
|
||||
"/proc/net/arp": [ "read" ],
|
||||
"/etc/xdns/whitelist.txt": [ "read" ]
|
||||
},
|
||||
"uci": [ "hostnames", "network", "aw-bpf" ]
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "hostnames", "aw-bpf" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.41
|
||||
PKG_RELEASE:=13
|
||||
PKG_RELEASE:=14
|
||||
|
||||
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
|
||||
+443
@@ -0,0 +1,443 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright 2025-2026 Lucas Albers <lucas.b.albers@gmail.com>
|
||||
#
|
||||
# Layer 1 adaptive poll cap (#306). Sourced by rpcd/fwlive.
|
||||
# Always on unless test/triage override (no UCI / no product config):
|
||||
# FWLIVE_ADAPTIVE=0|false|off|no
|
||||
# or sentinel ${FWLIVE_ADAPTIVE_OFF_FILE:-<state-dir>/fwlive-adaptive-off}
|
||||
# (default under /var/run next to state — not world-writable /tmp).
|
||||
#
|
||||
# State: ${FWLIVE_ADAPTIVE_STATE_FILE:-/var/run/fwlive-state.json}
|
||||
# Lock: sibling ${FWLIVE_ADAPTIVE_LOCK_FILE:-$STATE.lock} — never the JSON
|
||||
# inode (atomic mv replaces that inode; locking it would not serialize writers).
|
||||
# Lock covers only short update — never held across ubus/filter work.
|
||||
# Hot-path budget: ≤1 flock exec per update (release by closing the fd when the
|
||||
# subshell exits — no flock -u). /proc/uptime + state I/O via shell builtins/
|
||||
# redirects (no sed/cat/jsonfilter on the adaptive path). Fail-open on missing
|
||||
# flock, lock busy, or corrupt state. Lock-busy ⇒ unlocked last-writer-wins is
|
||||
# acceptable (state stays one valid JSON line; ordering is not guaranteed).
|
||||
# Failed ubus log.read must NOT call record() — a ~0 ms failure is not "cold"
|
||||
# health and must not clear an existing hot/shed cap (#329 Hermes Q1).
|
||||
# Outside the measured duration interval: plan (pre), record/merge (post).
|
||||
# messages_received is 0 in Layer 1 — do not ash-scan the filter JSON after
|
||||
# end_cs (Grok #329 P1); accurate count belongs in the filter or a later layer.
|
||||
|
||||
FWLIVE_ADAPTIVE_STATE_FILE="${FWLIVE_ADAPTIVE_STATE_FILE:-/var/run/fwlive-state.json}"
|
||||
# Optional overrides; when unset, lock/off paths are siblings of the current state.
|
||||
# Do not bake STATE into LOCK/OFF at source time — tests may relocate STATE after source.
|
||||
|
||||
# Bucket thresholds (processing duration, ms).
|
||||
FWLIVE_ADAPTIVE_COLD_MS=100
|
||||
FWLIVE_ADAPTIVE_COOL_MS=200
|
||||
FWLIVE_ADAPTIVE_WARM_MS=800
|
||||
FWLIVE_ADAPTIVE_HOT_EXIT_MS=500
|
||||
FWLIVE_ADAPTIVE_COOL_CAP=250
|
||||
FWLIVE_ADAPTIVE_HOT_FLOOR=250
|
||||
FWLIVE_ADAPTIVE_WARM_MIN=50
|
||||
FWLIVE_ADAPTIVE_COOLDOWN_WARM_CS=300
|
||||
FWLIVE_ADAPTIVE_COOLDOWN_HOT_CS=600
|
||||
|
||||
fwlive_adaptive_lock_path() {
|
||||
if [ -n "${FWLIVE_ADAPTIVE_LOCK_FILE:-}" ]; then
|
||||
printf '%s\n' "$FWLIVE_ADAPTIVE_LOCK_FILE"
|
||||
else
|
||||
printf '%s\n' "${FWLIVE_ADAPTIVE_STATE_FILE}.lock"
|
||||
fi
|
||||
}
|
||||
|
||||
fwlive_adaptive_off_path() {
|
||||
if [ -n "${FWLIVE_ADAPTIVE_OFF_FILE:-}" ]; then
|
||||
printf '%s\n' "$FWLIVE_ADAPTIVE_OFF_FILE"
|
||||
else
|
||||
_dir=${FWLIVE_ADAPTIVE_STATE_FILE%/*}
|
||||
[ "$_dir" = "$FWLIVE_ADAPTIVE_STATE_FILE" ] && _dir=/var/run
|
||||
printf '%s\n' "$_dir/fwlive-adaptive-off"
|
||||
fi
|
||||
}
|
||||
|
||||
fwlive_adaptive_enabled() {
|
||||
case "${FWLIVE_ADAPTIVE:-1}" in
|
||||
0|false|off|no|FALSE|OFF|NO) return 1 ;;
|
||||
esac
|
||||
[ -e "$(fwlive_adaptive_off_path)" ] && return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
fwlive_adaptive_atoi() {
|
||||
# Decimal int from digit string — avoids $((08)) octal errors under ash.
|
||||
_s=$1
|
||||
_n=0
|
||||
case "$_s" in ''|*[!0-9]*) printf '0\n'; return 0 ;; esac
|
||||
while [ -n "$_s" ]; do
|
||||
_d=${_s%"${_s#?}"}
|
||||
_s=${_s#?}
|
||||
_n=$((_n * 10 + _d))
|
||||
done
|
||||
printf '%s\n' "$_n"
|
||||
}
|
||||
|
||||
fwlive_adaptive_clock_cs() {
|
||||
# /proc/uptime centiseconds — BusyBox date lacks %N. Builtin read only.
|
||||
_up=
|
||||
read -r _up _ </proc/uptime 2>/dev/null || {
|
||||
printf '%s\n' 0
|
||||
return 0
|
||||
}
|
||||
_sec=${_up%.*}
|
||||
_frac=${_up#*.}
|
||||
[ "$_frac" = "$_up" ] && _frac=0
|
||||
# At most two decimal digits (centiseconds), no cut(1).
|
||||
case "$_frac" in
|
||||
'') _frac=0 ;;
|
||||
?) _frac="${_frac}0" ;;
|
||||
??) ;;
|
||||
*)
|
||||
_a=${_frac%${_frac#?}}
|
||||
_r=${_frac#?}
|
||||
_b=${_r%${_r#?}}
|
||||
_frac="${_a}${_b}"
|
||||
;;
|
||||
esac
|
||||
_sec=$(fwlive_adaptive_atoi "$_sec")
|
||||
_frac=$(fwlive_adaptive_atoi "$_frac")
|
||||
printf '%s\n' $((_sec * 100 + _frac))
|
||||
}
|
||||
|
||||
fwlive_adaptive_state_dir_ok() {
|
||||
_path=$FWLIVE_ADAPTIVE_STATE_FILE
|
||||
_dir=${_path%/*}
|
||||
[ -n "$_dir" ] || return 1
|
||||
[ "$_dir" = "$_path" ] && return 1
|
||||
[ -L "$_dir" ] && return 1
|
||||
[ -d "$_dir" ] || return 1
|
||||
# shellcheck disable=SC3067
|
||||
[ -O "$_dir" ] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
# Extract a decimal integer field from a one-line JSON object (builtin only).
|
||||
# Prints default ($2) when missing/corrupt.
|
||||
fwlive_adaptive_json_int() {
|
||||
_blob=$1
|
||||
_key=$2
|
||||
_def=$3
|
||||
case "$_blob" in
|
||||
*"\"$_key\""*) ;;
|
||||
*) printf '%s\n' "$_def"; return 0 ;;
|
||||
esac
|
||||
_rest=${_blob#*"\"$_key\""}
|
||||
_rest=${_rest#*:}
|
||||
while case "$_rest" in ' '*) true;; *) false;; esac; do
|
||||
_rest=${_rest# }
|
||||
done
|
||||
_num=
|
||||
while :; do
|
||||
case "$_rest" in
|
||||
'') break ;;
|
||||
esac
|
||||
_c=${_rest%"${_rest#?}"}
|
||||
case "$_c" in
|
||||
[0-9]) _num="${_num}${_c}"; _rest=${_rest#?} ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
case "$_num" in
|
||||
''|*[!0-9]*) printf '%s\n' "$_def" ;;
|
||||
*) printf '%s\n' "$_num" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Extract a lowercase alpha string field.
|
||||
fwlive_adaptive_json_str() {
|
||||
_blob=$1
|
||||
_key=$2
|
||||
_def=$3
|
||||
case "$_blob" in
|
||||
*"\"$_key\""*) ;;
|
||||
*) printf '%s\n' "$_def"; return 0 ;;
|
||||
esac
|
||||
_rest=${_blob#*"\"$_key\""}
|
||||
_rest=${_rest#*:}
|
||||
while case "$_rest" in ' '*) true;; *) false;; esac; do
|
||||
_rest=${_rest# }
|
||||
done
|
||||
case "$_rest" in
|
||||
\"*) _rest=${_rest#\"}; _val=${_rest%%\"*} ;;
|
||||
*) printf '%s\n' "$_def"; return 0 ;;
|
||||
esac
|
||||
case "$_val" in
|
||||
*[!a-z]*|'') printf '%s\n' "$_def" ;;
|
||||
*) printf '%s\n' "$_val" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Print: duration_ms limit bucket warm_halved shed completed_cs
|
||||
fwlive_adaptive_read_state() {
|
||||
_d=0
|
||||
_l="${POLL_LINES_MAX:-2000}"
|
||||
_b=cold
|
||||
_w=0
|
||||
_s=0
|
||||
_c=0
|
||||
_raw=
|
||||
if [ -f "$FWLIVE_ADAPTIVE_STATE_FILE" ]; then
|
||||
IFS= read -r _raw <"$FWLIVE_ADAPTIVE_STATE_FILE" || _raw=
|
||||
fi
|
||||
if [ -n "$_raw" ]; then
|
||||
_d=$(fwlive_adaptive_json_int "$_raw" duration_ms 0)
|
||||
_l=$(fwlive_adaptive_json_int "$_raw" limit "${POLL_LINES_MAX:-2000}")
|
||||
_b=$(fwlive_adaptive_json_str "$_raw" bucket cold)
|
||||
_w=$(fwlive_adaptive_json_int "$_raw" warm_halved 0)
|
||||
_s=$(fwlive_adaptive_json_int "$_raw" shed 0)
|
||||
_c=$(fwlive_adaptive_json_int "$_raw" completed_cs 0)
|
||||
case "$_w" in 0|1) ;; *) _w=0 ;; esac
|
||||
case "$_s" in 0|1) ;; *) _s=0 ;; esac
|
||||
fi
|
||||
printf '%s %s %s %s %s %s\n' "$_d" "$_l" "$_b" "$_w" "$_s" "$_c"
|
||||
}
|
||||
|
||||
fwlive_adaptive_write_state() {
|
||||
_d=$1
|
||||
_l=$2
|
||||
_b=$3
|
||||
_w=$4
|
||||
_s=$5
|
||||
_c=$6
|
||||
fwlive_adaptive_state_dir_ok || return 0
|
||||
# Refuse to follow a symlinked state path (same discipline as logging.lock).
|
||||
[ -L "$FWLIVE_ADAPTIVE_STATE_FILE" ] && return 0
|
||||
_tmp="${FWLIVE_ADAPTIVE_STATE_FILE}.tmp.$$"
|
||||
umask 077
|
||||
printf '{"duration_ms":%s,"limit":%s,"bucket":"%s","warm_halved":%s,"shed":%s,"completed_cs":%s}\n' \
|
||||
"$_d" "$_l" "$_b" "$_w" "$_s" "$_c" >"$_tmp" 2>/dev/null || {
|
||||
rm -f "$_tmp"
|
||||
return 0
|
||||
}
|
||||
mv -f "$_tmp" "$FWLIVE_ADAPTIVE_STATE_FILE" 2>/dev/null || rm -f "$_tmp"
|
||||
return 0
|
||||
}
|
||||
|
||||
# One non-blocking flock on the persistent sibling lock file; fail-open if
|
||||
# missing/busy. Release by exiting the subshell (closes fd 9) — no flock -u.
|
||||
# Busy ⇒ run unlocked: last-writer-wins is acceptable (valid one-line JSON;
|
||||
# ordering under contention is not guaranteed).
|
||||
# Lock-open failure (redirection) also fail-opens unlocked so record() still runs.
|
||||
fwlive_adaptive_with_lock() {
|
||||
if ! command -v flock >/dev/null 2>&1; then
|
||||
"$@"
|
||||
return $?
|
||||
fi
|
||||
fwlive_adaptive_state_dir_ok || {
|
||||
"$@"
|
||||
return $?
|
||||
}
|
||||
_lock=$(fwlive_adaptive_lock_path)
|
||||
# Symlinked or non-file lock path: do not create/follow; fail open unlocked.
|
||||
if [ -L "$_lock" ] || [ -d "$_lock" ]; then
|
||||
"$@"
|
||||
return $?
|
||||
fi
|
||||
# Create lock at 0600 (logging.lock #167 — world-readable fd can take LOCK_EX).
|
||||
if [ ! -e "$_lock" ]; then
|
||||
if ! ( umask 077; : >"$_lock" ) 2>/dev/null; then
|
||||
"$@"
|
||||
return $?
|
||||
fi
|
||||
elif [ ! -w "$_lock" ]; then
|
||||
"$@"
|
||||
return $?
|
||||
fi
|
||||
(
|
||||
if flock -n 9; then
|
||||
"$@"
|
||||
exit $?
|
||||
fi
|
||||
# Lock busy — fail open (unlocked).
|
||||
"$@"
|
||||
) 9>>"$_lock"
|
||||
return $?
|
||||
}
|
||||
|
||||
fwlive_adaptive_bucket_for_ms() {
|
||||
_ms=$1
|
||||
case "$_ms" in ''|*[!0-9]*) _ms=0 ;; esac
|
||||
if [ "$_ms" -lt "$FWLIVE_ADAPTIVE_COLD_MS" ]; then
|
||||
printf '%s\n' cold
|
||||
elif [ "$_ms" -lt "$FWLIVE_ADAPTIVE_COOL_MS" ]; then
|
||||
printf '%s\n' cool
|
||||
elif [ "$_ms" -le "$FWLIVE_ADAPTIVE_WARM_MS" ]; then
|
||||
printf '%s\n' warm
|
||||
else
|
||||
printf '%s\n' hot
|
||||
fi
|
||||
}
|
||||
|
||||
# Args: requested_limit duration_ms prev_limit prev_bucket prev_warm prev_completed_cs
|
||||
# Prints next limit.
|
||||
fwlive_adaptive_compute_limit() {
|
||||
_req=$1
|
||||
_ms=$2
|
||||
_prev_l=$3
|
||||
_prev_b=$4
|
||||
_prev_w=$5
|
||||
_prev_c=$6
|
||||
_max="${POLL_LINES_MAX:-2000}"
|
||||
_now=$(fwlive_adaptive_clock_cs)
|
||||
_bucket=$(fwlive_adaptive_bucket_for_ms "$_ms")
|
||||
_limit=$_max
|
||||
_cd=0
|
||||
|
||||
case "$_prev_b" in
|
||||
hot) _cd=$FWLIVE_ADAPTIVE_COOLDOWN_HOT_CS ;;
|
||||
warm) _cd=$FWLIVE_ADAPTIVE_COOLDOWN_WARM_CS ;;
|
||||
esac
|
||||
|
||||
if [ "$_cd" -gt 0 ] && [ "$_prev_c" -gt 0 ] && \
|
||||
[ "$((_now - _prev_c))" -lt "$_cd" ]; then
|
||||
if [ "$_ms" -le "$FWLIVE_ADAPTIVE_HOT_EXIT_MS" ] && \
|
||||
[ "$_bucket" != hot ]; then
|
||||
:
|
||||
else
|
||||
_limit=$_prev_l
|
||||
[ "$_limit" -gt "$_req" ] && _limit=$_req
|
||||
[ "$_limit" -gt "$_max" ] && _limit=$_max
|
||||
[ "$_limit" -lt 1 ] && _limit=1
|
||||
printf '%s\n' "$_limit"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
case "$_bucket" in
|
||||
cold) _limit=$_max ;;
|
||||
cool) _limit=$FWLIVE_ADAPTIVE_COOL_CAP ;;
|
||||
warm)
|
||||
if [ "$_prev_w" = 1 ] && [ "$_prev_b" = warm ]; then
|
||||
_limit=$_prev_l
|
||||
else
|
||||
_base=$_prev_l
|
||||
[ "$_base" -lt 1 ] && _base=$_max
|
||||
_limit=$((_base / 2))
|
||||
[ "$_limit" -lt "$FWLIVE_ADAPTIVE_WARM_MIN" ] && \
|
||||
_limit=$FWLIVE_ADAPTIVE_WARM_MIN
|
||||
fi
|
||||
;;
|
||||
hot) _limit=$FWLIVE_ADAPTIVE_HOT_FLOOR ;;
|
||||
esac
|
||||
|
||||
[ "$_limit" -gt "$_req" ] && _limit=$_req
|
||||
[ "$_limit" -gt "$_max" ] && _limit=$_max
|
||||
[ "$_limit" -lt 1 ] && _limit=1
|
||||
printf '%s\n' "$_limit"
|
||||
}
|
||||
|
||||
# Apply cap using *previous* poll duration. Prints: lines shed_flag bucket
|
||||
# Unlocked read (stale OK) — flock reserved for record().
|
||||
fwlive_adaptive_plan() {
|
||||
_req=$1
|
||||
_max="${POLL_LINES_MAX:-2000}"
|
||||
if ! fwlive_adaptive_enabled; then
|
||||
_l=$_req
|
||||
[ "$_l" -gt "$_max" ] && _l=$_max
|
||||
printf '%s 0 off\n' "$_l"
|
||||
return 0
|
||||
fi
|
||||
# shellcheck disable=SC2046
|
||||
set -- $(fwlive_adaptive_read_state)
|
||||
_d=$1
|
||||
_prev_l=$2
|
||||
_b=$3
|
||||
_prev_w=$4
|
||||
_s=$5
|
||||
_prev_c=$6
|
||||
_l=$(fwlive_adaptive_compute_limit "$_req" "$_d" "$_prev_l" "$_b" "$_prev_w" "$_prev_c")
|
||||
_nb=$(fwlive_adaptive_bucket_for_ms "$_d")
|
||||
_shed=0
|
||||
[ "$_nb" = hot ] && _shed=1
|
||||
[ "$_s" = 1 ] && [ "$_b" = hot ] && _shed=1
|
||||
printf '%s %s %s\n' "$_l" "$_shed" "$_nb"
|
||||
}
|
||||
|
||||
fwlive_adaptive__record_body() {
|
||||
_ms=$1
|
||||
_served=$2
|
||||
# shellcheck disable=SC2046
|
||||
set -- $(fwlive_adaptive_read_state)
|
||||
_prev_l=$2
|
||||
_prev_b=$3
|
||||
_prev_w=$4
|
||||
_prev_c=$6
|
||||
_bucket=$(fwlive_adaptive_bucket_for_ms "$_ms")
|
||||
_now=$(fwlive_adaptive_clock_cs)
|
||||
_limit=$(fwlive_adaptive_compute_limit "$_served" "$_ms" "$_prev_l" "$_prev_b" "$_prev_w" "$_prev_c")
|
||||
_warm=0
|
||||
_shed=0
|
||||
case "$_bucket" in
|
||||
warm) _warm=1 ;;
|
||||
hot) _shed=1 ;;
|
||||
esac
|
||||
fwlive_adaptive_write_state "$_ms" "$_limit" "$_bucket" "$_warm" "$_shed" "$_now"
|
||||
}
|
||||
|
||||
fwlive_adaptive_record() {
|
||||
_ms=$1
|
||||
_served=$2
|
||||
fwlive_adaptive_enabled || return 0
|
||||
fwlive_adaptive_with_lock fwlive_adaptive__record_body "$_ms" "$_served"
|
||||
}
|
||||
|
||||
fwlive_adaptive_is_hot() {
|
||||
fwlive_adaptive_enabled || return 1
|
||||
# shellcheck disable=SC2046
|
||||
set -- $(fwlive_adaptive_read_state)
|
||||
[ "$5" = 1 ] && return 0
|
||||
[ "$3" = hot ] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
# Merge adaptive siblings into a JSON object ending with }.
|
||||
# Args: json_body shed_flag limit truncated messages_received
|
||||
fwlive_adaptive_merge_reply() {
|
||||
_body=$1
|
||||
_shed=$2
|
||||
_limit=$3
|
||||
_trunc=$4
|
||||
_msgs=$5
|
||||
_adapt=1
|
||||
fwlive_adaptive_enabled || _adapt=0
|
||||
case "$_body" in
|
||||
*\}) ;;
|
||||
*) printf '%s' "$_body"; return 0 ;;
|
||||
esac
|
||||
_base=${_body%\}}
|
||||
if [ "$_adapt" = 0 ]; then
|
||||
printf '%s,"adaptive":0,"messages_received":%s}' "$_base" "$_msgs"
|
||||
return 0
|
||||
fi
|
||||
if [ "$_shed" = 1 ]; then
|
||||
printf '%s,"adaptive":1,"messages_received":%s,"truncated":%s,"shed":{"level":"hot","limit":%s}}' \
|
||||
"$_base" "$_msgs" "$_trunc" "$_limit"
|
||||
else
|
||||
printf '%s,"adaptive":1,"messages_received":%s,"truncated":%s}' \
|
||||
"$_base" "$_msgs" "$_trunc"
|
||||
fi
|
||||
}
|
||||
|
||||
# Count log[*] via "msg" keys — helper for tests / future filter-side count.
|
||||
# NOT called on the poll hot path (Grok #329 P1): ash-scanning a 2000-entry
|
||||
# reply after end_cs is unmeasured overhead that cannot shed itself.
|
||||
fwlive_adaptive_count_log() {
|
||||
_json=$1
|
||||
_n=0
|
||||
_rest=$_json
|
||||
while :; do
|
||||
case "$_rest" in
|
||||
*'"msg"'*)
|
||||
_n=$((_n + 1))
|
||||
_rest=${_rest#*\"msg\"}
|
||||
;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
printf '%s\n' "$_n"
|
||||
}
|
||||
@@ -18,6 +18,7 @@ FW4_TAG='!fw4: '
|
||||
LIBEXEC_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
FILTER_SH="$LIBEXEC_DIR/fwlive-log-filter.sh"
|
||||
LOGGING_SH="$LIBEXEC_DIR/fwlive-logging.sh"
|
||||
ADAPTIVE_SH="$LIBEXEC_DIR/fwlive-adaptive-cap.sh"
|
||||
# Max reverse-DNS lookups per resolve call (~32 names keeps nslookup load bounded on small routers).
|
||||
RESOLVE_MAX=32
|
||||
# Per-lookup nslookup timeout (integer seconds — BusyBox `timeout` truncates
|
||||
@@ -44,6 +45,8 @@ RULESMAP_IPTABLES_FILE='/tmp/rulesmap'
|
||||
# json_escape lives in fwlive-logging.sh so prerm can source that file alone (#222).
|
||||
# shellcheck disable=SC1090,SC1091 # LOGGING_SH resolves to sibling ../fwlive-logging.sh
|
||||
. "$LOGGING_SH"
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "$ADAPTIVE_SH"
|
||||
|
||||
slug_key() {
|
||||
echo "$1" | tr '[:upper:]' '[:lower:]' | tr ' _' '--'
|
||||
@@ -425,12 +428,19 @@ poll_lines_from_input() {
|
||||
|
||||
fetch_firewall_logs() {
|
||||
input="$1"
|
||||
lines=$(poll_lines_from_input "$input")
|
||||
# lines is numeric-clamped; stream/oneshot are fixed (not caller-controlled).
|
||||
# A dead logd must not look like an empty firewall table (#303): return a
|
||||
# structured error instead of filtering an empty fallback into {"log":[]}.
|
||||
if ! raw=$(ubus call log read "{\"lines\":$lines,\"stream\":false,\"oneshot\":true}" 2>/dev/null); then
|
||||
printf '%s' '{"log":[],"error":"log_read_failed"}'
|
||||
requested=$(poll_lines_from_input "$input")
|
||||
# shellcheck disable=SC2046
|
||||
set -- $(fwlive_adaptive_plan "$requested")
|
||||
lines=$1
|
||||
shed_flag=${2:-0}
|
||||
# Adaptive plan prints: lines shed bucket
|
||||
start_cs=$(fwlive_adaptive_clock_cs)
|
||||
# Drop dead oneshot flag — only consumed on logd's stream branch (#306).
|
||||
if ! raw=$(ubus call log read "{\"lines\":$lines,\"stream\":false}" 2>/dev/null); then
|
||||
# Do not record ~0 ms as cold: failure is not healthy fast processing
|
||||
# and must not clear an existing hot/shed cap (#329 Hermes Q1).
|
||||
out='{"log":[],"error":"log_read_failed"}'
|
||||
fwlive_adaptive_merge_reply "$out" "$shed_flag" "$lines" 0 0
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -444,7 +454,17 @@ fetch_firewall_logs() {
|
||||
fi
|
||||
fi
|
||||
[ -n "$out" ] || out='{"log":[],"error":"filter_empty"}'
|
||||
printf '%s' "$out"
|
||||
end_cs=$(fwlive_adaptive_clock_cs)
|
||||
# Uptime wrap / skew: end < start — do not record as cold (Grok #329 P2).
|
||||
if [ "$end_cs" -ge "$start_cs" ]; then
|
||||
_dur=$(((end_cs - start_cs) * 10))
|
||||
fwlive_adaptive_record "$_dur" "$lines"
|
||||
fi
|
||||
# Layer 1: messages_received stays 0 — no post-filter ash count (Grok #329 P1).
|
||||
_trunc=0
|
||||
[ "$shed_flag" = 1 ] && _trunc=1
|
||||
[ "$lines" -lt "$requested" ] && _trunc=1
|
||||
fwlive_adaptive_merge_reply "$out" "$shed_flag" "$lines" "$_trunc" 0
|
||||
}
|
||||
|
||||
poll_logs() {
|
||||
@@ -584,6 +604,12 @@ resolve_addresses() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Load shed: previous poll was hot (#306 Layer 1).
|
||||
if fwlive_adaptive_is_hot; then
|
||||
printf '{"names":{},"disabled":"load"}'
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Wall-clock budget clock starts at the TRUE function entry (luna
|
||||
# folds 2026-08-10): the budget must bound the whole call — including
|
||||
# read_rpc_input (stalled stdin must not escape the budget) and json
|
||||
|
||||
@@ -7,10 +7,10 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-gecoosac
|
||||
PKG_VERSION:=2.2
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
LUCI_TITLE:=LuCI Support for gecoosac
|
||||
LUCI_DEPENDS:=+luci-base +gecoosac +gecoosac-files
|
||||
LUCI_DEPENDS:=+luci-base +gecoosac +gecoosac-files +gecoosac-common
|
||||
LUCI_EXTRA_DEPENDS:=gecoosac (>=2.2.20251015)
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
|
||||
@@ -403,6 +403,7 @@ return view.extend({
|
||||
o.default = '8080';
|
||||
o.datatype = 'port';
|
||||
o.depends('isonlyoneprot', '0');
|
||||
o.retain = true;
|
||||
|
||||
portOption.validate = function(section_id, value) {
|
||||
return validatePortValue(section_id, value, managementPortOption, singlePortOption, true);
|
||||
@@ -422,10 +423,11 @@ return view.extend({
|
||||
return true;
|
||||
};
|
||||
httpsOption = s.option(form.Flag, 'https', _('Enable HTTPS service'),
|
||||
_('Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key.'));
|
||||
_('When HTTPS starts, default certificates are generated or renewed if fewer than 30 days remain. Custom certificates must be currently valid and match their private keys.'));
|
||||
o = httpsOption;
|
||||
o.default = '0';
|
||||
o.depends('isonlyoneprot', '0');
|
||||
o.retain = true;
|
||||
|
||||
certificateOption = s.option(form.Value, 'crt_file', _('Specify crt certificate file'));
|
||||
o = certificateOption;
|
||||
@@ -433,6 +435,7 @@ return view.extend({
|
||||
o.default = DEFAULT_CRT_FILE;
|
||||
o.datatype = 'file';
|
||||
o.depends({ isonlyoneprot: '0', https: '1' });
|
||||
o.retain = true;
|
||||
o.validate = function(section_id, value) {
|
||||
return validateCertificatePath(section_id, value, singlePortOption, httpsOption);
|
||||
};
|
||||
@@ -443,6 +446,7 @@ return view.extend({
|
||||
o.default = DEFAULT_KEY_FILE;
|
||||
o.datatype = 'file';
|
||||
o.depends({ isonlyoneprot: '0', https: '1' });
|
||||
o.retain = true;
|
||||
o.validate = function(section_id, value) {
|
||||
return validateCertificatePath(section_id, value, singlePortOption, httpsOption);
|
||||
};
|
||||
@@ -481,7 +485,7 @@ return view.extend({
|
||||
};
|
||||
|
||||
o = s.option(form.Value, 'db_dir', _('Database dir path'),
|
||||
_('Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Do not place it inside the upload directory.'));
|
||||
_('Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Only /etc/gecoosac persists across reboots on standard OpenWrt. Do not place it inside the upload directory.'));
|
||||
o.placeholder = DEFAULT_DB_DIR;
|
||||
o.default = DEFAULT_DB_DIR;
|
||||
o.datatype = 'directory';
|
||||
|
||||
@@ -9,8 +9,8 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key."
|
||||
msgstr "默认证书文件会在 HTTPS 启动时生成;自定义路径必须指向可读取且相互匹配的证书和私钥。"
|
||||
msgid "When HTTPS starts, default certificates are generated or renewed if fewer than 30 days remain. Custom certificates must be currently valid and match their private keys."
|
||||
msgstr "HTTPS 启动时会生成默认证书,或在剩余有效期不足 30 天时更新。自定义证书必须在有效期内且与私钥匹配。"
|
||||
|
||||
msgid "Gecoos AC"
|
||||
msgstr "集客AC控制器"
|
||||
@@ -68,8 +68,8 @@ msgstr "上传目录必须是 /tmp/gecoosac/upload 或 /mnt/storage-name/gecoosa
|
||||
msgid "Database dir path"
|
||||
msgstr "数据库目录"
|
||||
|
||||
msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Do not place it inside the upload directory."
|
||||
msgstr "将配置数据库存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。不要放在上传目录内。"
|
||||
msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Only /etc/gecoosac persists across reboots on standard OpenWrt. Do not place it inside the upload directory."
|
||||
msgstr "将配置数据库存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。标准 OpenWrt 上只有 /etc/gecoosac 的数据会在重启后保留。不要放在上传目录内。"
|
||||
|
||||
msgid "Database directory must be under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac."
|
||||
msgstr "数据库目录必须位于 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。"
|
||||
|
||||
@@ -9,8 +9,8 @@ msgstr ""
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
|
||||
msgid "Default certificate files are generated when HTTPS starts; custom paths must point to a readable certificate and matching key."
|
||||
msgstr "預設憑證檔案會在 HTTPS 啟動時產生;自訂路徑必須指向可讀取且相互匹配的憑證與私鑰。"
|
||||
msgid "When HTTPS starts, default certificates are generated or renewed if fewer than 30 days remain. Custom certificates must be currently valid and match their private keys."
|
||||
msgstr "HTTPS 啟動時會產生預設憑證,或在剩餘有效期不足 30 天時更新。自訂憑證必須在有效期內且與私鑰相符。"
|
||||
|
||||
msgid "Gecoos AC"
|
||||
msgstr "集客 AC 控制器"
|
||||
@@ -68,8 +68,8 @@ msgstr "上傳目錄必須是 /tmp/gecoosac/upload 或 /mnt/storage-name/gecoosa
|
||||
msgid "Database dir path"
|
||||
msgstr "資料庫目錄"
|
||||
|
||||
msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Do not place it inside the upload directory."
|
||||
msgstr "將設定資料庫存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。不要放在上傳目錄內。"
|
||||
msgid "Store the config database under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac. Only /etc/gecoosac persists across reboots on standard OpenWrt. Do not place it inside the upload directory."
|
||||
msgstr "將設定資料庫存放在 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。標準 OpenWrt 上只有 /etc/gecoosac 的資料會在重新啟動後保留。不要放在上傳目錄內。"
|
||||
|
||||
msgid "Database directory must be under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac."
|
||||
msgstr "資料庫目錄必須位於 /etc/gecoosac、/tmp/gecoosac 或 /var/lib/gecoosac 下。"
|
||||
|
||||
@@ -2,12 +2,7 @@
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
. /lib/functions.sh
|
||||
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
. /usr/share/gecoosac/common.sh
|
||||
|
||||
json_result() {
|
||||
local ok="$1"
|
||||
@@ -22,116 +17,6 @@ json_result() {
|
||||
json_cleanup
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
case "$path" in
|
||||
/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
load_clear_config() {
|
||||
local section_type
|
||||
|
||||
@@ -151,135 +36,17 @@ load_clear_config() {
|
||||
config_get clear_key_file config key_file "$DEFAULT_KEY_FILE"
|
||||
}
|
||||
|
||||
safe_upload_path() {
|
||||
local path
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
is_supported_upload_path "$path"
|
||||
}
|
||||
|
||||
path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
path_has_mount() {
|
||||
local root line mount_path
|
||||
|
||||
root="$(normalize_path "$1")" || return 2
|
||||
[ -r /proc/self/mountinfo ] || return 2
|
||||
|
||||
while IFS= read -r line; do
|
||||
mount_path="$(printf '%s\n' "$line" | cut -d ' ' -f 5)" || return 2
|
||||
[ -n "$mount_path" ] || return 2
|
||||
mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2
|
||||
mount_path="$(normalize_path "$mount_path")" || return 2
|
||||
path_in_dir "$mount_path" "$root" && return 0
|
||||
done < /proc/self/mountinfo
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_secure_dir() {
|
||||
local allow_sticky="$2" owner permissions metadata
|
||||
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
||||
metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1
|
||||
set -- $metadata
|
||||
permissions="$1"
|
||||
owner="$3"
|
||||
[ "$owner" = "0" ] || return 1
|
||||
case "$permissions" in
|
||||
d?????????) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \
|
||||
[ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then
|
||||
[ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_secure_upload_dir() {
|
||||
local path current part rest
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
[ -d "$path" ] && [ ! -L "$path" ] || return 1
|
||||
current="/"
|
||||
rest="${path#/}"
|
||||
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
current="${current%/}/$part"
|
||||
case "$current" in
|
||||
/tmp) is_secure_dir "$current" 1 || return 1 ;;
|
||||
*) is_secure_dir "$current" || return 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
configured_path_in_upload() {
|
||||
local option="$1" checked_root="$2"
|
||||
local live_logical="${3:-$2}" live_physical="${4:-$2}"
|
||||
local role path physical real_path suffix mapped
|
||||
local path role
|
||||
|
||||
case "$option" in
|
||||
case "$1" in
|
||||
db_dir) path="$clear_db_dir"; role=db ;;
|
||||
piddir) path="$clear_piddir"; role=pid ;;
|
||||
crt_file) path="$clear_crt_file"; role="file" ;;
|
||||
key_file) path="$clear_key_file"; role="file" ;;
|
||||
*) return 2 ;;
|
||||
esac
|
||||
[ -n "$path" ] || return 1
|
||||
|
||||
path="$(normalize_path "$path")" || return 2
|
||||
physical="$(managed_dir_path "$role" "$path")" || return 2
|
||||
checked_root="$(normalize_path "$checked_root")" || return 2
|
||||
live_logical="$(normalize_path "$live_logical")" || return 2
|
||||
live_physical="$(normalize_path "$live_physical")" || return 2
|
||||
|
||||
path_in_dir "$path" "$checked_root" && return 0
|
||||
path_in_dir "$physical" "$checked_root" && return 0
|
||||
real_path=
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
real_path="$(readlink -f "$path" 2>/dev/null)" || return 2
|
||||
[ -n "$real_path" ] || return 2
|
||||
real_path="$(normalize_path "$real_path")" || return 2
|
||||
path_in_dir "$real_path" "$checked_root" && return 0
|
||||
fi
|
||||
|
||||
[ "$checked_root" != "$live_physical" ] || {
|
||||
path_in_dir "$path" "$live_logical" && return 0
|
||||
path_in_dir "$physical" "$live_physical" && return 0
|
||||
if [ -n "$real_path" ] && path_in_dir "$real_path" "$live_physical"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
suffix=
|
||||
if path_in_dir "$path" "$live_logical"; then
|
||||
suffix="${path#"$live_logical"}"
|
||||
elif path_in_dir "$physical" "$live_physical"; then
|
||||
suffix="${physical#"$live_physical"}"
|
||||
elif [ -n "$real_path" ] && path_in_dir "$real_path" "$live_physical"; then
|
||||
suffix="${real_path#"$live_physical"}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
mapped="${checked_root%/}${suffix}"
|
||||
[ -e "$mapped" ] || [ -L "$mapped" ]
|
||||
protected_path_in_dir "$path" "$role" "$2" "${3:-$2}" "${4:-$2}"
|
||||
}
|
||||
|
||||
status_result() {
|
||||
@@ -534,7 +301,7 @@ clear_upload() {
|
||||
esac
|
||||
|
||||
logical="$(normalize_path "$logical")" || { json_result 0 "Expecting an absolute path"; return; }
|
||||
if ! safe_upload_path "$logical"; then
|
||||
if ! is_supported_upload_path "$logical"; then
|
||||
json_result 0 "Only Gecoos upload directories can be cleared" "$logical"
|
||||
return
|
||||
fi
|
||||
@@ -542,7 +309,7 @@ clear_upload() {
|
||||
json_result 0 "Unable to resolve upload directory" "$logical"
|
||||
return
|
||||
}
|
||||
if ! safe_upload_path "$physical"; then
|
||||
if ! is_supported_upload_path "$physical"; then
|
||||
json_result 0 "Only Gecoos upload directories can be cleared" "$logical"
|
||||
return
|
||||
fi
|
||||
|
||||
@@ -6,11 +6,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-openlist2
|
||||
PKG_VERSION:=1.0.3
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=1.0.4
|
||||
PKG_RELEASE:=6
|
||||
|
||||
LUCI_TITLE:=LuCI support for openlist
|
||||
LUCI_DEPENDS:=+openlist2
|
||||
# The virtual provide forces installation of a core package with the helper.
|
||||
LUCI_DEPENDS:=+openlist2 +openlist2-helper
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
const HELPER = '/usr/libexec/openlist2-helper';
|
||||
|
||||
const callServiceList = rpc.declare({
|
||||
object: 'service',
|
||||
method: 'list',
|
||||
@@ -25,25 +28,25 @@ const getServiceStatus = () => {
|
||||
};
|
||||
|
||||
const getWebInterfaceUrl = (protocol, webport, site_url) => {
|
||||
const fallbackUrl = `${protocol}//${window.location.hostname}:${webport}/`;
|
||||
const configuredUrl = (site_url || '').trim();
|
||||
if (configuredUrl) {
|
||||
try {
|
||||
const url = new URL(configuredUrl, fallbackUrl);
|
||||
const url = new URL(configuredUrl);
|
||||
if (url.protocol === 'http:' || url.protocol === 'https:')
|
||||
return url.href;
|
||||
} catch (e) { }
|
||||
}
|
||||
|
||||
return fallbackUrl;
|
||||
return webport ? protocol + '//' + window.location.hostname + ':' + webport + '/' : null;
|
||||
};
|
||||
|
||||
const renderStatus = (isRunning, protocol, webport, site_url) => {
|
||||
const status = E('em', {}, E('span', { 'style': `color:${isRunning ? 'green' : 'red'}` },
|
||||
E('strong', {}, ['OpenList ', isRunning ? _('RUNNING') : _('NOT RUNNING')])));
|
||||
|
||||
if (isRunning && webport) {
|
||||
if (isRunning && (webport || site_url)) {
|
||||
const buttonUrl = getWebInterfaceUrl(protocol, webport, site_url);
|
||||
if (!buttonUrl)
|
||||
return status;
|
||||
const button = E('input', {
|
||||
'class': 'cbi-button-reload',
|
||||
'type': 'button',
|
||||
@@ -58,32 +61,24 @@ const renderStatus = (isRunning, protocol, webport, site_url) => {
|
||||
return status;
|
||||
};
|
||||
|
||||
const isSafeLogPath = value => /^\/var\/log\/openlist2(?:[._-][A-Za-z0-9._-]+)?$/.test(value || '');
|
||||
const isSafeLogPath = value => !/[\r\n]/.test(value || '') &&
|
||||
/^\/var\/log\/openlist2(?:[._-][A-Za-z0-9._-]+)?$/.test(value || '');
|
||||
|
||||
const isEnabledPort = value => {
|
||||
const port = (value || '').trim();
|
||||
return port !== '' && port !== '-1';
|
||||
};
|
||||
|
||||
const HTTPS_ENABLED_PORT = /^[1-9][0-9]*$/;
|
||||
const isEnabledPort = value => /^\d+$/.test(String(value || '').trim()) && Number(value) >= 1 && Number(value) <= 65535;
|
||||
|
||||
const getListenPortValue = (section_id, option, fallback) => {
|
||||
const value = uci.get('openlist2', section_id, option);
|
||||
return value == null || String(value).trim() === '' ? fallback : value;
|
||||
};
|
||||
|
||||
const getHttpsPortValue = (section_id, httpsPortOption) => {
|
||||
const value = httpsPortOption.formvalue(section_id);
|
||||
return value == null || String(value).trim() === ''
|
||||
? getListenPortValue(section_id, 'listen_https_port', '-1')
|
||||
: value;
|
||||
};
|
||||
|
||||
const validateHttpsRequiredFile = (section_id, httpsPortOption, value, message) => {
|
||||
if (!HTTPS_ENABLED_PORT.test(String(getHttpsPortValue(section_id, httpsPortOption)).trim()))
|
||||
return true;
|
||||
|
||||
return (value || '').trim() ? true : message;
|
||||
if (value != null && String(value).trim() !== '')
|
||||
return value;
|
||||
const legacyPort = uci.get('openlist2', section_id, 'port');
|
||||
const legacySsl = uci.get('openlist2', section_id, 'ssl');
|
||||
if (legacyPort != null || legacySsl != null) {
|
||||
const tls = ['1', 'true', 'on', 'yes', 'enabled'].includes(legacySsl);
|
||||
return option === 'listen_https_port'
|
||||
? (tls ? legacyPort || '5244' : '-1')
|
||||
: (tls ? '-1' : legacyPort || '5244');
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
const validateListenPort = value => {
|
||||
@@ -136,49 +131,143 @@ const validateCorsOrigins = value => {
|
||||
};
|
||||
|
||||
const getEffectiveWebEndpoint = config => {
|
||||
const httpPort = uci.get(config, '@openlist2[0]', 'listen_http_port') || '5244';
|
||||
const httpsPort = uci.get(config, '@openlist2[0]', 'listen_https_port') || '-1';
|
||||
const httpPort = getListenPortValue('@openlist2[0]', 'listen_http_port', '5244');
|
||||
const httpsPort = getListenPortValue('@openlist2[0]', 'listen_https_port', '-1');
|
||||
|
||||
if (isEnabledPort(httpsPort))
|
||||
return { protocol: 'https:', port: httpsPort };
|
||||
return { protocol: 'https:', port: String(Number(httpsPort)) };
|
||||
|
||||
if (isEnabledPort(httpPort))
|
||||
return { protocol: 'http:', port: httpPort };
|
||||
return { protocol: 'http:', port: String(Number(httpPort)) };
|
||||
|
||||
return { protocol: 'http:', port: '' };
|
||||
};
|
||||
|
||||
|
||||
const normalizePath = value => String(value || '').replace(/\/+/g, '/').replace(/\/$/, '') || '/';
|
||||
const containsPath = (parent, child) => child === parent || child.startsWith(parent + '/');
|
||||
const broadDirectories = new Set(['/', '/bin', '/boot', '/dev', '/etc', '/home', '/lib', '/lib64', '/media', '/mnt', '/opt', '/overlay', '/proc', '/root', '/rom', '/run', '/sbin', '/srv', '/sys', '/tmp', '/usr', '/var', '/www', '/tmp/log', '/tmp/run']);
|
||||
const isBroadDirectory = path => broadDirectories.has(path) ||
|
||||
/^\/(?:usr|bin|sbin|lib|lib64|dev|proc|sys|rom|overlay|www)\//.test(path) ||
|
||||
/^\/(?:etc\/(?:config|dropbear|ssl)|tmp\/log)(?:\/|$)/.test(path);
|
||||
|
||||
const validateAbsolutePath = (value, required) => {
|
||||
const path = String(value || '');
|
||||
if (!path && !required)
|
||||
return true;
|
||||
if (!path.startsWith('/') || /[\x00-\x1f\x7f]/.test(path) || /\/\.{1,2}(?:\/|$)/.test(path))
|
||||
return _('Use an absolute path without control characters or dot segments.');
|
||||
return true;
|
||||
};
|
||||
|
||||
const passiveRanges = value => (Array.isArray(value) ? value : [value || ''])
|
||||
.flatMap(item => String(item).split(/[,\s]+/)).filter(Boolean);
|
||||
|
||||
const validatePassivePorts = value => {
|
||||
for (const range of passiveRanges(value)) {
|
||||
if (!/^\d+(?:-\d+)?$/.test(range))
|
||||
return _('Use passive ports or ranges from 1024 to 65535.');
|
||||
const [first, last = first] = range.split('-').map(Number);
|
||||
if (first < 1024 || last > 65535 || first > last)
|
||||
return _('Use passive ports or ranges from 1024 to 65535.');
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const backendUrl = route => {
|
||||
const endpoint = getEffectiveWebEndpoint('openlist2');
|
||||
const base = getWebInterfaceUrl(endpoint.protocol, endpoint.port,
|
||||
uci.get('openlist2', '@openlist2[0]', 'site_url'));
|
||||
if (!base)
|
||||
return null;
|
||||
const url = new URL(base);
|
||||
url.pathname = url.pathname.replace(/\/$/, '') + '/' + route.replace(/^\//, '');
|
||||
url.search = '';
|
||||
url.hash = '';
|
||||
return url.href;
|
||||
};
|
||||
|
||||
const pollServiceStatus = () => getServiceStatus().then(running => {
|
||||
const node = document.getElementById('service_status');
|
||||
const endpoint = L.hasViewPermission() ? getEffectiveWebEndpoint('openlist2') : { protocol: 'http:', port: '' };
|
||||
const site = L.hasViewPermission() ? uci.get('openlist2', '@openlist2[0]', 'site_url') : '';
|
||||
if (node)
|
||||
dom.content(node, renderStatus(running, endpoint.protocol, endpoint.port, site));
|
||||
});
|
||||
|
||||
const renderServiceSection = () => {
|
||||
poll.add(pollServiceStatus);
|
||||
return E('div', { 'class': 'cbi-section', 'id': 'status_bar' },
|
||||
E('p', { 'id': 'service_status' }, _('Collecting data...')));
|
||||
};
|
||||
|
||||
const copyPassword = async value => {
|
||||
if (typeof navigator !== 'undefined' && navigator.clipboard) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(value);
|
||||
return true;
|
||||
} catch (e) { }
|
||||
}
|
||||
const field = E('textarea', { 'style': 'position:fixed;left:-10000px' }, value);
|
||||
document.body.appendChild(field);
|
||||
try {
|
||||
field.select();
|
||||
return document.execCommand('copy') === true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
} finally {
|
||||
document.body.removeChild(field);
|
||||
}
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return Promise.all([
|
||||
uci.load('openlist2')
|
||||
]);
|
||||
// UCI contains credentials; readers may view status and bounded logs only.
|
||||
return L.hasViewPermission() ? uci.load('openlist2') : Promise.resolve();
|
||||
},
|
||||
|
||||
async handleResetPassword(data) {
|
||||
const data_dir = uci.get(data[0], '@openlist2[0]', 'data_dir') || '/etc/openlist2';
|
||||
async handleResetPassword() {
|
||||
if (!L.hasViewPermission())
|
||||
return;
|
||||
try {
|
||||
const newpassword = await fs.exec('/usr/bin/openlist2', ['admin', 'random', '--data', data_dir]);
|
||||
const new_password = newpassword.stdout.match(/password:\s*(\S+)/)[1];
|
||||
const result = await fs.exec(HELPER, ['password-reset']);
|
||||
const output = (result.stdout || '').replace(/\u001b\[[0-9;]*m/g, '');
|
||||
const username = output.match(/^username:\s*(.+)$/m);
|
||||
const password = output.match(/^password:\s*(\S+)\s*$/m);
|
||||
if (result.code !== 0 || !username || !password)
|
||||
throw new Error((result.stderr || _('The password reset did not return a username and password.')).trim());
|
||||
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = new_password;
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textArea);
|
||||
alert(`${_('Username:')}admin\n${_('New Password:')}${new_password}\n\n${_('New password has been copied to clipboard.')}`);
|
||||
const newPassword = password[1];
|
||||
const message = E('p', { 'role': 'status' }, _('Copy the new password before closing this dialog.'));
|
||||
const copy = async () => {
|
||||
message.textContent = await copyPassword(newPassword)
|
||||
? _('New password has been copied to clipboard.')
|
||||
: _('Automatic copy failed. Select and copy the password manually.');
|
||||
};
|
||||
ui.showModal(_('Password reset'), [
|
||||
E('p', {}, _('Username:') + ' ' + username[1].trim()),
|
||||
E('label', {}, _('New Password:')),
|
||||
E('input', { 'type': 'text', 'readonly': 'readonly', 'value': newPassword, 'style': 'width:100%' }),
|
||||
message,
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn cbi-button-action', 'click': ui.createHandlerFn(this, copy) }, _('Copy password')),
|
||||
' ',
|
||||
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Close'))
|
||||
])
|
||||
]);
|
||||
await copy();
|
||||
} catch (error) {
|
||||
console.error('Failed to reset password: ', error);
|
||||
ui.addNotification(null, E('p', _('Unable to reset the password: %s').format(error.message)));
|
||||
}
|
||||
},
|
||||
|
||||
render(data) {
|
||||
let m, s, o, httpsPortOption;
|
||||
const endpoint = getEffectiveWebEndpoint(data[0]);
|
||||
const webport = endpoint.port;
|
||||
const protocol = endpoint.protocol;
|
||||
const site_url = uci.get(data[0], '@openlist2[0]', 'site_url') || '';
|
||||
render() {
|
||||
if (!L.hasViewPermission())
|
||||
return E('div', {}, [
|
||||
renderServiceSection(),
|
||||
E('p', {}, _('Configuration contains credentials and is available only to users with write access.'))
|
||||
]);
|
||||
let m, s, o;
|
||||
|
||||
m = new form.Map('openlist2', _('OpenList'),
|
||||
_('A file list program that supports multiple storage.') + '<br />' +
|
||||
@@ -188,21 +277,28 @@ return view.extend({
|
||||
s.anonymous = true;
|
||||
s.addremove = false;
|
||||
|
||||
s.render = () => {
|
||||
poll.add(() => {
|
||||
return L.resolveDefault(getServiceStatus()).then(res => {
|
||||
const view = document.getElementById('service_status');
|
||||
if (view)
|
||||
dom.content(view, renderStatus(res, protocol, webport, site_url));
|
||||
});
|
||||
});
|
||||
|
||||
return E('div', { class: 'cbi-section', id: 'status_bar' }, [
|
||||
E('p', { id: 'service_status' }, _('Collecting data...'))
|
||||
]);
|
||||
};
|
||||
s.render = renderServiceSection;
|
||||
|
||||
s = m.section(form.NamedSection, '@openlist2[0]', 'openlist2');
|
||||
const fields = {};
|
||||
const addOption = (tab, type, name, ...args) => {
|
||||
const option = s.taboption(tab, type, name, ...args);
|
||||
fields[name] = option;
|
||||
return option;
|
||||
};
|
||||
const fieldValue = (section_id, name, fallback = '') => {
|
||||
const value = fields[name] && fields[name].formvalue(section_id);
|
||||
return value == null ? uci.get('openlist2', section_id, name) ?? fields[name]?.default ?? fallback : value;
|
||||
};
|
||||
const addBackendLink = (tab, name, route, label, description) => {
|
||||
const option = addOption(tab, form.DummyValue, name, label, description);
|
||||
option.renderWidget = () => {
|
||||
const url = backendUrl(route);
|
||||
return url ? E('a', { 'href': url, 'target': '_blank', 'rel': 'noopener noreferrer' }, label)
|
||||
: E('span', {}, _('Configure a web port or Site URL to open the management page.'));
|
||||
};
|
||||
return option;
|
||||
};
|
||||
|
||||
s.tab('basic', _('Basic Settings'));
|
||||
s.tab('global', _('Global Settings'));
|
||||
@@ -210,7 +306,8 @@ return view.extend({
|
||||
s.tab('database', _('Database'));
|
||||
s.tab('search', _('Search'));
|
||||
s.tab('scheme', _('Web Protocol'));
|
||||
s.tab('tasks', _('Task threads'));
|
||||
s.tab('tasks', _('Task threads'),
|
||||
_('Worker counts below only initialize missing settings. After the first start, change worker counts and speed limits in OpenList Management > Settings > Traffic. Upload and decompression-upload tasks do not support persistence.'));
|
||||
s.tab('cors', _('CORS Settings'));
|
||||
s.tab('s3', _('Object Storage'));
|
||||
s.tab('ftp', _('FTP'));
|
||||
@@ -218,103 +315,108 @@ return view.extend({
|
||||
s.tab('mcp', _('MCP'));
|
||||
|
||||
// init
|
||||
o = s.taboption('basic', form.Flag, 'enabled', _('Enabled'));
|
||||
o = addOption('basic', form.Flag, 'enabled', _('Enabled'));
|
||||
o.default = o.disabled;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Flag, 'debug', _('Debug logging'),
|
||||
o = addOption('basic', form.Flag, 'debug', _('Debug logging'),
|
||||
_('Pass --debug to OpenList and send stderr to procd logs.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Value, 'delayed_start', _('Delayed Start (seconds)'));
|
||||
o = addOption('basic', form.Value, 'delayed_start', _('Delayed Start (seconds)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Flag, 'allow_wan', _('Open firewall port'));
|
||||
o = addOption('basic', form.Flag, 'allow_wan', _('Open firewall port'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Value, 'data_dir', _('Data directory'));
|
||||
o = addOption('basic', form.Value, 'data_dir', _('Data directory'),
|
||||
_('Changing this path does not move data. Stop OpenList, back up and move the complete data directory, then apply the new path.'));
|
||||
o.default = '/etc/openlist2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Value, 'temp_dir', _('Cache directory'));
|
||||
o = addOption('basic', form.Value, 'temp_dir', _('Cache directory'),
|
||||
_('OpenList clears this directory on startup. Use a dedicated empty subdirectory. Never select a disk mount root or a directory containing files you want to keep.'));
|
||||
o.default = '/tmp/openlist2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('basic', form.Button, '_newpassword', _('Reset Password'),
|
||||
_('Generate a new random password.'));
|
||||
o = addOption('basic', form.Button, '_newpassword', _('Reset Password'),
|
||||
_('Generate a new random password using the saved and applied data directory. Start OpenList once before using this action.'));
|
||||
o.inputtitle = _('Reset Password');
|
||||
o.inputstyle = 'apply';
|
||||
o.onclick = L.bind(this.handleResetPassword, this, data);
|
||||
o.onclick = ui.createHandlerFn(this, 'handleResetPassword');
|
||||
|
||||
// global
|
||||
o = s.taboption('global', form.Flag, 'force', _('Force read config'),
|
||||
o = addOption('global', form.Flag, 'force', _('Force read config'),
|
||||
_('Setting this to true will force the program to read the configuration file, ignoring environment variables.'));
|
||||
o.default = true;
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'site_url', _('Site URL'),
|
||||
o = addOption('global', form.Value, 'site_url', _('Site URL'),
|
||||
_('When the web is reverse proxied to a subdirectory, this option must be filled out to ensure proper functioning of the web. Do not include \'/\' at the end of the URL'));
|
||||
o.validate = function(section_id, value) {
|
||||
return validateSiteUrl(value);
|
||||
};
|
||||
|
||||
o = s.taboption('global', form.Value, 'cdn', _('CDN URL'));
|
||||
o = addOption('global', form.Value, 'cdn', _('CDN URL'));
|
||||
o.default = '';
|
||||
|
||||
o = s.taboption('global', form.Value, 'jwt_secret', _('JWT Key'));
|
||||
o = addOption('global', form.Value, 'jwt_secret', _('JWT Key'));
|
||||
o.password = true;
|
||||
o.default = '';
|
||||
|
||||
o = s.taboption('global', form.Value, 'token_expires_in', _('Login Validity Period (hours)'));
|
||||
o = addOption('global', form.Value, 'token_expires_in', _('Login Validity Period (hours)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '48';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'proxy_address', _('Proxy address'),
|
||||
o = addOption('global', form.Value, 'proxy_address', _('Proxy address'),
|
||||
_('HTTP, HTTPS, SOCKS4, SOCKS5 or SOCKS5HOSTNAME proxy used by OpenList outbound requests.'));
|
||||
o.password = true;
|
||||
o.default = '';
|
||||
|
||||
o = s.taboption('global', form.Value, 'auto_memory_limit', _('Auto memory limit (MB)'),
|
||||
o = addOption('global', form.Value, 'auto_memory_limit', _('Auto memory limit (MB)'),
|
||||
_('0 disables the automatic memory limit. The upstream default is 4 MB.'));
|
||||
o.default = '4';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'min_free_memory', _('Minimum free memory (MB)'),
|
||||
o = addOption('global', form.Value, 'min_free_memory', _('Minimum free memory (MB)'),
|
||||
_('Values below 16 let OpenList calculate a default; negative values disable memory cache.'));
|
||||
o.default = '0';
|
||||
o.datatype = 'integer';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'max_block_limit', _('Maximum block size (MB)'),
|
||||
o = addOption('global', form.Value, 'max_block_limit', _('Maximum block size (MB)'),
|
||||
_('Values below 4 let OpenList calculate a default.'));
|
||||
o.default = '0';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'max_connections', _('Max Connections'),
|
||||
o = addOption('global', form.Value, 'max_connections', _('Max Connections'),
|
||||
_('0 is unlimited, It is recommend to set a low number of concurrency (10-20) for poor performance device'));
|
||||
o.default = '0';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Value, 'max_concurrency', _('Max concurrency of local proxies'),
|
||||
o = addOption('global', form.Value, 'max_concurrency', _('Max concurrency of local proxies'),
|
||||
_('0 is unlimited, Limit the maximum concurrency of local agents. The default value is 64'));
|
||||
o.default = '64';
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('global', form.Flag, 'tls_insecure_skip_verify', _('Disable TLS Verify'),
|
||||
o = addOption('global', form.Flag, 'tls_insecure_skip_verify', _('Disable TLS Verify'),
|
||||
_('Skip remote TLS certificate verification (not recommended).'));
|
||||
o.default = false;
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
// Logs
|
||||
o = s.taboption('log', form.Flag, 'log', _('Enable Logs'));
|
||||
o.default = 1;
|
||||
o = addOption('log', form.Flag, 'log', _('Enable Logs'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_path', _('Log path'));
|
||||
o = addOption('log', form.Value, 'log_path', _('Log path'));
|
||||
o.default = '/var/log/openlist2.log';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
@@ -325,104 +427,114 @@ return view.extend({
|
||||
return _('Log path must be /var/log/openlist2* without subdirectories.');
|
||||
};
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_max_size', _('Max Size (MB)'));
|
||||
o = addOption('log', form.Value, 'log_max_size', _('Max Size (MB)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '50';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_max_backups', _('Max backups'));
|
||||
o = addOption('log', form.Value, 'log_max_backups', _('Max backups'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '30';
|
||||
o.default = '3';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_max_age', _('Max age'));
|
||||
o = addOption('log', form.Value, 'log_max_age', _('Max age'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '28';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Flag, 'log_compress', _('Log Compress'));
|
||||
o.default = 'false';
|
||||
o = addOption('log', form.Flag, 'log_compress', _('Log Compress'));
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Flag, 'log_filter', _('Enable common log filters'),
|
||||
o = addOption('log', form.Flag, 'log_filter', _('Enable common log filters'),
|
||||
_('Filter noisy access logs such as health checks, HEAD requests and WebDAV PROPFIND.'));
|
||||
o.rmempty = false;
|
||||
o.depends('log', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_filter_cidr', _('Custom log filter CIDR'));
|
||||
o = addOption('log', form.Value, 'log_filter_cidr', _('Custom log filter CIDR'));
|
||||
o.depends('log_filter', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_filter_path', _('Custom log filter path'));
|
||||
o = addOption('log', form.Value, 'log_filter_path', _('Custom log filter path'));
|
||||
o.depends('log_filter', '1');
|
||||
|
||||
o = s.taboption('log', form.Value, 'log_filter_method', _('Custom log filter method'));
|
||||
o = addOption('log', form.Value, 'log_filter_method', _('Custom log filter method'));
|
||||
o.depends('log_filter', '1');
|
||||
|
||||
// database
|
||||
o = s.taboption('database', form.ListValue, 'database_type', _('Database Type'));
|
||||
o = addOption('database', form.ListValue, 'database_type', _('Database Type'));
|
||||
o.default = 'sqlite3';
|
||||
o.value('sqlite3', _('SQLite'));
|
||||
o.value('mysql', _('MySQL'));
|
||||
o.value('postgres', _('PostgreSQL'));
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_host', _('Database Host'));
|
||||
o = addOption('database', form.Value, 'mysql_host', _('Database Host'));
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_port', _('Database Port'),
|
||||
o = addOption('database', form.Value, 'mysql_port', _('Database Port'),
|
||||
_('Use 0 to apply the usual default for the selected database: 3306 for MySQL, 5432 for PostgreSQL.'));
|
||||
o.datatype = 'port';
|
||||
o.default = '0';
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_username', _('Database Username'));
|
||||
o = addOption('database', form.Value, 'mysql_username', _('Database Username'));
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_password', _('Database Password'));
|
||||
o = addOption('database', form.Value, 'mysql_password', _('Database Password'));
|
||||
o.password = true;
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_database', _('Database Name'));
|
||||
o = addOption('database', form.Value, 'mysql_database', _('Database Name'));
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_table_prefix', _('Database Table Prefix'));
|
||||
o = addOption('database', form.Value, 'mysql_table_prefix', _('Database Table Prefix'));
|
||||
o.default = 'x_';
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_ssl_mode', _('Database SSL Mode'));
|
||||
o = addOption('database', form.Value, 'mysql_ssl_mode', _('Database SSL Mode'));
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('database', form.Value, 'mysql_dsn', _('Database DSN'));
|
||||
o = addOption('database', form.Value, 'mysql_dsn', _('Database DSN'));
|
||||
o.password = true;
|
||||
o.depends('database_type', 'mysql');
|
||||
o.depends('database_type', 'postgres');
|
||||
|
||||
o = s.taboption('search', form.Value, 'meilisearch_host', _('Meilisearch Host'));
|
||||
o = addOption('search', form.Value, 'meilisearch_host', _('Meilisearch Host'));
|
||||
o.default = 'http://localhost:7700';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('search', form.Value, 'meilisearch_api_key', _('Meilisearch API Key'));
|
||||
o = addOption('search', form.Value, 'meilisearch_api_key', _('Meilisearch API Key'));
|
||||
o.password = true;
|
||||
|
||||
o = s.taboption('search', form.Value, 'meilisearch_index', _('Meilisearch Index'));
|
||||
o = addOption('search', form.Value, 'meilisearch_index', _('Meilisearch Index'));
|
||||
o.default = 'openlist';
|
||||
o.rmempty = false;
|
||||
|
||||
o = addOption('search', form.Value, 'bleve_dir', _('Bleve index directory'),
|
||||
_('Empty uses the bleve subdirectory of the data directory. Keep the index separate from cache and frontend files.'));
|
||||
o.default = '';
|
||||
|
||||
o = addOption('global', form.Value, 'dist_dir', _('External frontend directory'),
|
||||
_('Optional directory containing index.html from a matching OpenList frontend. Empty uses the bundled frontend. Restart OpenList after changing it.'));
|
||||
o.default = '';
|
||||
|
||||
// scheme
|
||||
o = s.taboption('scheme', form.Value, 'listen_addr', _('Listen address'));
|
||||
o = addOption('scheme', form.Value, 'listen_addr', _('Listen address'));
|
||||
o.default = '0.0.0.0';
|
||||
o.datatype = 'ipaddr';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'listen_http_port', _('HTTP listen port'),
|
||||
o = addOption('scheme', form.Value, 'listen_http_port', _('HTTP listen port'),
|
||||
_('The upstream default is 5244. Set to -1 to disable HTTP.'));
|
||||
o.datatype = 'integer';
|
||||
o.default = '5244';
|
||||
@@ -432,7 +544,7 @@ return view.extend({
|
||||
return validateListenPort(value);
|
||||
};
|
||||
|
||||
httpsPortOption = o = s.taboption('scheme', form.Value, 'listen_https_port', _('HTTPS listen port'),
|
||||
o = addOption('scheme', form.Value, 'listen_https_port', _('HTTPS listen port'),
|
||||
_('The upstream default is -1 (disabled). Set a valid port number to enable HTTPS.'));
|
||||
o.datatype = 'integer';
|
||||
o.default = '-1';
|
||||
@@ -442,132 +554,122 @@ return view.extend({
|
||||
return validateListenPort(value);
|
||||
};
|
||||
|
||||
o = s.taboption('scheme', form.Flag, 'force_https', _('Force HTTPS'));
|
||||
o = addOption('scheme', form.Flag, 'force_https', _('Force HTTPS'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'ssl_cert', _('SSL cert'),
|
||||
o = addOption('scheme', form.Value, 'ssl_cert', _('SSL cert'),
|
||||
_('SSL certificate file path'));
|
||||
o.validate = function(section_id, value) {
|
||||
return validateHttpsRequiredFile(section_id, httpsPortOption, value,
|
||||
_('SSL certificate file path is required when HTTPS is enabled.'));
|
||||
};
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'ssl_key', _('SSL key'),
|
||||
o = addOption('scheme', form.Value, 'ssl_key', _('SSL key'),
|
||||
_('SSL key file path'));
|
||||
o.validate = function(section_id, value) {
|
||||
return validateHttpsRequiredFile(section_id, httpsPortOption, value,
|
||||
_('SSL key file path is required when HTTPS is enabled.'));
|
||||
};
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'listen_unix_file', _('Unix socket file'));
|
||||
o = addOption('scheme', form.Value, 'listen_unix_file', _('Unix socket file'));
|
||||
|
||||
o = s.taboption('scheme', form.Value, 'listen_unix_file_perm', _('Unix socket permission'));
|
||||
o = addOption('scheme', form.Value, 'listen_unix_file_perm', _('Unix socket permission'));
|
||||
|
||||
o = s.taboption('scheme', form.Flag, 'listen_enable_h2c', _('Enable H2C'),
|
||||
o = addOption('scheme', form.Flag, 'listen_enable_h2c', _('Enable H2C'),
|
||||
_('Enable cleartext HTTP/2 for reverse proxies that use grpc_pass.'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('scheme', form.Flag, 'listen_enable_h3', _('Enable HTTP/3/QUIC'),
|
||||
o = addOption('scheme', form.Flag, 'listen_enable_h3', _('Enable HTTP/3/QUIC'),
|
||||
_('Enable HTTP/3 over QUIC on the HTTPS listen port. HTTPS must be enabled and certificate files must be configured.'));
|
||||
o.rmempty = false;
|
||||
|
||||
// tasks
|
||||
o = s.taboption('tasks', form.Value, 'download_workers', _('Download Workers'));
|
||||
addBackendLink('tasks', '_traffic', '@manage/settings/traffic', _('Open traffic settings'),
|
||||
_('Use the OpenList backend to change the effective worker counts and transfer speed limits.'));
|
||||
o = addOption('tasks', form.Value, 'download_workers', _('Download Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'download_max_retry', _('Download Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'download_max_retry', _('Download Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'download_task_persistant', _('Download Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'download_task_persistant', _('Download Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'transfer_workers', _('Transfer Workers'));
|
||||
o = addOption('tasks', form.Value, 'transfer_workers', _('Transfer Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'transfer_max_retry', _('Transfer Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'transfer_max_retry', _('Transfer Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'transfer_task_persistant', _('Transfer Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'transfer_task_persistant', _('Transfer Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'upload_workers', _('Upload Workers'));
|
||||
o = addOption('tasks', form.Value, 'upload_workers', _('Upload Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'upload_max_retry', _('Upload Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'upload_max_retry', _('Upload Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '0';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'upload_task_persistant', _('Upload Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'copy_workers', _('Copy Workers'));
|
||||
o = addOption('tasks', form.Value, 'copy_workers', _('Copy Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'copy_max_retry', _('Copy Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'copy_max_retry', _('Copy Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'copy_task_persistant', _('Copy Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'copy_task_persistant', _('Copy Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'move_workers', _('Move Workers'));
|
||||
o = addOption('tasks', form.Value, 'move_workers', _('Move Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'move_max_retry', _('Move Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'move_max_retry', _('Move Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'move_task_persistant', _('Move Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'move_task_persistant', _('Move Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'decompress_workers', _('Decompress Workers'));
|
||||
o = addOption('tasks', form.Value, 'decompress_workers', _('Decompress Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'decompress_max_retry', _('Decompress Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'decompress_max_retry', _('Decompress Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'decompress_task_persistant', _('Decompress Task Persistence'));
|
||||
o = addOption('tasks', form.Flag, 'decompress_task_persistant', _('Decompress Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'decompress_upload_workers', _('Decompress Upload Workers'));
|
||||
o = addOption('tasks', form.Value, 'decompress_upload_workers', _('Decompress Upload Workers (initial)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '5';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Value, 'decompress_upload_max_retry', _('Decompress Upload Max Retry'));
|
||||
o = addOption('tasks', form.Value, 'decompress_upload_max_retry', _('Decompress Upload Max Retry'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '2';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'decompress_upload_task_persistant', _('Decompress Upload Task Persistence'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('tasks', form.Flag, 'allow_retry_canceled', _('Allow retry canceled tasks'));
|
||||
o = addOption('tasks', form.Flag, 'allow_retry_canceled', _('Allow retry canceled tasks'));
|
||||
o.rmempty = false;
|
||||
|
||||
// cors
|
||||
o = s.taboption('cors', form.Value, 'cors_allow_origins', _('Allow Origins'),
|
||||
o = addOption('cors', form.Value, 'cors_allow_origins', _('Allow Origins'),
|
||||
_('Comma-separated list. Use * to allow any value.'));
|
||||
o.default = '*';
|
||||
o.rmempty = false;
|
||||
@@ -575,83 +677,190 @@ return view.extend({
|
||||
return validateCorsOrigins(value);
|
||||
};
|
||||
|
||||
o = s.taboption('cors', form.Value, 'cors_allow_methods', _('Allow Methods'),
|
||||
o = addOption('cors', form.Value, 'cors_allow_methods', _('Allow Methods'),
|
||||
_('Comma-separated list. Use * to allow any value.'));
|
||||
o.default = '*';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('cors', form.Value, 'cors_allow_headers', _('Allow Headers'),
|
||||
o = addOption('cors', form.Value, 'cors_allow_headers', _('Allow Headers'),
|
||||
_('Comma-separated list. Use * to allow any value.'));
|
||||
o.default = '*';
|
||||
o.rmempty = false;
|
||||
|
||||
// s3
|
||||
o = s.taboption('s3', form.Flag, 's3', _('Enabled S3'));
|
||||
addBackendLink('s3', '_s3_settings', '@manage/settings/s3', _('Open S3 management'),
|
||||
_('Generate the S3 access key and secret in OpenList settings, then configure bucket mappings. Restart OpenList after changing credentials.'));
|
||||
o = addOption('s3', form.Flag, 's3', _('Enabled S3'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('s3', form.Value, 's3_port', _('Port'));
|
||||
o = addOption('s3', form.Value, 's3_port', _('Port'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = 5246;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('s3', form.Flag, 's3_ssl', _('Enable SSL'));
|
||||
o = addOption('s3', form.Flag, 's3_ssl', _('Enable SSL'));
|
||||
o.rmempty = false;
|
||||
|
||||
// ftp
|
||||
o = s.taboption('ftp', form.Flag, 'ftp', _('Enabled FTP'));
|
||||
addBackendLink('ftp', '_ftp_settings', '@manage/settings/ftp', _('Open FTP settings'),
|
||||
_('Configure the public host and passive port mapping in OpenList. Restart the service after changing FTP settings.'));
|
||||
o = addOption('ftp', form.DynamicList, 'ftp_pasv_ports', _('FTP passive firewall ports'),
|
||||
_('Ports or ranges to allow from WAN, for example 50000-50100. They must match the listening ports in OpenList passive port mapping. Empty means passive ports must be allowed manually.'));
|
||||
o.placeholder = '50000-50100';
|
||||
o.depends({ ftp: '1', allow_wan: '1' });
|
||||
o = addOption('ftp', form.Flag, 'ftp', _('Enabled FTP'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Value, 'ftp_port', _('FTP Port'));
|
||||
o = addOption('ftp', form.Value, 'ftp_port', _('FTP Port'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = 5221;
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Value, 'find_pasv_port_attempts', _('Max retries on port conflict during passive transfer'));
|
||||
o = addOption('ftp', form.Value, 'find_pasv_port_attempts', _('Max retries on port conflict during passive transfer'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '50';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'active_transfer_port_non_20', _('Enable non-20 port for active transfer'));
|
||||
o = addOption('ftp', form.Flag, 'active_transfer_port_non_20', _('Enable non-20 port for active transfer'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Value, 'idle_timeout', _('Client idle timeout (seconds)'));
|
||||
o = addOption('ftp', form.Value, 'idle_timeout', _('Client idle timeout (seconds)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '900';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Value, 'connection_timeout', _('Connection timeout (seconds)'));
|
||||
o = addOption('ftp', form.Value, 'connection_timeout', _('Connection timeout (seconds)'));
|
||||
o.datatype = 'uinteger';
|
||||
o.default = '30';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'disable_active_mode', _('Disable active transfer mode'));
|
||||
o = addOption('ftp', form.Flag, 'disable_active_mode', _('Disable active transfer mode'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'default_transfer_binary', _('Enable binary transfer mode'));
|
||||
o = addOption('ftp', form.Flag, 'default_transfer_binary', _('Enable binary transfer mode'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'enable_active_conn_ip_check', _('Client IP check in active transfer mode'));
|
||||
o = addOption('ftp', form.Flag, 'enable_active_conn_ip_check', _('Client IP check in active transfer mode'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('ftp', form.Flag, 'enable_pasv_conn_ip_check', _('Client IP check in passive transfer mode'));
|
||||
o = addOption('ftp', form.Flag, 'enable_pasv_conn_ip_check', _('Client IP check in passive transfer mode'));
|
||||
o.default = '1';
|
||||
o.rmempty = false;
|
||||
|
||||
// sftp
|
||||
o = s.taboption('sftp', form.Flag, 'sftp', _('Enabled SFTP'));
|
||||
o = addOption('sftp', form.Flag, 'sftp', _('Enabled SFTP'));
|
||||
o.rmempty = false;
|
||||
|
||||
o = s.taboption('sftp', form.Value, 'sftp_port', _('SFTP Port'));
|
||||
o = addOption('sftp', form.Value, 'sftp_port', _('SFTP Port'));
|
||||
o.datatype = 'and(port,min(1))';
|
||||
o.default = 5222;
|
||||
o.rmempty = false;
|
||||
|
||||
// mcp
|
||||
o = s.taboption('mcp', form.Flag, 'mcp', _('Enabled MCP'),
|
||||
o = addOption('mcp', form.Flag, 'mcp', _('Enabled MCP'),
|
||||
_('Enable MCP (Model Context Protocol) server.'));
|
||||
o.rmempty = false;
|
||||
|
||||
|
||||
o = addOption('mcp', form.DummyValue, '_mcp_endpoint', _('MCP endpoint'),
|
||||
_('Use Streamable HTTP with Authorization: TOKEN, without a Bearer prefix. Use an OpenList user login token; retain MCP-Session-Id for subsequent requests.'));
|
||||
o.depends('mcp', '1');
|
||||
o.renderWidget = () => E('div', {}, [
|
||||
E('code', {}, backendUrl('mcp') || _('Configure a web port or Site URL.')),
|
||||
E('p', {}, E('a', { 'href': 'https://doc.oplist.org/guide/advanced/mcp', 'target': '_blank', 'rel': 'noopener noreferrer' }, _('MCP documentation')))
|
||||
]);
|
||||
|
||||
const validateDirectories = (section_id, changed, value) => {
|
||||
const required = changed === 'data_dir' || changed === 'temp_dir';
|
||||
const valid = validateAbsolutePath(value, required);
|
||||
if (valid !== true)
|
||||
return valid;
|
||||
const get = (name, fallback) => normalizePath(name === changed ? value || fallback : fieldValue(section_id, name, fallback) || fallback);
|
||||
const data = get('data_dir', '/etc/openlist2');
|
||||
const cache = get('temp_dir', '/tmp/openlist2');
|
||||
const index = get('bleve_dir', data + '/bleve');
|
||||
const distValue = changed === 'dist_dir' ? value : fieldValue(section_id, 'dist_dir');
|
||||
const dist = distValue ? normalizePath(distValue) : '';
|
||||
for (const path of [data, cache, index, dist].filter(Boolean))
|
||||
if (isBroadDirectory(path))
|
||||
return _('Use a dedicated subdirectory, not a system directory or disk mount root.');
|
||||
if (/^\/(?:mnt|media)\/[^/]+$/.test(cache))
|
||||
return _('Use a cache subdirectory below the disk mount.');
|
||||
if (containsPath(cache, data) || containsPath(index, data) ||
|
||||
containsPath(cache, index) || containsPath(index, cache))
|
||||
return _('Cache and index directories must be separate and must not contain the data directory.');
|
||||
if (dist && (containsPath(dist, data) || containsPath(dist, cache) ||
|
||||
containsPath(cache, dist) || containsPath(dist, index) || containsPath(index, dist)))
|
||||
return _('Frontend files must be separate from private data, cache and index files.');
|
||||
return true;
|
||||
};
|
||||
for (const name of ['data_dir', 'temp_dir', 'bleve_dir', 'dist_dir'])
|
||||
fields[name].validate = (section_id, value) => validateDirectories(section_id, name, value);
|
||||
|
||||
const tlsRequired = section_id => isEnabledPort(fieldValue(section_id, 'listen_https_port', '-1')) ||
|
||||
(fieldValue(section_id, 's3') === '1' && fieldValue(section_id, 's3_ssl') === '1');
|
||||
for (const name of ['ssl_cert', 'ssl_key']) {
|
||||
fields[name].validate = (section_id, value) => {
|
||||
if (tlsRequired(section_id) && !String(value || '').trim())
|
||||
return _('Certificate and key paths are required for HTTPS or S3 SSL.');
|
||||
return validateAbsolutePath(value, false);
|
||||
};
|
||||
}
|
||||
for (const name of ['force_https', 'listen_enable_h3'])
|
||||
fields[name].validate = (section_id, value) => value !== '1' || isEnabledPort(fieldValue(section_id, 'listen_https_port', '-1'))
|
||||
? true : _('Force HTTPS and HTTP/3 require an enabled HTTPS port.');
|
||||
fields.s3_ssl.validate = (section_id, value) => value !== '1' || fieldValue(section_id, 's3') !== '1' ||
|
||||
(fieldValue(section_id, 'ssl_cert').trim() && fieldValue(section_id, 'ssl_key').trim())
|
||||
? true : _('Certificate and key paths are required for HTTPS or S3 SSL.');
|
||||
fields.listen_unix_file.validate = (section_id, value) => validateAbsolutePath(value, false);
|
||||
fields.listen_unix_file_perm.validate = (section_id, value) => !value || /^[0-7]{3,4}$/.test(value)
|
||||
? true : _('Use 3 or 4 octal digits, for example 660 or 0660.');
|
||||
fields.log_max_size.datatype = 'and(uinteger,min(1))';
|
||||
|
||||
const validatePorts = (section_id, changed, value) => {
|
||||
const get = name => name === changed ? value : fieldValue(section_id, name,
|
||||
name === 'listen_http_port' ? '5244' : name === 'listen_https_port' ? '-1' : '');
|
||||
const names = ['listen_http_port', 'listen_https_port'];
|
||||
if (fieldValue(section_id, 's3') === '1') names.push('s3_port');
|
||||
if (fieldValue(section_id, 'ftp') === '1') names.push('ftp_port');
|
||||
if (fieldValue(section_id, 'sftp') === '1') names.push('sftp_port');
|
||||
const seen = new Set();
|
||||
const usePassive = changed === 'ftp_pasv_ports' ||
|
||||
(fieldValue(section_id, 'ftp') === '1' && fieldValue(section_id, 'allow_wan') === '1');
|
||||
const passive = usePassive ? passiveRanges(changed === 'ftp_pasv_ports' ? value : fieldValue(section_id, 'ftp_pasv_ports')) : [];
|
||||
const passiveValid = validatePassivePorts(passive);
|
||||
if (passiveValid !== true)
|
||||
return passiveValid;
|
||||
for (const name of names) {
|
||||
const candidate = String(get(name)).trim();
|
||||
const valid = validateListenPort(candidate);
|
||||
if (valid !== true)
|
||||
return valid;
|
||||
if (candidate === '-1')
|
||||
continue;
|
||||
const port = Number(candidate);
|
||||
if (seen.has(port))
|
||||
return _('Enabled services cannot share TCP port %s.').format(port);
|
||||
seen.add(port);
|
||||
if (fieldValue(section_id, 'ftp') === '1')
|
||||
for (const range of passive) {
|
||||
const [first, last = first] = range.split('-').map(Number);
|
||||
if (port >= first && port <= last)
|
||||
return _('FTP passive ports must not overlap service port %s.').format(port);
|
||||
}
|
||||
}
|
||||
if (!isEnabledPort(get('listen_https_port')) &&
|
||||
(fieldValue(section_id, 'force_https') === '1' || fieldValue(section_id, 'listen_enable_h3') === '1'))
|
||||
return _('Force HTTPS and HTTP/3 require an enabled HTTPS port.');
|
||||
return true;
|
||||
};
|
||||
for (const name of ['listen_http_port', 'listen_https_port', 's3_port', 'ftp_port', 'sftp_port', 'ftp_pasv_ports'])
|
||||
fields[name].validate = (section_id, value) => validatePorts(section_id, name, value);
|
||||
for (const name of ['ftp_pasv_ports', 'log_path', 'log_max_size', 'log_max_backups', 'log_max_age',
|
||||
'log_compress', 'log_filter', 'log_filter_cidr', 'log_filter_path', 'log_filter_method'])
|
||||
fields[name].retain = true;
|
||||
|
||||
return m.render();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,91 +1,72 @@
|
||||
'use strict';
|
||||
'require fs';
|
||||
'require poll';
|
||||
'require uci';
|
||||
'require ui';
|
||||
'require view';
|
||||
|
||||
const DEFAULT_LOG_PATH = '/var/log/openlist2.log';
|
||||
const HELPER = '/usr/libexec/openlist2-helper';
|
||||
let logTextarea, errorMessage;
|
||||
|
||||
let scrollPosition = 0;
|
||||
let userScrolled = false;
|
||||
let logTextarea;
|
||||
let log_path = DEFAULT_LOG_PATH;
|
||||
|
||||
const isSafeLogPath = value => /^\/var\/log\/openlist2(?:[._-][A-Za-z0-9._-]+)?$/.test(value || '');
|
||||
|
||||
const formatLog = res => {
|
||||
const log = (res || '').trim().replace(/\u001b\[[0-9;]*m/g, '');
|
||||
|
||||
return log || _('No log data.');
|
||||
};
|
||||
const formatLog = text => (text || '').replace(/\u001b\[[0-9;]*m/g, '').trim() || _('No log data.');
|
||||
|
||||
const pollLog = () => {
|
||||
if (!logTextarea)
|
||||
return Promise.resolve();
|
||||
|
||||
return fs.read_direct(log_path, 'text')
|
||||
.then(formatLog)
|
||||
.catch(() => _('No log data.'))
|
||||
.then(log => {
|
||||
logTextarea.value = log;
|
||||
return fs.exec(HELPER, ['log-read']).then(result => {
|
||||
if (result.code !== 0)
|
||||
throw new Error((result.stderr || _('Unable to read the log.')).trim());
|
||||
|
||||
if (!userScrolled) {
|
||||
logTextarea.scrollTop = logTextarea.scrollHeight;
|
||||
} else {
|
||||
logTextarea.scrollTop = scrollPosition;
|
||||
}
|
||||
});
|
||||
// Decide before replacing text, so programmatic scrolling is not
|
||||
// mistaken for the user scrolling away from the bottom.
|
||||
const atBottom = logTextarea.scrollTop + logTextarea.clientHeight >= logTextarea.scrollHeight - 10;
|
||||
const position = logTextarea.scrollTop;
|
||||
logTextarea.value = formatLog(result.stdout);
|
||||
logTextarea.scrollTop = atBottom ? logTextarea.scrollHeight : position;
|
||||
errorMessage.textContent = '';
|
||||
errorMessage.hidden = true;
|
||||
}).catch(error => {
|
||||
errorMessage.textContent = _('Unable to read the log: %s').format(error.message);
|
||||
errorMessage.hidden = false;
|
||||
});
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
load() {
|
||||
return uci.load('openlist2');
|
||||
},
|
||||
|
||||
handleCleanLogs() {
|
||||
return fs.write(log_path, '')
|
||||
.then(pollLog)
|
||||
.catch(e => {
|
||||
ui.addNotification(null, E('p', e.message));
|
||||
});
|
||||
if (!L.hasViewPermission())
|
||||
return Promise.resolve();
|
||||
return fs.exec(HELPER, ['log-clear']).then(result => {
|
||||
if (result.code !== 0)
|
||||
throw new Error((result.stderr || _('Unable to clear the log.')).trim());
|
||||
return pollLog();
|
||||
}).catch(error => ui.addNotification(null, E('p', error.message)));
|
||||
},
|
||||
|
||||
render() {
|
||||
const configuredLogPath = uci.get('openlist2', '@openlist2[0]', 'log_path');
|
||||
log_path = isSafeLogPath(configuredLogPath) ? configuredLogPath : DEFAULT_LOG_PATH;
|
||||
|
||||
logTextarea = E('textarea', {
|
||||
'id': 'log_content',
|
||||
'class': 'cbi-input-textarea',
|
||||
'wrap': 'off',
|
||||
'readonly': 'readonly',
|
||||
'style': 'width: calc(100% - 20px);height: 535px;margin: 10px;overflow-y: scroll;',
|
||||
});
|
||||
'style': 'width:100%;height:535px;overflow:auto;'
|
||||
}, _('Collecting data...'));
|
||||
errorMessage = E('p', { 'class': 'alert-message warning', 'role': 'status', 'hidden': true });
|
||||
const clearButton = E('button', {
|
||||
'class': 'btn cbi-button-action',
|
||||
'type': 'button',
|
||||
'disabled': !L.hasViewPermission(),
|
||||
'click': ui.createHandlerFn(this, 'handleCleanLogs')
|
||||
}, _('Clear current log'));
|
||||
|
||||
logTextarea.addEventListener('scroll', () => {
|
||||
userScrolled = true;
|
||||
scrollPosition = logTextarea.scrollTop;
|
||||
});
|
||||
|
||||
const log_textarea_wrapper = E('div', { 'id': 'log_textarea' }, logTextarea);
|
||||
|
||||
setTimeout(() => {
|
||||
poll.add(pollLog);
|
||||
}, 100);
|
||||
|
||||
const clear_logs_button = E('input', { 'class': 'btn cbi-button-action', 'type': 'button', 'style': 'margin-left: 10px; margin-top: 10px;', 'value': _('Clear logs') });
|
||||
clear_logs_button.addEventListener('click', L.bind(this.handleCleanLogs, this));
|
||||
|
||||
return E([
|
||||
E('div', { 'class': 'cbi-map' }, [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
clear_logs_button,
|
||||
log_textarea_wrapper,
|
||||
E('div', { 'style': 'text-align:right' },
|
||||
E('small', {}, _('Refresh every %s seconds.').format(L.env.pollinterval))
|
||||
)
|
||||
])
|
||||
poll.add(pollLog);
|
||||
return E('div', { 'class': 'cbi-map' }, [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('p', {}, _('Showing up to the last 200 lines and 16 KiB. Older entries remain in the log files.')),
|
||||
clearButton,
|
||||
errorMessage,
|
||||
logTextarea,
|
||||
E('p', { 'style': 'text-align:right' },
|
||||
_('Refresh every %s seconds.').format(L.env.pollinterval))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
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
@@ -8,6 +8,9 @@
|
||||
EEOF
|
||||
}
|
||||
|
||||
. /usr/share/openlist2/functions.sh
|
||||
openlist2_migrate_config || exit 1
|
||||
|
||||
[ -f "/etc/config/ucitrack" ] && {
|
||||
uci -q batch <<-EOF >/dev/null
|
||||
delete ucitrack.@openlist2[-1]
|
||||
|
||||
@@ -1,23 +1,27 @@
|
||||
{
|
||||
"luci-app-openlist2": {
|
||||
"description": "Grant UCI access for luci-app-openlist2",
|
||||
"description": "Read OpenList status and bounded logs; grant configuration and reset actions only to editors",
|
||||
"read": {
|
||||
"file": {
|
||||
"/usr/bin/openlist2": [ "exec" ],
|
||||
"/var/log/openlist2.log": [ "read" ],
|
||||
"/var/log/openlist2*": [ "read" ]
|
||||
"/usr/libexec/openlist2-helper log-read": [ "exec" ]
|
||||
},
|
||||
"ubus": {
|
||||
"service": [ "list" ]
|
||||
},
|
||||
"uci": [ "openlist2" ]
|
||||
"service": [ "list" ],
|
||||
"file": [ "exec" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"file": {
|
||||
"/var/log/openlist2.log": [ "write" ],
|
||||
"/var/log/openlist2*": [ "write" ]
|
||||
"/usr/libexec/openlist2-helper log-clear": [ "exec" ],
|
||||
"/usr/libexec/openlist2-helper password-reset": [ "exec" ]
|
||||
},
|
||||
"uci": [ "openlist2" ]
|
||||
"ubus": {
|
||||
"file": [ "exec" ],
|
||||
"uci": [ "get", "set", "add", "delete", "changes", "apply", "confirm", "revert", "order" ]
|
||||
},
|
||||
"uci": {
|
||||
"openlist2": [ "read", "write" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.9
|
||||
PKG_RELEASE:=269
|
||||
PKG_RELEASE:=270
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -913,7 +913,7 @@ function gen_config_server(node)
|
||||
stream_receive_window = node.hysteria_recv_window_conn and tonumber(node.hysteria_recv_window_conn) or nil,
|
||||
connection_receive_window = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil,
|
||||
max_concurrent_streams = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil,
|
||||
disable_path_mtu_discover = (node.hysteria_disable_mtu_discovery == "1") and true or false,
|
||||
disable_path_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false,
|
||||
tls = tls
|
||||
}
|
||||
end
|
||||
|
||||
@@ -1098,7 +1098,7 @@ add_firewall_rule() {
|
||||
|
||||
insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW"
|
||||
# Only TCP, UDP Invalid.
|
||||
insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT"
|
||||
insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket --transparent -j PSW_DIVERT"
|
||||
|
||||
$ipt_m -N PSW_OUTPUT
|
||||
$ipt_m -A PSW_OUTPUT $(dst $IPSET_LAN) -j RETURN
|
||||
@@ -1172,7 +1172,7 @@ add_firewall_rule() {
|
||||
|
||||
insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW"
|
||||
# Only TCP, UDP Invalid.
|
||||
insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT"
|
||||
insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket --transparent -j PSW_DIVERT"
|
||||
|
||||
$ip6t_m -N PSW_OUTPUT
|
||||
$ip6t_m -A PSW_OUTPUT -m mark --mark 0xff/0xff -j RETURN
|
||||
@@ -1477,7 +1477,7 @@ gen_include() {
|
||||
[ -z "${_ipt}" ] && return
|
||||
|
||||
echo "*$2"
|
||||
${_ipt}-save -t $2 | grep "PSW" | grep -v "\-j PSW$" | grep -v "mangle\-OUTPUT\-PSW" | grep -v "socket \-j PSW_DIVERT$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/"
|
||||
${_ipt}-save -t $2 | grep "PSW" | grep -v "\-j PSW$" | grep -v "mangle\-OUTPUT\-PSW" | grep -v "\-m socket .*\-j PSW_DIVERT$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/"
|
||||
echo 'COMMIT'
|
||||
}
|
||||
local __ipt=""
|
||||
@@ -1501,7 +1501,7 @@ gen_include() {
|
||||
[ -z "${is_tproxy}" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW")
|
||||
|
||||
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW")
|
||||
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT")
|
||||
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW" "-p tcp -m socket --transparent -j PSW_DIVERT")
|
||||
EOF
|
||||
)
|
||||
}
|
||||
@@ -1522,7 +1522,7 @@ gen_include() {
|
||||
[ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING -p ipv6-icmp -j PSW
|
||||
|
||||
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW")
|
||||
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket -j PSW_DIVERT")
|
||||
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW" "-p tcp -m socket --transparent -j PSW_DIVERT")
|
||||
EOF
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.9.12
|
||||
PKG_RELEASE:=119
|
||||
PKG_RELEASE:=121
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -336,7 +336,7 @@ end
|
||||
|
||||
function index_status()
|
||||
local e = {}
|
||||
e["global_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep '/acl/default' >/dev/null" % api.TMP_PATH) == 0
|
||||
e["global_status"] = luci.sys.call("/bin/busybox top -bn1 | grep -v 'grep' | grep '%s/bin/' | grep '/acl_default\\.json' >/dev/null" % api.TMP_PATH) == 0
|
||||
http_write_json(e)
|
||||
end
|
||||
|
||||
|
||||
@@ -311,8 +311,8 @@ loglevel:value("error")
|
||||
o = s:taboption("log", DummyValue, "_log", translate("Log File"))
|
||||
o.rawhtml = true
|
||||
o.cfgvalue = function(t, n)
|
||||
local log_path = api.TMP_PATH .. "/acl/default.log"
|
||||
local log_url = api.url("get_redir_log") .. "?id=default"
|
||||
local log_path = api.TMP_PATH .. "/acl/acl_default.log"
|
||||
local log_url = api.url("get_redir_log") .. "?id=acl_default"
|
||||
return string.format(
|
||||
'<code>%s</code> <input class="btn cbi-button cbi-button-apply" type="button" value="%s" onclick="window.open(\'%s\', \'_blank\')" />',
|
||||
log_path,
|
||||
|
||||
@@ -192,12 +192,16 @@ function sh_uci_commit(config)
|
||||
exec_call(string.format("uci -q commit %s", config))
|
||||
end
|
||||
|
||||
function del_cache_var(key)
|
||||
sys.call(string.format('. /usr/share/passwall2/utils.sh ; del_cache_var "%s"', key))
|
||||
end
|
||||
|
||||
function set_cache_var(key, val)
|
||||
sys.call(string.format('. /usr/share/passwall2/utils.sh ; set_cache_var %s "%s"', key, val))
|
||||
sys.call(string.format('. /usr/share/passwall2/utils.sh ; set_cache_var "%s" "%s"', key, val))
|
||||
end
|
||||
|
||||
function get_cache_var(key)
|
||||
local val = sys.exec(string.format('. /usr/share/passwall2/utils.sh ; echo -n $(get_cache_var %s)', key))
|
||||
local val = sys.exec(string.format('. /usr/share/passwall2/utils.sh ; echo -n $(get_cache_var "%s")', key))
|
||||
if val == "" then val = nil end
|
||||
return val
|
||||
end
|
||||
@@ -2054,3 +2058,11 @@ function parseDNS(dns)
|
||||
end
|
||||
return dns, 53
|
||||
end
|
||||
|
||||
function get_socks_port_by_cache(node_id)
|
||||
return get_cache_var("node_%s_socks_port" % { node_id })
|
||||
end
|
||||
|
||||
function set_socks_port_to_cache(node_id, v)
|
||||
set_cache_var("node_%s_socks_port" % { node_id }, v)
|
||||
end
|
||||
|
||||
@@ -123,31 +123,46 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
end
|
||||
|
||||
if node.type ~= "sing-box" then
|
||||
local relay_port = node.port
|
||||
local new_port = api.get_new_port()
|
||||
local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
|
||||
if tag and node_id and not tag:find(node_id) then
|
||||
config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port)
|
||||
end
|
||||
local new_port
|
||||
if run_socks_instance then
|
||||
sys.call(string.format('/usr/share/passwall2/app.sh run_socks "%s"> /dev/null',
|
||||
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s",
|
||||
new_port, --flag
|
||||
node_id, --node
|
||||
"127.0.0.1", --bind
|
||||
new_port, --socks port
|
||||
config_file, --config file
|
||||
(proxy_tag and relay_port) and tostring(relay_port) or "" --relay port
|
||||
local relay_port = (proxy_tag and node.port) and tostring(node.port) or ""
|
||||
if relay_port == "" then
|
||||
local cache = api.get_socks_port_by_cache(node_id)
|
||||
if cache then
|
||||
new_port = cache
|
||||
run_socks_instance = nil
|
||||
end
|
||||
end
|
||||
if run_socks_instance then
|
||||
new_port = api.get_new_port()
|
||||
local config_file = string.format("nodesocks_%s_%s.json", node_id, new_port)
|
||||
if tag and node_id and not tag:find(node_id) then
|
||||
config_file = string.format("nodesocks_%s_%s_%s.json", tag, node_id, new_port)
|
||||
end
|
||||
sys.call(string.format('/usr/share/passwall2/app.sh run_socks "%s"> /dev/null',
|
||||
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s",
|
||||
new_port, --flag
|
||||
node_id, --node
|
||||
"127.0.0.1", --bind
|
||||
new_port, --socks port
|
||||
config_file, --config file
|
||||
relay_port --relay port
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
if relay_port == "" then
|
||||
api.set_socks_port_to_cache(node_id, new_port)
|
||||
end
|
||||
end
|
||||
end
|
||||
if new_port then
|
||||
node = {
|
||||
protocol = "socks",
|
||||
address = "127.0.0.1",
|
||||
port = new_port
|
||||
}
|
||||
proxy_tag = "socks <- " .. node_id
|
||||
end
|
||||
node = {
|
||||
protocol = "socks",
|
||||
address = "127.0.0.1",
|
||||
port = new_port
|
||||
}
|
||||
proxy_tag = "socks <- " .. node_id
|
||||
else
|
||||
if proxy_tag then
|
||||
node.detour = proxy_tag
|
||||
@@ -934,7 +949,7 @@ function gen_config_server(node)
|
||||
stream_receive_window = node.hysteria_recv_window_conn and tonumber(node.hysteria_recv_window_conn) or nil,
|
||||
connection_receive_window = node.hysteria_recv_window_client and tonumber(node.hysteria_recv_window_client) or nil,
|
||||
max_concurrent_streams = node.hysteria_max_conn_client and tonumber(node.hysteria_max_conn_client) or nil,
|
||||
disable_path_mtu_discover = (node.hysteria_disable_mtu_discovery == "1") and true or false,
|
||||
disable_path_mtu_discovery = (node.hysteria_disable_mtu_discovery == "1") and true or false,
|
||||
tls = tls
|
||||
}
|
||||
end
|
||||
|
||||
@@ -71,32 +71,47 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
end
|
||||
|
||||
if node.type ~= "Xray" then
|
||||
local relay_port = node.port
|
||||
local new_port = api.get_new_port()
|
||||
local config_file = string.format("%s_%s_%s.json", flag, tag, new_port)
|
||||
if tag and node_id and not tag:find(node_id) then
|
||||
config_file = string.format("%s_%s_%s_%s.json", flag, tag, node_id, new_port)
|
||||
end
|
||||
local new_port
|
||||
if run_socks_instance then
|
||||
sys.call(string.format('/usr/share/passwall2/app.sh run_socks "%s"> /dev/null',
|
||||
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s",
|
||||
new_port, --flag
|
||||
node_id, --node
|
||||
"127.0.0.1", --bind
|
||||
new_port, --socks port
|
||||
config_file, --config file
|
||||
(proxy_tag and relay_port) and tostring(relay_port) or "" --relay port
|
||||
local relay_port = (proxy_tag and node.port) and tostring(node.port) or ""
|
||||
if relay_port == "" then
|
||||
local cache = api.get_socks_port_by_cache(node_id)
|
||||
if cache then
|
||||
new_port = cache
|
||||
run_socks_instance = nil
|
||||
end
|
||||
end
|
||||
if run_socks_instance then
|
||||
new_port = api.get_new_port()
|
||||
local config_file = string.format("nodesocks_%s_%s.json", node_id, new_port)
|
||||
if tag and node_id and not tag:find(node_id) then
|
||||
config_file = string.format("nodesocks_%s_%s_%s.json", tag, node_id, new_port)
|
||||
end
|
||||
sys.call(string.format('/usr/share/passwall2/app.sh run_socks "%s"> /dev/null',
|
||||
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s relay_port=%s",
|
||||
new_port, --flag
|
||||
node_id, --node
|
||||
"127.0.0.1", --bind
|
||||
new_port, --socks port
|
||||
config_file, --config file
|
||||
relay_port --relay port
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
if relay_port == "" then
|
||||
api.set_socks_port_to_cache(node_id, new_port)
|
||||
end
|
||||
end
|
||||
end
|
||||
if new_port then
|
||||
node = {}
|
||||
node.protocol = "socks"
|
||||
node.transport = "tcp"
|
||||
node.address = "127.0.0.1"
|
||||
node.port = new_port
|
||||
node.stream_security = "none"
|
||||
proxy_tag = "socks <- " .. node_id
|
||||
end
|
||||
node = {}
|
||||
node.protocol = "socks"
|
||||
node.transport = "tcp"
|
||||
node.address = "127.0.0.1"
|
||||
node.port = new_port
|
||||
node.stream_security = "none"
|
||||
proxy_tag = "socks <- " .. node_id
|
||||
else
|
||||
dialer_proxy_tag = proxy_tag
|
||||
end
|
||||
|
||||
@@ -127,7 +127,7 @@ local sid = "@global[0]"
|
||||
|
||||
const m = cbid.match(/\.node$/);
|
||||
if (m) {
|
||||
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_redir_log")%>?id=default' + '\', \'_blank\')"><%:Log%></a>';
|
||||
html += '<a href="#" onclick="window.open(\'' + '<%=api.url("get_redir_log")%>?id=acl_default' + '\', \'_blank\')"><%:Log%></a>';
|
||||
}
|
||||
|
||||
html = '<div class="node-actions" style="display:inline-flex; align-items:center; gap:4px; flex-wrap:wrap; margin-left:4px;">' + html + '</div>'
|
||||
|
||||
@@ -855,7 +855,7 @@ acl_node() {
|
||||
local DNSMASQ_DEFAULT_DNS="${AUTO_DNS}"
|
||||
local DNSMASQ_LOCAL_DNS="${LOCAL_DNS:-${AUTO_DNS}}"
|
||||
[ -n "${DIRECT_DNS_DNSMASQ_SERVER}" ] && DNSMASQ_LOCAL_DNS="${DIRECT_DNS_DNSMASQ_SERVER}"
|
||||
if [ "${flag}" = "default" ]; then
|
||||
if [ "${flag}" = "acl_default" ]; then
|
||||
set_cache_var "GLOBAL_SOCKS_server" "127.0.0.1:$socks_port"
|
||||
set_cache_var "ACL_GLOBAL_node" "$node"
|
||||
run_new_dnsmasq=$(config_n_get @global[0] dns_redirect 1)
|
||||
@@ -864,7 +864,7 @@ acl_node() {
|
||||
#Modify the default dnsmasq service
|
||||
lua $APP_PATH/helper_dnsmasq.lua stretch
|
||||
json_init
|
||||
json_add_string "FLAG" "default"
|
||||
json_add_string "FLAG" "${flag}"
|
||||
json_add_string "TMP_DNSMASQ_PATH" "${GLOBAL_DNSMASQ_CONF_PATH}"
|
||||
json_add_string "DNSMASQ_CONF_FILE" "${GLOBAL_DNSMASQ_CONF}"
|
||||
json_add_string "DEFAULT_DNS" "${DNSMASQ_DEFAULT_DNS}"
|
||||
@@ -912,7 +912,7 @@ start() {
|
||||
check_run_environment
|
||||
[ -n "$USE_TABLES" ] && {
|
||||
ACL_JSON=$(lua $APP_PATH/app_acl.lua)
|
||||
[ ! -f ${TMP_ACL_PATH}/acl_node_default ] && ENABLED_DEFAULT_ACL=0
|
||||
[ ! -f ${TMP_ACL_PATH}/acl_node_acl_default ] && ENABLED_DEFAULT_ACL=0
|
||||
local acl_node_num=$(jsonfilter -s "${ACL_JSON}" -e '$.node_order[*]' | wc -l)
|
||||
|
||||
if [ "${acl_node_num}" == 0 ]; then
|
||||
@@ -1062,7 +1062,7 @@ get_config() {
|
||||
fi
|
||||
fi
|
||||
set_cache_var GLOBAL_DNSMASQ_CONF ${DNSMASQ_CONF_DIR}/dnsmasq-${CONFIG}.conf
|
||||
set_cache_var GLOBAL_DNSMASQ_CONF_PATH ${TMP_ACL_PATH}/default_dnsmasq.d
|
||||
set_cache_var GLOBAL_DNSMASQ_CONF_PATH ${TMP_ACL_PATH}/acl_default_dnsmasq.d
|
||||
|
||||
QUEUE_RUN=1
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ end
|
||||
function init_acl()
|
||||
if true then
|
||||
-- Get Default AC
|
||||
D.flag = "default"
|
||||
D.flag = "acl_default"
|
||||
D.remarks = api.i18n.translatef("Default")
|
||||
D.tcp_no_redir_ports = uci_get("@global_forwarding[0]", "tcp_no_redir_ports")
|
||||
D.udp_no_redir_ports = uci_get("@global_forwarding[0]", "udp_no_redir_ports")
|
||||
|
||||
@@ -109,20 +109,26 @@ api.uci_foreach_c("haproxy_config", function(t)
|
||||
t.origin_port = server_port
|
||||
if health_check_type == "script_logic" then
|
||||
if server_node.type ~= "Socks" then
|
||||
local relay_port = server_node.port
|
||||
local new_port = api.get_new_port()
|
||||
local config_file = string.format("%s_%s.json", t[".name"], new_port)
|
||||
sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null',
|
||||
appname,
|
||||
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s",
|
||||
new_port, --flag
|
||||
server_node[".name"], --node
|
||||
"127.0.0.1", --bind
|
||||
new_port, --socks port
|
||||
config_file --config file
|
||||
local new_port
|
||||
local cache = api.get_socks_port_by_cache(server_node[".name"])
|
||||
if cache then
|
||||
new_port = cache
|
||||
else
|
||||
new_port = api.get_new_port()
|
||||
local config_file = string.format("%s_%s.json", t[".name"], new_port)
|
||||
sys.call(string.format('/usr/share/%s/app.sh run_socks "%s"> /dev/null',
|
||||
appname,
|
||||
string.format("flag=%s node=%s bind=%s socks_port=%s config_file=%s",
|
||||
new_port, --flag
|
||||
server_node[".name"], --node
|
||||
"127.0.0.1", --bind
|
||||
new_port, --socks port
|
||||
config_file --config file
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
api.set_socks_port_to_cache(server_node[".name"], new_port)
|
||||
end
|
||||
server_address = "127.0.0.1"
|
||||
server_port = new_port
|
||||
end
|
||||
|
||||
@@ -329,7 +329,7 @@ function add_rule(var)
|
||||
tinsert(conf_lines, "no-poll")
|
||||
tinsert(conf_lines, "no-resolv")
|
||||
|
||||
if FLAG == "default" then
|
||||
if FLAG == "acl_default" then
|
||||
api.set_cache_var("DEFAULT_DNS", DEFAULT_DNS)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -755,8 +755,8 @@ add_firewall_rule() {
|
||||
|
||||
$ipt_m -N PSW2
|
||||
# Socket Only TCP, UDP Invalid.
|
||||
$ipt_m -A PSW2 -p tcp -m socket -j MARK --set-mark ${FWMARK}
|
||||
$ipt_m -A PSW2 -p tcp -m socket -j ACCEPT
|
||||
$ipt_m -A PSW2 -p tcp -m socket --transparent -j MARK --set-mark ${FWMARK}
|
||||
$ipt_m -A PSW2 -p tcp -m socket --transparent -j ACCEPT
|
||||
$ipt_m -A PSW2 $(dst $IPSET_VPS) -j RETURN
|
||||
$ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
|
||||
$ipt_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN
|
||||
@@ -798,8 +798,8 @@ add_firewall_rule() {
|
||||
|
||||
$ip6t_m -N PSW2
|
||||
# Socket Only TCP, UDP Invalid.
|
||||
$ip6t_m -A PSW2 -p tcp -m socket -j MARK --set-mark ${FWMARK}
|
||||
$ip6t_m -A PSW2 -p tcp -m socket -j ACCEPT
|
||||
$ip6t_m -A PSW2 -p tcp -m socket --transparent -j MARK --set-mark ${FWMARK}
|
||||
$ip6t_m -A PSW2 -p tcp -m socket --transparent -j ACCEPT
|
||||
$ip6t_m -A PSW2 $(dst $IPSET_VPS6) -j RETURN
|
||||
$ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN
|
||||
$ip6t_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN
|
||||
|
||||
@@ -331,12 +331,15 @@ load_acl() {
|
||||
[ -n "$dns_redirect_port" ] && {
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp udp dport 53 counter accept"
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol tcp tcp dport 53 counter accept"
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp udp dport 53 counter accept"
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME nat_output ip protocol udp oif lo udp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
nft "add rule $NFTABLE_NAME nat_output ip protocol tcp oif lo tcp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
nft "add rule $NFTABLE_NAME nat_output meta l4proto udp oif lo udp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
nft "add rule $NFTABLE_NAME nat_output meta l4proto tcp oif lo tcp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
if [ "$PROXY_IPV6" == "1" ]; then
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp udp dport 53 counter accept"
|
||||
#nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto tcp tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo meta l4proto udp udp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo meta l4proto tcp tcp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
else
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo ip protocol udp udp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
nft "add rule $NFTABLE_NAME nat_output oif lo ip protocol tcp tcp dport 53 counter redirect to :$dns_redirect_port comment \"PSW2_DNS\""
|
||||
fi
|
||||
log 2 "${msg}$(i18n "DNS will redirected to the dedicated DNS server [%s]." "${dns_redirect_port}")"
|
||||
}
|
||||
fi
|
||||
@@ -501,20 +504,26 @@ load_acl() {
|
||||
}
|
||||
|
||||
if ([ -z "$no_tcp_proxy" ] || [ -z "$no_udp_proxy" ]) && [ -n "$dns_redirect_port" ]; then
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} udp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp ${_ipt_source} tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} udp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
if [ "$PROXY_IPV6" == "1" ]; then
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp ${_ipt_source} udp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp ${_ipt_source} tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
else
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp ${_ipt_source} udp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp ${_ipt_source} tcp dport 53 counter accept"
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol udp ${_ipt_source} udp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter redirect to :$dns_redirect_port comment \"$remarks\""
|
||||
fi
|
||||
log 2 "${msg}$(i18n "DNS will redirected to the dedicated DNS server [%s]." "${dns_redirect_port}")"
|
||||
else
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\""
|
||||
if [ "$PROXY_IPV6" == "1" ]; then
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS meta l4proto tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\""
|
||||
else
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol udp ${_ipt_source} udp dport 53 counter return comment \"$remarks\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_DNS ip protocol tcp ${_ipt_source} tcp dport 53 counter return comment \"$remarks\""
|
||||
fi
|
||||
fi
|
||||
|
||||
[ -z "$no_tcp_proxy" ] && [ -n "$redir_port" ] && {
|
||||
|
||||
@@ -60,15 +60,24 @@ get_cache_var() {
|
||||
}
|
||||
}
|
||||
|
||||
del_cache_var() {
|
||||
local key="${1}"
|
||||
[ -n "${key}" ] && [ -f "${TMP_PATH}/var" ] && {
|
||||
sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1
|
||||
}
|
||||
}
|
||||
|
||||
set_cache_var() {
|
||||
local key="${1}"
|
||||
shift 1
|
||||
local val="$@"
|
||||
[ -n "${key}" ] && [ -n "${val}" ] && {
|
||||
[ ! -d $TMP_PATH ] && mkdir -p $TMP_PATH
|
||||
sed -i "/${key}=/d" $TMP_PATH/var >/dev/null 2>&1
|
||||
echo "${key}=\"${val}\"" >> $TMP_PATH/var
|
||||
eval ${key}=\"${val}\"
|
||||
[ -n "${key}" ] && {
|
||||
del_cache_var ${key}
|
||||
local val="$@"
|
||||
[ -n "${val}" ] && {
|
||||
[ ! -d $TMP_PATH ] && mkdir -p $TMP_PATH
|
||||
echo "${key}=\"${val}\"" >> $TMP_PATH/var
|
||||
eval ${key}=\"${val}\"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_NAME:=luci-app-ssr-plus
|
||||
PKG_VERSION:=196
|
||||
PKG_RELEASE:=70
|
||||
PKG_RELEASE:=71
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
|
||||
|
||||
@@ -628,16 +628,6 @@ prepare_clash_runtime_config() {
|
||||
client_policy_stats=""
|
||||
client_rules=0
|
||||
enable_fake_ip="$(uci_get_by_type server_subscribe enable_fake_ip "")"
|
||||
socks5_auth="$(uci_get_by_type socks5_proxy socks5_auth noauth)"
|
||||
socks5_user="$(uci_get_by_type socks5_proxy socks5_user "")"
|
||||
socks5_pass="$(uci_get_by_type socks5_proxy socks5_pass "")"
|
||||
|
||||
if [ "$socks5_auth" = "password" ]; then
|
||||
if [ -z "$socks5_user" ] || [ -z "$socks5_pass" ]; then
|
||||
echolog "警告:SOCKS5 代理未完整配置用户名或密码,已自动降级为无认证模式 (noauth)。"
|
||||
socks5_auth="noauth"
|
||||
fi
|
||||
fi
|
||||
|
||||
[ -s "$cache_file" ] || return 1
|
||||
|
||||
@@ -684,6 +674,18 @@ prepare_clash_runtime_config() {
|
||||
|
||||
if [ -n "$socks_port" ] && [ "$socks_port" != "0" ]; then
|
||||
echo "socks-port: $socks_port" >>"$overlay_file"
|
||||
|
||||
socks5_auth="$(uci_get_by_type socks5_proxy socks5_auth noauth)"
|
||||
socks5_user="$(uci_get_by_type socks5_proxy socks5_user "")"
|
||||
socks5_pass="$(uci_get_by_type socks5_proxy socks5_pass "")"
|
||||
|
||||
if [ "$socks5_auth" = "password" ]; then
|
||||
if [ -z "$socks5_user" ] || [ -z "$socks5_pass" ]; then
|
||||
echolog "警告:SOCKS5 代理未完整配置用户名或密码,已自动降级为无认证模式 (noauth)。"
|
||||
socks5_auth="noauth"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$socks5_auth" = "password" ]; then
|
||||
cat >>"$overlay_file" <<-EOF
|
||||
authentication:
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wwand
|
||||
PKG_VERSION:=1.6.6_p7
|
||||
PKG_RELEASE:=19
|
||||
PKG_VERSION:=1.6.6_p9
|
||||
PKG_RELEASE:=20
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
|
||||
PKG_SOURCE_VERSION:=ef4b59a891229d189ad5167def96eab22a5ceb03
|
||||
PKG_SOURCE_VERSION:=630dd7b09fdbd12d9ddb23540968694d898a6d7c
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
@@ -175,7 +175,7 @@ table.table,
|
||||
/* Mobile cards (bootstrap-style): value fields stack one per row —
|
||||
the row title and long match-rule cells get the full width — while
|
||||
display-only widgets below pair up two per row. */
|
||||
@apply break-all max-md:mb-1 max-md:basis-full max-md:self-start max-md:text-left;
|
||||
@apply wrap-anywhere md:max-w-prose max-md:mb-1 max-md:basis-full max-md:self-start max-md:text-left;
|
||||
|
||||
&[data-widget="CBI.DummyValue"],
|
||||
&[data-widget="CBI.Button"],
|
||||
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=Aurora Theme (A modern browser theme built with Vite and Tailwind CSS)
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=1.3.1
|
||||
PKG_RELEASE:=78
|
||||
PKG_VERSION:=1.3.2
|
||||
PKG_RELEASE:=79
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -17,7 +17,7 @@ LUCI_NAME:=luci-theme-footstrap
|
||||
FOOTSTRAP_VERSION?=
|
||||
ifneq ($(FOOTSTRAP_VERSION),)
|
||||
PKG_VERSION:=$(FOOTSTRAP_VERSION)
|
||||
PKG_RELEASE:=71
|
||||
PKG_RELEASE:=72
|
||||
endif
|
||||
|
||||
LUCI_TITLE:=Footstrap Theme
|
||||
|
||||
@@ -853,6 +853,18 @@ function anchorRef() {
|
||||
}
|
||||
|
||||
let _anchorPending = null;
|
||||
/* WHY THE LAST ENGINE-OFF CORRECTION DID OR DID NOT WRITE — the `applyAnchor()` twin of `_lateWhy`,
|
||||
* exported as `anchorWhy()` for the sweep. The engine-OFF cell of `/admin/network/dhcp @390 top
|
||||
* compact` corrected at 1034 ms and 1885 ms on firefox with `theme said: null`, i.e. through this
|
||||
* path and not `lateDrift()`, and `applyAnchor()` has five ways to return without a write that all
|
||||
* read as "late" from outside. Named here so that finding says which. */
|
||||
let _anchorWhy = null;
|
||||
const _anchorTrail = [];
|
||||
function awhy(w) {
|
||||
_anchorWhy = w;
|
||||
_anchorTrail.push(w + '@' + Math.round(performance.now()));
|
||||
if (_anchorTrail.length > 8) _anchorTrail.shift();
|
||||
}
|
||||
let _anchorFrame = 0;
|
||||
/* dev switch: `localStorage.fsAnchor = 'off'` stops the theme writing the scroll offset at all,
|
||||
* which is the one thing here that can move a page nobody is touching */
|
||||
@@ -886,7 +898,17 @@ let _lateFrame = 0;
|
||||
* try from, or read the engine as having already done the job — and each guess cost a push. One
|
||||
* short string, set at every exit, ends that: the finding names the line instead of the silence. */
|
||||
let _lateWhy = null;
|
||||
function why(w) { _lateWhy = w; }
|
||||
/* THE LAST EIGHT, WITH THE CLOCK — task trail. One last word is ambiguous: a correction whose `settle`
|
||||
* has not run yet and one that ran, exited, and was re-armed by a later mutation both read `armed` at
|
||||
* the end of the sweep's window. Seen on firefox, /admin/network/dhcp @390 top large, engine on:
|
||||
* `theme said: armed` with the correction landing at 1744 ms. `performance.now()`, the clock the sweep
|
||||
* measures the refill on, so the gate can print each entry relative to it. */
|
||||
const _lateTrail = [];
|
||||
function why(w) {
|
||||
_lateWhy = w;
|
||||
_lateTrail.push(w + '@' + Math.round(performance.now()));
|
||||
if (_lateTrail.length > 8) _lateTrail.shift();
|
||||
}
|
||||
|
||||
function lateDrift(ref, grow, floorShrink) {
|
||||
/* the reference from BEFORE this tick, captured by the caller: one taken after the mutation
|
||||
@@ -1200,8 +1222,9 @@ function settleDeferredFloor(offsetBefore, shrink) {
|
||||
}
|
||||
|
||||
function scheduleAnchor(ref) {
|
||||
if (!ref || !anchorEnabled()) return;
|
||||
if (_anchorPending) return;
|
||||
if (!ref) return awhy('no-reference');
|
||||
if (!anchorEnabled()) return awhy('anchoring-off');
|
||||
if (_anchorPending) return awhy('pending-kept-first');
|
||||
_anchorPending = ref;
|
||||
if (_anchorFrame) return;
|
||||
_anchorFrame = requestAnimationFrame(() => {
|
||||
@@ -1234,7 +1257,7 @@ function applyAnchor(ref) {
|
||||
* `sawClamp()` is the pixel `holdFloor()` watched the clamp land on, and it stands only while
|
||||
* the offset has not left it — a reader who really is scrolling has moved off it by definition,
|
||||
* so this reopens the guard for exactly one case and no other. */
|
||||
if (scrolling() && !sawClamp()) return;
|
||||
if (scrolling() && !sawClamp()) return awhy('refused-moving');
|
||||
/* through scroller(), not a second probe: two copies of the same question can answer
|
||||
* differently within one frame */
|
||||
const sc = scroller();
|
||||
@@ -1247,14 +1270,15 @@ function applyAnchor(ref) {
|
||||
* collapse clamps the offset to zero, which is the worst version of this fault rather than the
|
||||
* one case to sit out. */
|
||||
if (ref.by != null) {
|
||||
if (ref.by < 1) return;
|
||||
if (ref.by < 1) return awhy('by-under-1');
|
||||
writeOffset(sc, at + ref.by);
|
||||
awhy('wrote-by-' + Math.round(ref.by));
|
||||
return true;
|
||||
}
|
||||
if (at <= 0) return;
|
||||
if (!ref.el.isConnected) return;
|
||||
if (at <= 0) return awhy('at-top');
|
||||
if (!ref.el.isConnected) return awhy('reference-gone');
|
||||
const drift = ref.el.getBoundingClientRect().top - ref.top;
|
||||
if (Math.abs(drift) < 1) return; /* nothing needed correcting here */
|
||||
if (Math.abs(drift) < 1) return awhy('no-drift'); /* nothing needed correcting here */
|
||||
/* A definite write is a definite miss — task trust: any recovery streak counted so far said
|
||||
* nothing about THIS tick, and this tick just proved the engine did not do the job on its own.
|
||||
* (Recovery evidence itself is gathered earlier, in the mutation callback — see TRUST_RECOVERY
|
||||
@@ -1267,8 +1291,9 @@ function applyAnchor(ref) {
|
||||
* is the most a single tick can honestly account for — where `innerHeight` is unreadable those
|
||||
* 200px are the whole ceiling — plus whatever the engine is on record for having clamped away
|
||||
* (`slack`, see anchorFor()). */
|
||||
if (Math.abs(drift) > (window.innerHeight || 0) + 200 + (ref.slack || 0)) return;
|
||||
if (Math.abs(drift) > (window.innerHeight || 0) + 200 + (ref.slack || 0)) return awhy('drift-too-big');
|
||||
writeOffset(sc, at + drift);
|
||||
awhy('wrote-' + Math.round(drift));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1437,9 +1462,17 @@ function observeContent() {
|
||||
* win"). This is the discriminator that survives both: a batch that removed nodes, added none
|
||||
* and left the floored box no taller is a removal, and a removal is not a page to correct
|
||||
* against. A synchronous `dom.content()` — every real poll tick — delivers its removals and
|
||||
* its additions in ONE batch and is untouched. */
|
||||
* its additions in ONE batch and is untouched.
|
||||
*
|
||||
* AND ONLY WHERE THE FLOORED BOX DID NOT SHRINK. The first cut skipped every pure removal and
|
||||
* that took out the correction REPEAT's pad removal between refills needs: the pad goes, the
|
||||
* floor comes down with it, and `floorShrink` is carried into `lateDrift()` exactly so the clamp
|
||||
* that follows gets put back. CI on cbcfd5d: `refill 2/3 left the reader -60px off, corrected
|
||||
* never` on chromium and firefox, /admin/network/dhcp @390, both stands. The transient this
|
||||
* guard is for is the empty half of a refill, where the floor HOLDS the box — so floorShrink
|
||||
* is 0 there and non-zero on a real shrink. */
|
||||
if (trustEngine) {
|
||||
if (took && !gave && grew <= 0) why('emptying');
|
||||
if (took && !gave && grew <= 0 && floorShrink <= 1) why('emptying');
|
||||
else lateDrift(settled, grew, floorShrink);
|
||||
}
|
||||
else scheduleAnchor(ref);
|
||||
@@ -1544,6 +1577,9 @@ return baseclass.extend({
|
||||
scrolling,
|
||||
/* unmarked, for tools/scroll-anchor.mjs — see `_lateWhy` */
|
||||
lateWhy: () => _lateWhy,
|
||||
lateTrail: () => _lateTrail.slice(),
|
||||
anchorWhy: () => _anchorWhy,
|
||||
anchorTrail: () => _anchorTrail.slice(),
|
||||
deferMeasurement,
|
||||
|
||||
/* -> the offset this file last took a reference at, or null before it has taken one.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
}
|
||||
|
||||
.cbi-value {
|
||||
@apply flex flex-row flex-wrap gap-4 py-1.5;
|
||||
@apply flex flex-row flex-wrap gap-4 py-1.5 max-md:flex-col max-md:gap-1.5 max-md:py-1;
|
||||
|
||||
&.hidden {
|
||||
@apply hidden;
|
||||
@@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
> .cbi-value-title {
|
||||
@apply text-foreground pt-1 text-right text-sm font-medium md:flex-[0_0_10rem];
|
||||
@apply text-foreground pt-1 text-right text-sm font-medium max-md:text-left md:flex-[0_0_10rem];
|
||||
}
|
||||
|
||||
> .cbi-value-field {
|
||||
@@ -41,7 +41,7 @@
|
||||
}
|
||||
|
||||
.cbi-dynlist {
|
||||
@apply inline-flex w-max max-w-96 flex-col items-start gap-3;
|
||||
@apply inline-flex w-max max-w-96 flex-col items-start gap-3 max-md:max-w-full;
|
||||
|
||||
.item {
|
||||
@apply border-border bg-panel-bg pointer-events-auto relative inline-flex cursor-move flex-col items-start gap-2 self-stretch overflow-hidden rounded-lg border py-3 pr-9 pl-4 break-all shadow-sm transition-all duration-200 select-text;
|
||||
@@ -80,17 +80,3 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cbi-value {
|
||||
@apply flex-col gap-1.5 py-1;
|
||||
|
||||
> .cbi-value-title {
|
||||
@apply text-left;
|
||||
}
|
||||
}
|
||||
|
||||
.cbi-dynlist {
|
||||
@apply max-w-full;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
table.table,
|
||||
.table {
|
||||
@apply border-border mb-4 w-full border-separate border-spacing-0 overflow-visible rounded-lg border;
|
||||
@apply border-border mb-4 w-full border-separate border-spacing-0 overflow-visible rounded-lg border max-md:block max-md:rounded-none max-md:border-0 max-md:shadow-none;
|
||||
|
||||
&[width="100%"] {
|
||||
@apply w-full;
|
||||
@@ -27,9 +27,14 @@ table.table,
|
||||
@apply before:bg-label-surface before:rounded-tl-lg;
|
||||
}
|
||||
|
||||
tbody,
|
||||
.tbody {
|
||||
@apply max-md:block;
|
||||
}
|
||||
|
||||
tr,
|
||||
.tr {
|
||||
@apply hover:bg-label-surface transition-colors duration-200;
|
||||
@apply hover:bg-label-surface max-md:border-border transition-colors duration-200 max-md:flex max-md:flex-wrap max-md:border-b max-md:px-2 max-md:py-2;
|
||||
|
||||
&:first-child {
|
||||
&:not([data-title]) {
|
||||
@@ -69,7 +74,7 @@ table.table,
|
||||
|
||||
th,
|
||||
.th {
|
||||
@apply border-border bg-label-surface border-b px-3 py-2.5 text-left text-sm font-medium backdrop-blur-sm;
|
||||
@apply border-border bg-label-surface border-b px-3 py-2.5 text-left text-sm font-medium backdrop-blur-sm max-md:hidden;
|
||||
|
||||
&[width="100%"] {
|
||||
@apply w-full;
|
||||
@@ -91,8 +96,8 @@ table.table,
|
||||
|
||||
td,
|
||||
.td {
|
||||
@apply border-border table-cell border-b px-3 py-2.5 align-middle text-sm break-words whitespace-normal;
|
||||
@apply before:hidden before:content-[attr(data-title)];
|
||||
@apply border-border table-cell border-b px-3 py-2.5 align-middle text-sm break-words whitespace-normal max-md:flex-[1_1_50%] max-md:border-0 max-md:px-1 max-md:py-1;
|
||||
@apply before:hidden before:content-[attr(data-title)] before:max-md:mb-1 before:max-md:block before:max-md:text-xs before:max-md:font-semibold;
|
||||
|
||||
> input[type="text"],
|
||||
> input[type="password"],
|
||||
@@ -109,76 +114,38 @@ table.table,
|
||||
}
|
||||
|
||||
&.cbi-value-field {
|
||||
@apply break-all;
|
||||
/* Mobile cards: value fields stack one per row —
|
||||
the row title and long match-rule cells get the full width — while
|
||||
display-only widgets below pair up two per row. */
|
||||
@apply wrap-anywhere max-md:mb-1 max-md:basis-full max-md:self-start max-md:text-left md:max-w-prose;
|
||||
|
||||
&[data-widget="CBI.DummyValue"],
|
||||
&[data-widget="CBI.Button"],
|
||||
&[data-widget="CBI.FlagValue"],
|
||||
&[data-widget="dvalue"],
|
||||
&[data-widget="button"],
|
||||
&[data-widget="fvalue"] {
|
||||
@apply max-md:basis-1/2;
|
||||
}
|
||||
}
|
||||
|
||||
&.cbi-section-actions {
|
||||
@apply max-md:flex-[0_0_100%];
|
||||
}
|
||||
|
||||
&.cbi-section-actions > div > .cbi-button {
|
||||
@apply flex-1;
|
||||
}
|
||||
|
||||
> .cbi-button {
|
||||
@apply max-md:w-full;
|
||||
}
|
||||
|
||||
&[width="100%"] {
|
||||
@apply w-full;
|
||||
@apply w-full max-md:flex-[0_0_100%];
|
||||
}
|
||||
&[width="33%"] {
|
||||
@apply w-1/3;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Mobile responsive — rows become stacked cards */
|
||||
@media (max-width: 768px) {
|
||||
table.table,
|
||||
.table {
|
||||
@apply block w-full overflow-visible rounded-none border-0 shadow-none;
|
||||
|
||||
tbody,
|
||||
.tbody {
|
||||
@apply block;
|
||||
}
|
||||
|
||||
tr,
|
||||
.tr {
|
||||
@apply border-border flex flex-wrap border-b px-2 py-2;
|
||||
}
|
||||
|
||||
th,
|
||||
.th {
|
||||
@apply hidden;
|
||||
}
|
||||
|
||||
td,
|
||||
.td {
|
||||
@apply flex-[1_1_50%] border-0 px-1 py-1 text-sm;
|
||||
@apply before:mb-1 before:block before:text-xs before:font-semibold;
|
||||
|
||||
&.cbi-value-field {
|
||||
/* Mobile cards: value fields stack one per row —
|
||||
the row title and long match-rule cells get the full width — while
|
||||
display-only widgets below pair up two per row. */
|
||||
@apply mb-1 basis-full self-start text-left;
|
||||
|
||||
&[data-widget="CBI.DummyValue"],
|
||||
&[data-widget="CBI.Button"],
|
||||
&[data-widget="CBI.FlagValue"],
|
||||
&[data-widget="dvalue"],
|
||||
&[data-widget="button"],
|
||||
&[data-widget="fvalue"] {
|
||||
@apply basis-1/2;
|
||||
}
|
||||
}
|
||||
|
||||
&.cbi-section-actions {
|
||||
@apply flex-[0_0_100%];
|
||||
}
|
||||
> .cbi-button {
|
||||
@apply w-full;
|
||||
}
|
||||
&[width="100%"] {
|
||||
@apply flex-[0_0_100%];
|
||||
}
|
||||
&[width="33%"] {
|
||||
@apply flex-[0_0_33.333%];
|
||||
}
|
||||
@apply w-1/3 max-md:flex-[0_0_33.333%];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
LUCI_TITLE:=A modern sidebar LuCI theme for OpenWrt, built with shadcn/ui design language.
|
||||
LUCI_DEPENDS:=+luci-base
|
||||
|
||||
PKG_VERSION:=0.5.2
|
||||
PKG_RELEASE:=1
|
||||
PKG_VERSION:=0.5.3
|
||||
PKG_RELEASE:=2
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
|
||||
LUCI_MINIFY_CSS:=
|
||||
|
||||
File diff suppressed because one or more lines are too long
+2
-2
@@ -14,8 +14,8 @@ MISE_HASH_x86_64:=3832f39c325e343f81fe3d92b2447c5d1a5eea1bc85092bb7b6c2580622264
|
||||
MISE_HASH_aarch64:=06186cfbfe947049b21d58575fb0ea800cc26ed1375f20f4b678cb3a9d679437
|
||||
|
||||
PKG_NAME:=mise
|
||||
PKG_VERSION:=2026.9.5
|
||||
PKG_RELEASE:=11
|
||||
PKG_VERSION:=2026.9.6
|
||||
PKG_RELEASE:=12
|
||||
PKG_SOURCE:=$(PKG_NAME)-v$(PKG_VERSION)-linux-$(MISE_ARCH)-musl.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/jdx/mise/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
+2
-2
@@ -10,12 +10,12 @@ include $(INCLUDE_DIR)/kernel.mk
|
||||
|
||||
PKG_NAME:=natflow
|
||||
PKG_VERSION:=20260531
|
||||
PKG_RELEASE:=83
|
||||
PKG_RELEASE:=84
|
||||
|
||||
PKG_SOURCE:=$(PKG_VERSION).tar.xz
|
||||
PKG_SOURCE_URL:=https://github.com/ptpt52/natflow.git
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=6f03a5d1e50438ff72b0c9f19a5c9c97c329c62c
|
||||
PKG_SOURCE_VERSION:=36123df5df3371516b648e511dad66d5413b47aa
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_MAINTAINER:=Chen Minqiang <ptpt52@gmail.com>
|
||||
PKG_LICENSE:=GPL-2.0
|
||||
|
||||
+9
-4
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_NAME:=openlist2
|
||||
PKG_VERSION:=4.2.6
|
||||
PKG_WEB_VERSION:=4.2.6
|
||||
PKG_RELEASE:=15
|
||||
PKG_RELEASE:=16
|
||||
|
||||
PKG_SOURCE:=openlist-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/OpenListTeam/OpenList/tar.gz/v$(PKG_VERSION)?
|
||||
@@ -51,7 +51,8 @@ define Package/openlist2
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=A file list program that supports multiple storage
|
||||
URL:=https://doc.oplist.org/
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) +ca-bundle +jshn +jsonfilter
|
||||
PROVIDES:=openlist2-helper
|
||||
endef
|
||||
|
||||
define Package/openlist2/conffiles
|
||||
@@ -84,10 +85,14 @@ define Package/openlist2/install
|
||||
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/OpenList $(1)/usr/bin/openlist2
|
||||
$(INSTALL_DIR) $(1)/etc/config $(1)/etc/init.d $(1)/etc/openlist2
|
||||
$(INSTALL_DIR) $(1)/etc/config $(1)/etc/init.d $(1)/etc/openlist2 $(1)/etc/uci-defaults
|
||||
$(INSTALL_DIR) $(1)/usr/libexec $(1)/usr/share/openlist2
|
||||
$(INSTALL_CONF) $(CURDIR)/files/openlist2.config $(1)/etc/config/openlist2
|
||||
$(INSTALL_BIN) $(CURDIR)/files/openlist2.init $(1)/etc/init.d/openlist2
|
||||
$(INSTALL_DATA) $(CURDIR)/files/data.db $(1)/etc/openlist2/data.db
|
||||
$(INSTALL_DATA) $(CURDIR)/files/openlist2.common $(1)/usr/share/openlist2/functions.sh
|
||||
$(INSTALL_BIN) $(CURDIR)/files/openlist2.helper $(1)/usr/libexec/openlist2-helper
|
||||
$(INSTALL_BIN) $(CURDIR)/files/openlist2.defaults $(1)/etc/uci-defaults/99-openlist2
|
||||
$(INSTALL_CONF) $(CURDIR)/files/data.db $(1)/etc/openlist2/data.db
|
||||
endef
|
||||
|
||||
$(eval $(call Download,openlist-frontend))
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,252 @@
|
||||
#!/bin/sh
|
||||
# Shared validation for the service and its limited LuCI helper.
|
||||
|
||||
openlist2_error() {
|
||||
printf 'openlist2: %s\n' "$*" >&2
|
||||
logger -t openlist2 "$*" 2>/dev/null
|
||||
return 1
|
||||
}
|
||||
|
||||
openlist2_read_scalar() {
|
||||
# Preserve the value itself, including any trailing newline. Remove only
|
||||
# the one output terminator emitted by uci/jsonfilter.
|
||||
OPENLIST2_VALUE="$("$@"; printf '.')"
|
||||
OPENLIST2_VALUE="${OPENLIST2_VALUE%.}"
|
||||
OPENLIST2_VALUE="${OPENLIST2_VALUE%'
|
||||
'}"
|
||||
}
|
||||
|
||||
openlist2_port() {
|
||||
local value="$1"
|
||||
[ "$value" = "-1" ] && [ "${2:-0}" = "1" ] && { printf '%s\n' -1; return 0; }
|
||||
case "$value" in ''|*[!0-9]*) return 1 ;; esac
|
||||
value="$(printf '%s' "$value" | sed 's/^0*//')"
|
||||
[ -n "$value" ] && [ "${#value}" -le 5 ] || return 1
|
||||
[ "$value" -ge 1 ] && [ "$value" -le 65535 ] || return 1
|
||||
printf '%s\n' "$value"
|
||||
}
|
||||
|
||||
openlist2_safe_log_path() {
|
||||
local filename="${1##*/}"
|
||||
[ "$1" = "/var/log/$filename" ] || return 1
|
||||
case "$filename" in ''|*[!A-Za-z0-9._-]*) return 1 ;; esac
|
||||
printf '%s\n' "$1" | grep -Eq '^/var/log/openlist2([._-][A-Za-z0-9._-]+)?$' || return 1
|
||||
# Do not let a root helper follow a user-created log file symlink.
|
||||
[ ! -L "$1" ] && { [ ! -e "$1" ] || [ -f "$1" ]; }
|
||||
}
|
||||
|
||||
openlist2_normalize_dir() {
|
||||
local path="$1" suffix="" part
|
||||
case "$path" in /*) ;; *) return 1 ;; esac
|
||||
case "$path" in *'
|
||||
'*|*' '*|*"$(printf '\r')"*) return 1 ;; esac
|
||||
if printf '%s' "$path" | LC_ALL=C grep -q '[[:cntrl:]]'; then return 1; fi
|
||||
path="$(printf '%s' "$path" | sed 's://*:/:g; s:/*$::')"
|
||||
[ -n "$path" ] || path="/"
|
||||
case "$path/" in */../*|*/./*) return 1 ;; esac
|
||||
while [ ! -d "$path" ]; do
|
||||
[ ! -e "$path" ] && [ ! -L "$path" ] || return 1
|
||||
part="${path##*/}"
|
||||
suffix="/$part$suffix"
|
||||
path="${path%/*}"
|
||||
[ -n "$path" ] || path="/"
|
||||
done
|
||||
path="$(CDPATH='' cd "$path" 2>/dev/null && pwd -P && printf '.')" || return 1
|
||||
path="${path%.}"
|
||||
path="${path%'
|
||||
'}"
|
||||
case "$path" in *'
|
||||
'*) return 1 ;; esac
|
||||
if printf '%s' "$path" | LC_ALL=C grep -q '[[:cntrl:]]'; then return 1; fi
|
||||
[ "$path" != "/" ] || [ -n "$suffix" ] || { printf '/\n'; return 0; }
|
||||
printf '%s\n' "${path%/}$suffix"
|
||||
}
|
||||
|
||||
openlist2_contains_path() {
|
||||
case "$2/" in "$1/"*) return 0 ;; esac
|
||||
return 1
|
||||
}
|
||||
|
||||
openlist2_broad_dir() {
|
||||
case "$1" in
|
||||
''|/|/bin|/boot|/dev|/etc|/home|/lib|/lib64|/media|/mnt|/opt|/overlay|/proc|/root|/rom|/run|/sbin|/srv|/sys|/tmp|/usr|/var|/www|/tmp/log|/tmp/run|/usr/*|/bin/*|/sbin/*|/lib/*|/lib64/*|/dev/*|/proc/*|/sys/*|/rom/*|/overlay/*|/etc/config|/etc/config/*|/etc/dropbear|/etc/dropbear/*|/etc/ssl|/etc/ssl/*|/tmp/log/*|/www/*)
|
||||
return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
}
|
||||
|
||||
openlist2_mountpoint() {
|
||||
[ -d "$1" ] || return 1
|
||||
if command -v mountpoint >/dev/null 2>&1; then
|
||||
mountpoint -q "$1"
|
||||
return $?
|
||||
fi
|
||||
awk -v target="$1" '
|
||||
{
|
||||
p = $5
|
||||
gsub(/\\040/, " ", p)
|
||||
gsub(/\\134/, "\\", p)
|
||||
if (p == target) found = 1
|
||||
}
|
||||
END { exit !found }
|
||||
' /proc/self/mountinfo
|
||||
}
|
||||
|
||||
openlist2_validate_dirs() {
|
||||
data_dir="$(openlist2_normalize_dir "$data_dir")" ||
|
||||
{ openlist2_error "Data directory must be an absolute directory path without dot segments."; return 1; }
|
||||
temp_dir="$(openlist2_normalize_dir "$temp_dir")" ||
|
||||
{ openlist2_error "Cache directory must be an absolute directory path without dot segments."; return 1; }
|
||||
bleve_dir="$(openlist2_normalize_dir "${bleve_dir:-$data_dir/bleve}")" ||
|
||||
{ openlist2_error "Invalid Bleve index directory."; return 1; }
|
||||
local path protected
|
||||
for path in "$data_dir" "$temp_dir" "$bleve_dir"; do
|
||||
if openlist2_broad_dir "$path" || openlist2_mountpoint "$path"; then
|
||||
openlist2_error "Use a dedicated subdirectory, not a system directory or mount root: $path"
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
case "$temp_dir" in
|
||||
/mnt/*|/media/*)
|
||||
path="${temp_dir#/}"; path="${path#*/}"
|
||||
case "$path" in */*) ;; *)
|
||||
openlist2_error "Use a cache subdirectory below the disk mount: $temp_dir"; return 1 ;;
|
||||
esac ;;
|
||||
esac
|
||||
if openlist2_contains_path "$temp_dir" "$data_dir" ||
|
||||
openlist2_contains_path "$bleve_dir" "$data_dir" ||
|
||||
openlist2_contains_path "$temp_dir" "$bleve_dir" ||
|
||||
openlist2_contains_path "$bleve_dir" "$temp_dir"; then
|
||||
openlist2_error "Cache and index directories must be separate and must not contain the data directory."
|
||||
return 1
|
||||
fi
|
||||
if [ -n "$dist_dir" ]; then
|
||||
dist_dir="$(openlist2_normalize_dir "$dist_dir")" || return 1
|
||||
if openlist2_broad_dir "$dist_dir" || openlist2_mountpoint "$dist_dir" ||
|
||||
openlist2_contains_path "$dist_dir" "$data_dir" ||
|
||||
openlist2_contains_path "$dist_dir" "$temp_dir" ||
|
||||
openlist2_contains_path "$temp_dir" "$dist_dir" ||
|
||||
openlist2_contains_path "$dist_dir" "$bleve_dir" ||
|
||||
openlist2_contains_path "$bleve_dir" "$dist_dir"; then
|
||||
openlist2_error "Frontend files must be separate from private data, cache and index files."
|
||||
return 1
|
||||
fi
|
||||
[ -f "$dist_dir/index.html" ] && [ -r "$dist_dir/index.html" ] ||
|
||||
{ openlist2_error "The frontend directory must contain a readable index.html."; return 1; }
|
||||
fi
|
||||
# shellcheck disable=SC2154
|
||||
for path in "$ssl_cert" "$ssl_key"; do
|
||||
[ -n "$path" ] || continue
|
||||
protected="$(readlink -f "$path" 2>/dev/null)" || protected="$path"
|
||||
if openlist2_contains_path "$temp_dir" "$protected" ||
|
||||
{ [ -n "$dist_dir" ] && openlist2_contains_path "$dist_dir" "$protected"; }; then
|
||||
openlist2_error "TLS files must not be inside the cache or public frontend directory."
|
||||
return 1
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
openlist2_prepare_dirs() {
|
||||
local entry identity previous registered marker="$data_dir/.temp-dir"
|
||||
umask 077
|
||||
mkdir -p "$data_dir" "$temp_dir" "$bleve_dir" ||
|
||||
{ openlist2_error "Unable to create data, cache or index directory."; return 1; }
|
||||
[ ! -L "$data_dir/config.json" ] && [ ! -L "$marker" ] ||
|
||||
{ openlist2_error "Config and cache ownership files must not be symlinks."; return 1; }
|
||||
[ ! -e "$data_dir/config.json" ] || [ -f "$data_dir/config.json" ] ||
|
||||
{ openlist2_error "config.json must be a regular file."; return 1; }
|
||||
[ ! -e "$marker" ] || [ -f "$marker" ] ||
|
||||
{ openlist2_error "Cache ownership marker must be a regular file."; return 1; }
|
||||
entry="$(find "$temp_dir" -mindepth 1 -maxdepth 1 -print -quit)" || return 1
|
||||
identity="$(stat -c '%d:%i' "$temp_dir")" || return 1
|
||||
registered="$(cat "$marker" 2>/dev/null)"
|
||||
if [ -n "$entry" ] && [ "$registered" != "$(printf '%s\n%s' "$temp_dir" "$identity")" ]; then
|
||||
# An existing config identifies the old cache on the first upgrade.
|
||||
# Once registered, an inode/device change must never inherit ownership.
|
||||
previous=""
|
||||
if [ ! -e "$marker" ] && [ -s "$data_dir/config.json" ]; then
|
||||
openlist2_read_scalar jsonfilter -qi "$data_dir/config.json" -e '@.temp_dir' 2>/dev/null
|
||||
previous="$OPENLIST2_VALUE"
|
||||
previous="$(openlist2_normalize_dir "$previous")" || previous=""
|
||||
fi
|
||||
[ "$previous" = "$temp_dir" ] ||
|
||||
{ openlist2_error "Refusing to clear a non-empty, unregistered cache directory. Choose a new empty subdirectory: $temp_dir"; return 1; }
|
||||
fi
|
||||
chmod 700 "$data_dir" "$temp_dir" "$bleve_dir" || return 1
|
||||
# Keep the marker outside the cache: OpenList clears cache contents.
|
||||
(
|
||||
local marker_tmp
|
||||
marker_tmp="$(mktemp "$data_dir/.temp-dir.XXXXXX")" || exit 1
|
||||
trap 'rm -f "$marker_tmp"' 0
|
||||
printf '%s\n%s\n' "$temp_dir" "$identity" > "$marker_tmp" &&
|
||||
chmod 600 "$marker_tmp" && mv -f "$marker_tmp" "$marker"
|
||||
) || { openlist2_error "Unable to record cache directory ownership."; return 1; }
|
||||
}
|
||||
|
||||
openlist2_migrate_config() {
|
||||
local prefix='openlist2.@openlist2[0]' old_port old_ssl http https changed=0 size backups schema legacy
|
||||
uci -q get "$prefix" >/dev/null || return 0
|
||||
old_port="$(uci -q get "$prefix.port")"
|
||||
old_ssl="$(uci -q get "$prefix.ssl")"
|
||||
http="$(uci -q get "$prefix.listen_http_port")"
|
||||
https="$(uci -q get "$prefix.listen_https_port")"
|
||||
if { [ -z "$http" ] || [ -z "$https" ]; } && { [ -n "$old_port" ] || [ -n "$old_ssl" ]; }; then
|
||||
old_port="$(openlist2_port "${old_port:-5244}")" ||
|
||||
{ openlist2_error "Invalid legacy listen port; migration stopped."; return 1; }
|
||||
case "$old_ssl" in 1|on|true|yes|enabled) http="${http:--1}"; https="${https:-$old_port}" ;; *)
|
||||
http="${http:-$old_port}"; https="${https:--1}" ;;
|
||||
esac
|
||||
uci set "$prefix.listen_http_port=$http" &&
|
||||
uci set "$prefix.listen_https_port=$https" || return 1
|
||||
changed=1
|
||||
fi
|
||||
if [ -n "$http" ] || [ -n "$https" ]; then
|
||||
legacy="$(openlist2_port "$old_port" 2>/dev/null)" || legacy=""
|
||||
if [ -n "$legacy" ]; then
|
||||
uci set "$prefix.legacy_firewall_port=$legacy" || return 1
|
||||
fi
|
||||
if [ -n "$old_port" ]; then uci -q delete "$prefix.port" || return 1; changed=1; fi
|
||||
if [ -n "$old_ssl" ]; then uci -q delete "$prefix.ssl" || return 1; changed=1; fi
|
||||
fi
|
||||
schema="$(uci -q get "$prefix.config_version")"
|
||||
if [ "$schema" != "2" ]; then
|
||||
size="$(uci -q get "$prefix.log_max_size")"
|
||||
backups="$(uci -q get "$prefix.log_max_backups")"
|
||||
case "$size:$backups" in
|
||||
50:30|10:5)
|
||||
uci set "$prefix.log_max_size=1" && uci set "$prefix.log_max_backups=3" || return 1 ;;
|
||||
esac
|
||||
# These switches have no implementation in OpenList 4.2.6.
|
||||
uci -q delete "$prefix.upload_task_persistant"
|
||||
uci -q delete "$prefix.decompress_upload_task_persistant"
|
||||
uci set "$prefix.config_version=2" || return 1
|
||||
changed=1
|
||||
fi
|
||||
# Retry a previously staged migration if its commit failed.
|
||||
if [ "$changed" = "1" ] || uci -q changes openlist2 | grep -q '\.config_version='; then
|
||||
uci commit openlist2 || { openlist2_error "Unable to save migrated UCI configuration."; return 1; }
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
openlist2_pasv_ports() {
|
||||
local values="$1" item first last result="" oldifs="$IFS" noglob=0
|
||||
case "$values" in *[!0-9,\ -]*) return 1 ;; esac
|
||||
case "$-" in *f*) noglob=1 ;; *) set -f ;; esac
|
||||
IFS=' ,'
|
||||
for item in $values; do
|
||||
IFS="$oldifs"
|
||||
first="${item%%-*}"; last="${item#*-}"
|
||||
first="$(openlist2_port "$first")" && last="$(openlist2_port "$last")" &&
|
||||
[ "$first" -ge 1024 ] && [ "$first" -le "$last" ] || {
|
||||
[ "$noglob" = "1" ] || set +f
|
||||
return 1
|
||||
}
|
||||
if [ "$first" = "$last" ]; then item="$first"; else item="$first-$last"; fi
|
||||
result="${result:+$result }$item"
|
||||
IFS=' ,'
|
||||
done
|
||||
IFS="$oldifs"
|
||||
[ "$noglob" = "1" ] || set +f
|
||||
printf '%s\n' "$result"
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
|
||||
config openlist2
|
||||
option config_version '2'
|
||||
option enabled '0'
|
||||
option debug '0'
|
||||
option delayed_start '0'
|
||||
@@ -15,10 +16,12 @@ config openlist2
|
||||
option tls_insecure_skip_verify '0'
|
||||
option data_dir '/etc/openlist2'
|
||||
option temp_dir '/tmp/openlist2'
|
||||
option bleve_dir ''
|
||||
option dist_dir ''
|
||||
option log '1'
|
||||
option log_path '/var/log/openlist2.log'
|
||||
option log_max_size '50'
|
||||
option log_max_backups '30'
|
||||
option log_max_size '1'
|
||||
option log_max_backups '3'
|
||||
option log_max_age '28'
|
||||
option log_compress '0'
|
||||
option log_filter '0'
|
||||
@@ -44,7 +47,6 @@ config openlist2
|
||||
option transfer_task_persistant '0'
|
||||
option upload_workers '5'
|
||||
option upload_max_retry '0'
|
||||
option upload_task_persistant '0'
|
||||
option copy_workers '5'
|
||||
option copy_max_retry '2'
|
||||
option copy_task_persistant '0'
|
||||
@@ -56,7 +58,6 @@ config openlist2
|
||||
option decompress_task_persistant '0'
|
||||
option decompress_upload_workers '5'
|
||||
option decompress_upload_max_retry '2'
|
||||
option decompress_upload_task_persistant '0'
|
||||
option allow_retry_canceled '0'
|
||||
option cors_allow_origins '*'
|
||||
option cors_allow_methods '*'
|
||||
@@ -66,6 +67,8 @@ config openlist2
|
||||
option s3_ssl '0'
|
||||
option ftp '0'
|
||||
option ftp_port '5221'
|
||||
# Listening ports matching the passive port mapping in OpenList's FTP settings.
|
||||
# Example: list ftp_pasv_ports '50000-50100'
|
||||
option find_pasv_port_attempts '50'
|
||||
option active_transfer_port_non_20 '0'
|
||||
option idle_timeout '900'
|
||||
|
||||
Executable
+4
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /usr/share/openlist2/functions.sh
|
||||
openlist2_migrate_config
|
||||
Executable
+46
@@ -0,0 +1,46 @@
|
||||
#!/bin/sh
|
||||
# The ACL grants individual verb names, never this executable with arbitrary arguments.
|
||||
|
||||
. /usr/share/openlist2/functions.sh
|
||||
umask 077
|
||||
|
||||
[ "$#" -eq 1 ] || { openlist2_error "Exactly one helper action is required."; exit 64; }
|
||||
|
||||
case "$1" in
|
||||
log-read|log-clear)
|
||||
openlist2_read_scalar uci -q get 'openlist2.@openlist2[0].log_path'
|
||||
log_path="$OPENLIST2_VALUE"
|
||||
log_path="${log_path:-/var/log/openlist2.log}"
|
||||
openlist2_safe_log_path "$log_path" ||
|
||||
{ openlist2_error "Unsafe log path or non-regular log file."; exit 1; }
|
||||
if [ "$1" = "log-read" ]; then
|
||||
[ -e "$log_path" ] || exit 0
|
||||
[ -r "$log_path" ] || { openlist2_error "Log file is not readable."; exit 1; }
|
||||
# Bound output before line selection, including logs with a very long line.
|
||||
log_data="$(tail -c 16383 "$log_path")" || exit 1
|
||||
printf '%s\n' "$log_data" | tail -n 200
|
||||
else
|
||||
[ -e "$log_path" ] || exit 0
|
||||
chmod 600 "$log_path" && : > "$log_path"
|
||||
fi
|
||||
;;
|
||||
password-reset)
|
||||
openlist2_read_scalar uci -q get 'openlist2.@openlist2[0].data_dir'
|
||||
data_dir="$OPENLIST2_VALUE"
|
||||
data_dir="$(openlist2_normalize_dir "${data_dir:-/etc/openlist2}")" ||
|
||||
{ openlist2_error "Invalid data directory."; exit 1; }
|
||||
if openlist2_broad_dir "$data_dir" || openlist2_mountpoint "$data_dir"; then
|
||||
openlist2_error "Use a dedicated data subdirectory."; exit 1
|
||||
fi
|
||||
[ ! -L "$data_dir/config.json" ] && [ -f "$data_dir/config.json" ] &&
|
||||
[ -s "$data_dir/config.json" ] ||
|
||||
{ openlist2_error "Start OpenList once with the saved settings before resetting the password."; exit 1; }
|
||||
jsonfilter -qi "$data_dir/config.json" -e '@' >/dev/null ||
|
||||
{ openlist2_error "Invalid config.json; password was not reset."; exit 1; }
|
||||
exec /usr/bin/openlist2 admin random --data "$data_dir" --no-prefix
|
||||
;;
|
||||
*)
|
||||
openlist2_error "Unsupported helper action."
|
||||
exit 64
|
||||
;;
|
||||
esac
|
||||
+212
-107
@@ -1,25 +1,15 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# UCI config_get assigns variables; rc.common consumes START and USE_PROCD.
|
||||
# The && chains deliberately handle failure from every command.
|
||||
# shellcheck disable=SC2034,SC2154,SC2015
|
||||
|
||||
. /usr/share/libubox/jshn.sh
|
||||
. /usr/share/openlist2/functions.sh
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
PROG=/usr/bin/openlist2
|
||||
|
||||
is_safe_log_path() {
|
||||
local path="$1"
|
||||
local base="${path##*/}"
|
||||
|
||||
case "$path" in
|
||||
/var/log/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
[ "$path" = "/var/log/$base" ] || return 1
|
||||
|
||||
printf '%s\n' "$base" | grep -Eq '^openlist2([._-][A-Za-z0-9._-]+)?$'
|
||||
}
|
||||
|
||||
append_dest_port() {
|
||||
local value="$1"
|
||||
|
||||
@@ -65,19 +55,24 @@ json_add_csv_array() {
|
||||
}
|
||||
|
||||
get_config() {
|
||||
# LuCI manages the first section and procd runs one instance.
|
||||
[ -z "$section_id" ] || return 0
|
||||
section_id="$1"
|
||||
uci_changed=0
|
||||
|
||||
config_get_bool enabled "$1" enabled 1
|
||||
config_get debug "$1" debug 0
|
||||
config_get allow_wan "$1" allow_wan 0
|
||||
config_get_bool debug "$1" debug 0
|
||||
config_get_bool allow_wan "$1" allow_wan 0
|
||||
config_get delayed_start "$1" delayed_start 0
|
||||
|
||||
config_get force "$1" force 1
|
||||
config_get_bool force "$1" force 1
|
||||
config_get site_url "$1" site_url ""
|
||||
config_get cdn "$1" cdn ""
|
||||
config_get jwt_secret "$1" jwt_secret ""
|
||||
config_get data_dir "$1" data_dir "/etc/openlist2"
|
||||
config_get temp_dir "$1" temp_dir "/tmp/openlist2"
|
||||
config_get bleve_dir "$1" bleve_dir ""
|
||||
config_get dist_dir "$1" dist_dir ""
|
||||
config_get token_expires_in "$1" token_expires_in 48
|
||||
config_get proxy_address "$1" proxy_address ""
|
||||
config_get auto_memory_limit "$1" auto_memory_limit 4
|
||||
@@ -85,27 +80,27 @@ get_config() {
|
||||
config_get max_block_limit "$1" max_block_limit 0
|
||||
config_get max_connections "$1" max_connections 0
|
||||
config_get max_concurrency "$1" max_concurrency 64
|
||||
config_get tls_insecure_skip_verify "$1" tls_insecure_skip_verify 0
|
||||
config_get_bool tls_insecure_skip_verify "$1" tls_insecure_skip_verify 0
|
||||
|
||||
# log
|
||||
config_get log "$1" log 1
|
||||
config_get_bool log "$1" log 1
|
||||
config_get log_path "$1" log_path '/var/log/openlist2.log'
|
||||
if ! is_safe_log_path "$log_path"; then
|
||||
if ! openlist2_safe_log_path "$log_path"; then
|
||||
log_path='/var/log/openlist2.log'
|
||||
uci -q set openlist2.@openlist2[0].log_path="$log_path"
|
||||
uci -q set "openlist2.$section_id.log_path=$log_path" || { config_error=1; return 1; }
|
||||
uci_changed=1
|
||||
fi
|
||||
config_get log_max_size "$1" log_max_size 50
|
||||
config_get log_max_backups "$1" log_max_backups 30
|
||||
config_get log_max_size "$1" log_max_size 1
|
||||
config_get log_max_backups "$1" log_max_backups 3
|
||||
config_get log_max_age "$1" log_max_age 28
|
||||
config_get log_compress "$1" log_compress 0
|
||||
config_get log_filter "$1" log_filter 0
|
||||
config_get_bool log_compress "$1" log_compress 0
|
||||
config_get_bool log_filter "$1" log_filter 0
|
||||
config_get log_filter_cidr "$1" log_filter_cidr ""
|
||||
config_get log_filter_path "$1" log_filter_path ""
|
||||
config_get log_filter_method "$1" log_filter_method ""
|
||||
|
||||
# scheme
|
||||
config_get force_https "$1" force_https 0
|
||||
config_get_bool force_https "$1" force_https 0
|
||||
config_get ssl_cert "$1" ssl_cert ""
|
||||
config_get ssl_key "$1" ssl_key ""
|
||||
config_get listen_addr "$1" listen_addr "0.0.0.0"
|
||||
@@ -113,8 +108,8 @@ get_config() {
|
||||
config_get listen_https_port "$1" listen_https_port "-1"
|
||||
config_get listen_unix_file "$1" listen_unix_file ""
|
||||
config_get listen_unix_file_perm "$1" listen_unix_file_perm ""
|
||||
config_get listen_enable_h2c "$1" listen_enable_h2c 0
|
||||
config_get listen_enable_h3 "$1" listen_enable_h3 0
|
||||
config_get_bool listen_enable_h2c "$1" listen_enable_h2c 0
|
||||
config_get_bool listen_enable_h3 "$1" listen_enable_h3 0
|
||||
|
||||
# database
|
||||
config_get database_type "$1" database_type "sqlite3"
|
||||
@@ -137,26 +132,24 @@ get_config() {
|
||||
# tasks
|
||||
config_get download_workers "$1" download_workers 5
|
||||
config_get download_max_retry "$1" download_max_retry 1
|
||||
config_get download_task_persistant "$1" download_task_persistant 0
|
||||
config_get_bool download_task_persistant "$1" download_task_persistant 0
|
||||
config_get transfer_workers "$1" transfer_workers 5
|
||||
config_get transfer_max_retry "$1" transfer_max_retry 2
|
||||
config_get transfer_task_persistant "$1" transfer_task_persistant 0
|
||||
config_get_bool transfer_task_persistant "$1" transfer_task_persistant 0
|
||||
config_get upload_workers "$1" upload_workers 5
|
||||
config_get upload_max_retry "$1" upload_max_retry 0
|
||||
config_get upload_task_persistant "$1" upload_task_persistant 0
|
||||
config_get copy_workers "$1" copy_workers 5
|
||||
config_get copy_max_retry "$1" copy_max_retry 2
|
||||
config_get copy_task_persistant "$1" copy_task_persistant 0
|
||||
config_get_bool copy_task_persistant "$1" copy_task_persistant 0
|
||||
config_get move_workers "$1" move_workers 5
|
||||
config_get move_max_retry "$1" move_max_retry 2
|
||||
config_get move_task_persistant "$1" move_task_persistant 0
|
||||
config_get_bool move_task_persistant "$1" move_task_persistant 0
|
||||
config_get decompress_workers "$1" decompress_workers 5
|
||||
config_get decompress_max_retry "$1" decompress_max_retry 2
|
||||
config_get decompress_task_persistant "$1" decompress_task_persistant 0
|
||||
config_get_bool decompress_task_persistant "$1" decompress_task_persistant 0
|
||||
config_get decompress_upload_workers "$1" decompress_upload_workers 5
|
||||
config_get decompress_upload_max_retry "$1" decompress_upload_max_retry 2
|
||||
config_get decompress_upload_task_persistant "$1" decompress_upload_task_persistant 0
|
||||
config_get allow_retry_canceled "$1" allow_retry_canceled 0
|
||||
config_get_bool allow_retry_canceled "$1" allow_retry_canceled 0
|
||||
|
||||
# cors
|
||||
config_get cors_allow_origins "$1" cors_allow_origins '*'
|
||||
@@ -164,96 +157,205 @@ get_config() {
|
||||
config_get cors_allow_headers "$1" cors_allow_headers '*'
|
||||
|
||||
# s3
|
||||
config_get s3 "$1" s3 0
|
||||
config_get_bool s3 "$1" s3 0
|
||||
config_get s3_port "$1" s3_port 5246
|
||||
config_get s3_ssl "$1" s3_ssl 0
|
||||
config_get_bool s3_ssl "$1" s3_ssl 0
|
||||
|
||||
# ftp
|
||||
config_get ftp "$1" ftp 0
|
||||
config_get_bool ftp "$1" ftp 0
|
||||
config_get ftp_port "$1" ftp_port 5221
|
||||
config_get ftp_pasv_ports "$1" ftp_pasv_ports ""
|
||||
config_get find_pasv_port_attempts "$1" find_pasv_port_attempts 50
|
||||
config_get active_transfer_port_non_20 "$1" active_transfer_port_non_20 0
|
||||
config_get_bool active_transfer_port_non_20 "$1" active_transfer_port_non_20 0
|
||||
config_get idle_timeout "$1" idle_timeout 900
|
||||
config_get connection_timeout "$1" connection_timeout 30
|
||||
config_get disable_active_mode "$1" disable_active_mode 0
|
||||
config_get default_transfer_binary "$1" default_transfer_binary 0
|
||||
config_get enable_active_conn_ip_check "$1" enable_active_conn_ip_check 1
|
||||
config_get enable_pasv_conn_ip_check "$1" enable_pasv_conn_ip_check 1
|
||||
config_get_bool disable_active_mode "$1" disable_active_mode 0
|
||||
config_get_bool default_transfer_binary "$1" default_transfer_binary 0
|
||||
config_get_bool enable_active_conn_ip_check "$1" enable_active_conn_ip_check 1
|
||||
config_get_bool enable_pasv_conn_ip_check "$1" enable_pasv_conn_ip_check 1
|
||||
|
||||
# sftp
|
||||
config_get sftp "$1" sftp 0
|
||||
config_get_bool sftp "$1" sftp 0
|
||||
config_get sftp_port "$1" sftp_port 5222
|
||||
|
||||
# mcp
|
||||
config_get mcp "$1" mcp 0
|
||||
config_get_bool mcp "$1" mcp 0
|
||||
|
||||
# init jwt_secret
|
||||
if [ -z "$jwt_secret" ]; then
|
||||
jwt_secret=$(tr -cd "a-zA-Z0-9" < "/dev/urandom" | head -c16)
|
||||
uci -q set openlist2.@openlist2[0].jwt_secret="$jwt_secret"
|
||||
[ "${#jwt_secret}" -eq 16 ] || { config_error=1; return 1; }
|
||||
uci -q set "openlist2.$section_id.jwt_secret=$jwt_secret" || { config_error=1; return 1; }
|
||||
uci_changed=1
|
||||
fi
|
||||
[ "$uci_changed" = "1" ] && uci commit openlist2
|
||||
if [ "$uci_changed" = "1" ]; then
|
||||
uci commit openlist2 || { config_error=1; return 1; }
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
validate_config() {
|
||||
http_port="$(openlist2_port "${listen_http_port:-5244}" 1)" &&
|
||||
https_port="$(openlist2_port "${listen_https_port:--1}" 1)" &&
|
||||
s3_port="$(openlist2_port "$s3_port")" &&
|
||||
ftp_port="$(openlist2_port "$ftp_port")" &&
|
||||
sftp_port="$(openlist2_port "$sftp_port")" ||
|
||||
{ openlist2_error "Invalid listen port. Use 1-65535, or -1 to disable HTTP/HTTPS."; return 1; }
|
||||
if [ "$ftp" = "1" ] && [ "$allow_wan" = "1" ]; then
|
||||
ftp_pasv_ports="$(openlist2_pasv_ports "$ftp_pasv_ports")" ||
|
||||
{ openlist2_error "FTP passive firewall ports must be ports or ranges within 1024-65535."; return 1; }
|
||||
else
|
||||
ftp_pasv_ports=""
|
||||
fi
|
||||
if [ "$https_port" = "-1" ] && { [ "$listen_enable_h3" = "1" ] || [ "$force_https" = "1" ]; }; then
|
||||
openlist2_error "Force HTTPS and HTTP/3 require an enabled HTTPS port."
|
||||
return 1
|
||||
fi
|
||||
if [ "$https_port" != "-1" ] || { [ "$s3" = "1" ] && [ "$s3_ssl" = "1" ]; }; then
|
||||
local file
|
||||
for file in "$ssl_cert" "$ssl_key"; do
|
||||
case "$file" in /*) ;; *)
|
||||
openlist2_error "HTTPS/S3 TLS requires absolute certificate and key paths."; return 1 ;;
|
||||
esac
|
||||
[ -f "$file" ] && [ -r "$file" ] && [ -s "$file" ] ||
|
||||
{ openlist2_error "TLS certificate/key is missing, empty or unreadable: $file"; return 1; }
|
||||
done
|
||||
fi
|
||||
if [ -n "$listen_unix_file" ]; then
|
||||
case "$listen_unix_file" in /*) ;; *)
|
||||
openlist2_error "Unix socket path must be absolute."; return 1 ;;
|
||||
esac
|
||||
[ ! -e "$listen_unix_file" ] || [ -S "$listen_unix_file" ] ||
|
||||
{ openlist2_error "Unix socket path is occupied by a non-socket file."; return 1; }
|
||||
fi
|
||||
if [ -n "$listen_unix_file_perm" ]; then
|
||||
printf '%s\n' "$listen_unix_file_perm" | grep -Eq '^[0-7]{3,4}$' ||
|
||||
{ openlist2_error "Unix socket permissions must contain 3 or 4 octal digits."; return 1; }
|
||||
fi
|
||||
local ports="" port item first last
|
||||
dest_ports=""
|
||||
append_dest_port "$http_port"
|
||||
append_dest_port "$https_port"
|
||||
[ "$s3" = "1" ] && append_dest_port "$s3_port"
|
||||
[ "$ftp" = "1" ] && append_dest_port "$ftp_port"
|
||||
[ "$sftp" = "1" ] && append_dest_port "$sftp_port"
|
||||
for port in $dest_ports; do
|
||||
case " $ports " in *" $port "*)
|
||||
openlist2_error "Enabled services cannot share TCP port $port."; return 1 ;;
|
||||
esac
|
||||
ports="$ports $port"
|
||||
if [ "$ftp" = "1" ]; then
|
||||
for item in $ftp_pasv_ports; do
|
||||
first="${item%%-*}"; last="${item#*-}"
|
||||
if [ "$port" -ge "$first" ] && [ "$port" -le "$last" ]; then
|
||||
openlist2_error "FTP passive ports must not overlap service port $port."; return 1
|
||||
fi
|
||||
done
|
||||
fi
|
||||
done
|
||||
if [ "$log" = "1" ] && ! [ "$log_max_size" -ge 1 ] 2>/dev/null; then
|
||||
openlist2_error "Log size must be at least 1 MB."
|
||||
return 1
|
||||
fi
|
||||
openlist2_validate_dirs
|
||||
}
|
||||
|
||||
write_config() (
|
||||
local config_tmp
|
||||
umask 077
|
||||
config_tmp="$(mktemp "$data_dir/.config.json.XXXXXX")" || exit 1
|
||||
trap 'rm -f "$config_tmp"' 0
|
||||
trap 'exit 1' HUP INT TERM
|
||||
json_dump > "$config_tmp" &&
|
||||
jsonfilter -qi "$config_tmp" -e '@' >/dev/null &&
|
||||
chmod 600 "$config_tmp" &&
|
||||
mv -f "$config_tmp" "$data_dir/config.json"
|
||||
)
|
||||
|
||||
set_firewall_rule() {
|
||||
local section="$1" name="$2" proto="$3" ports="$4"
|
||||
if [ -z "$ports" ]; then
|
||||
if uci -q get "firewall.$section" >/dev/null; then
|
||||
uci -q delete "firewall.$section" || return 1
|
||||
firewall_changed=1
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
if [ "$(uci -q get "firewall.$section")" = "rule" ] &&
|
||||
[ "$(uci -q get "firewall.$section.name")" = "$name" ] &&
|
||||
[ "$(uci -q get "firewall.$section.src")" = "wan" ] &&
|
||||
[ "$(uci -q get "firewall.$section.target")" = "ACCEPT" ] &&
|
||||
[ "$(uci -q get "firewall.$section.proto")" = "$proto" ] &&
|
||||
[ "$(uci -q get "firewall.$section.dest_port")" = "$ports" ] &&
|
||||
[ "$(uci -q get "firewall.$section.enabled")" = "1" ]; then
|
||||
return 0
|
||||
fi
|
||||
uci -q delete "firewall.$section"
|
||||
uci set "firewall.$section=rule" &&
|
||||
uci set "firewall.$section.name=$name" &&
|
||||
uci set "firewall.$section.target=ACCEPT" &&
|
||||
uci set "firewall.$section.src=wan" &&
|
||||
uci set "firewall.$section.proto=$proto" &&
|
||||
uci set "firewall.$section.dest_port=$ports" &&
|
||||
uci set "firewall.$section.enabled=1" || return 1
|
||||
firewall_changed=1
|
||||
}
|
||||
|
||||
set_firewall() {
|
||||
local quic_ports="" legacy legacy_ports
|
||||
firewall_changed=0
|
||||
legacy="$(uci -q get 'openlist2.@openlist2[0].legacy_firewall_port')"
|
||||
legacy_ports="$(uci -q get firewall.openlist.dest_port)"
|
||||
# Only retire an old rule when the migrated config and rule fingerprint agree.
|
||||
if [ -n "$legacy" ] &&
|
||||
[ "$(uci -q get firewall.openlist.name)" = "openlist" ] &&
|
||||
[ "$(uci -q get firewall.openlist.src)" = "wan" ] &&
|
||||
[ "$(uci -q get firewall.openlist.target)" = "ACCEPT" ] &&
|
||||
[ "$(uci -q get firewall.openlist.proto)" = "tcp" ]; then
|
||||
case " $legacy_ports " in *" $legacy "*)
|
||||
uci -q delete firewall.openlist || return 1
|
||||
firewall_changed=1 ;;
|
||||
esac
|
||||
fi
|
||||
dest_ports=""
|
||||
if [ "$external_access" = "allow" ]; then
|
||||
dest_ports=""
|
||||
append_dest_port "$http_port"
|
||||
append_dest_port "$https_port"
|
||||
[ "$s3" = "1" ] && append_dest_port "$s3_port"
|
||||
[ "$ftp" = "1" ] && append_dest_port "$ftp_port"
|
||||
if [ "$ftp" = "1" ]; then
|
||||
append_dest_port "$ftp_port"
|
||||
append_dest_port "$ftp_pasv_ports"
|
||||
fi
|
||||
[ "$sftp" = "1" ] && append_dest_port "$sftp_port"
|
||||
|
||||
uci -q delete firewall.openlist
|
||||
uci -q delete firewall.openlist2
|
||||
uci -q delete firewall.openlist2_quic
|
||||
if [ -z "$dest_ports" ]; then
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
return 0
|
||||
fi
|
||||
|
||||
uci set firewall.openlist2=rule
|
||||
uci set firewall.openlist2.name="openlist2"
|
||||
uci set firewall.openlist2.target="ACCEPT"
|
||||
uci set firewall.openlist2.src="wan"
|
||||
uci set firewall.openlist2.proto="tcp"
|
||||
uci set firewall.openlist2.dest_port="$dest_ports"
|
||||
uci set firewall.openlist2.enabled="1"
|
||||
if [ "$listen_enable_h3" = "1" ] && [ -n "$https_port" ] && [ "$https_port" != "-1" ]; then
|
||||
uci set firewall.openlist2_quic=rule
|
||||
uci set firewall.openlist2_quic.name="openlist2 QUIC"
|
||||
uci set firewall.openlist2_quic.target="ACCEPT"
|
||||
uci set firewall.openlist2_quic.src="wan"
|
||||
uci set firewall.openlist2_quic.proto="udp"
|
||||
uci set firewall.openlist2_quic.dest_port="$https_port"
|
||||
uci set firewall.openlist2_quic.enabled="1"
|
||||
fi
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
elif [ "$external_access" = "deny" ]; then
|
||||
uci -q delete firewall.openlist
|
||||
uci -q delete firewall.openlist2
|
||||
uci -q delete firewall.openlist2_quic
|
||||
uci commit firewall
|
||||
/etc/init.d/firewall reload >/dev/null 2>&1
|
||||
[ "$listen_enable_h3" = "1" ] && quic_ports="$https_port"
|
||||
fi
|
||||
set_firewall_rule openlist2 openlist2 tcp "$dest_ports" &&
|
||||
set_firewall_rule openlist2_quic "openlist2 QUIC" udp "$quic_ports" || return 1
|
||||
if [ "$firewall_changed" = "1" ] || uci -q changes firewall | grep -Eq '^[-+]?firewall\.openlist'; then
|
||||
uci commit firewall && /etc/init.d/firewall reload || {
|
||||
openlist2_error "Unable to apply OpenList firewall rules."
|
||||
return 1
|
||||
}
|
||||
fi
|
||||
if [ -n "$legacy" ]; then
|
||||
uci -q delete 'openlist2.@openlist2[0].legacy_firewall_port' && uci commit openlist2 || return 1
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load openlist2
|
||||
openlist2_migrate_config || return 1
|
||||
config_load openlist2 || return 1
|
||||
section_id=""
|
||||
config_error=0
|
||||
config_foreach get_config openlist2
|
||||
|
||||
# Remove firewall rules when stopping openlist process via uci
|
||||
external_access="deny" set_firewall
|
||||
|
||||
[ "$enabled" -ne 1 ] && return 1
|
||||
mkdir -p "$temp_dir" "$data_dir"
|
||||
[ -n "$listen_http_port" ] || listen_http_port="5244"
|
||||
[ -n "$listen_https_port" ] || listen_https_port="-1"
|
||||
http_port="$listen_http_port"
|
||||
https_port="$listen_https_port"
|
||||
[ -n "$section_id" ] && [ "$config_error" = "0" ] ||
|
||||
{ openlist2_error "Unable to load or save OpenList configuration."; return 1; }
|
||||
if [ "$enabled" != "1" ]; then
|
||||
external_access="deny" set_firewall
|
||||
return $?
|
||||
fi
|
||||
validate_config && openlist2_prepare_dirs || return 1
|
||||
if [ -e /proc/uptime ]; then
|
||||
boot_uptime="$(awk -F. '{print $1}' /proc/uptime)"
|
||||
[ "$boot_uptime" -lt "120" ] || delayed_start=0
|
||||
@@ -261,11 +363,13 @@ start_service() {
|
||||
|
||||
[ "$allow_wan" -eq "1" ] && external_access="allow" || external_access="deny"
|
||||
|
||||
set_firewall
|
||||
|
||||
log_dir="${log_path%/*}"
|
||||
[ "$log_dir" != "$log_path" ] && mkdir -p "$log_dir"
|
||||
: > "$log_path"
|
||||
if [ "$log" = "1" ]; then
|
||||
openlist2_safe_log_path "$log_path" ||
|
||||
{ openlist2_error "Unsafe log path or non-regular log file."; return 1; }
|
||||
log_dir="${log_path%/*}"
|
||||
mkdir -p "$log_dir" && : >> "$log_path" && chmod 600 "$log_path" ||
|
||||
{ openlist2_error "Unable to open the log file."; return 1; }
|
||||
fi
|
||||
|
||||
last_launched_version=""
|
||||
if command -v jsonfilter >/dev/null 2>&1 && [ -s "$data_dir/config.json" ]; then
|
||||
@@ -316,8 +420,8 @@ start_service() {
|
||||
json_close_object
|
||||
|
||||
json_add_string "temp_dir" "$temp_dir"
|
||||
json_add_string "bleve_dir" "$data_dir/bleve"
|
||||
json_add_string "dist_dir" ""
|
||||
json_add_string "bleve_dir" "$bleve_dir"
|
||||
json_add_string "dist_dir" "$dist_dir"
|
||||
|
||||
# log
|
||||
json_add_object "log"
|
||||
@@ -363,7 +467,7 @@ start_service() {
|
||||
json_add_object "upload"
|
||||
json_add_int "workers" "$upload_workers"
|
||||
json_add_int "max_retry" "$upload_max_retry"
|
||||
json_add_boolean "task_persistant" "$upload_task_persistant"
|
||||
json_add_boolean "task_persistant" 0
|
||||
json_close_object
|
||||
json_add_object "copy"
|
||||
json_add_int "workers" "$copy_workers"
|
||||
@@ -383,7 +487,7 @@ start_service() {
|
||||
json_add_object "decompress_upload"
|
||||
json_add_int "workers" "$decompress_upload_workers"
|
||||
json_add_int "max_retry" "$decompress_upload_max_retry"
|
||||
json_add_boolean "task_persistant" "$decompress_upload_task_persistant"
|
||||
json_add_boolean "task_persistant" 0
|
||||
json_close_object
|
||||
json_add_boolean "allow_retry_canceled" "$allow_retry_canceled"
|
||||
json_close_object
|
||||
@@ -430,7 +534,8 @@ start_service() {
|
||||
json_add_string "last_launched_version" "$last_launched_version"
|
||||
json_add_string "proxy_address" "$proxy_address"
|
||||
|
||||
json_dump > "$data_dir/config.json"
|
||||
write_config || { openlist2_error "Unable to atomically save config.json; service was not started."; return 1; }
|
||||
set_firewall || return 1
|
||||
|
||||
procd_open_instance openlist2
|
||||
procd_set_param env OPENLIST_ADMIN_PASSWORD=admin
|
||||
@@ -439,7 +544,8 @@ start_service() {
|
||||
procd_append_param command --data "$data_dir"
|
||||
procd_append_param command --no-prefix
|
||||
[ "$debug" = "1" ] && procd_append_param command --debug
|
||||
[ "$debug" = "1" ] && procd_set_param stderr 1
|
||||
procd_set_param stderr 1
|
||||
[ "$debug" = "1" ] && procd_set_param stdout 1
|
||||
procd_set_param respawn
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="200000 200000"
|
||||
@@ -453,7 +559,6 @@ service_triggers() {
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
return 0
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
Subject: Retire the obsolete OpenWrt polyfill shim without changing custom HTML
|
||||
|
||||
--- a/internal/bootstrap/data/setting.go
|
||||
+++ b/internal/bootstrap/data/setting.go
|
||||
@@ -70,6 +70,11 @@ func initSettings() {
|
||||
continue
|
||||
}
|
||||
}
|
||||
+ // Retire only the exact polyfill shim shipped by old OpenWrt packages.
|
||||
+ if item.Key == conf.CustomizeHead && stored != nil &&
|
||||
+ stored.Value == "<script src=\"/assets/polyfill.min.js?features=String.prototype.replaceAll\"></script>" {
|
||||
+ migrationValue = stored.Value
|
||||
+ }
|
||||
if item.Key != conf.VERSION && stored != nil &&
|
||||
(len(migrationValue) == 0 || stored.Value != migrationValue) {
|
||||
item.Value = stored.Value
|
||||
+3
-3
@@ -8,12 +8,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=torrserver
|
||||
PKG_VERSION:=MatriX.144.3
|
||||
PKG_RELEASE:=22
|
||||
PKG_VERSION:=MatriX.144.4
|
||||
PKG_RELEASE:=23
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/YouROK/TorrServer.git
|
||||
PKG_SOURCE_VERSION:=80f50702c1e852dcad59cdd1399ac8aa7a092fee
|
||||
PKG_SOURCE_VERSION:=215f6fcaa84685fc0263584748206fca0810a8b6
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_MAINTAINER:=Konstantine Shevlakov <shevlakov@132lan.ru>
|
||||
|
||||
+21
-7
@@ -10,7 +10,7 @@
|
||||
# qmi_backend)
|
||||
# wwand-ncm - NCM/ECM backend (DEPENDS wwand)
|
||||
# wwand-mhi - PCIe/MHI transport + drivers (DEPENDS wwand; add a backend)
|
||||
# wwand-esim - eSIM management (DEPENDS wwand-qmi + wwand-lpac)
|
||||
# wwand-esim - eSIM management (DEPENDS wwand + lpac; no backend)
|
||||
# A typical QMI router installs `wwand-qmi` (which pulls in `wwand`); add
|
||||
# `wwand-mbim` / `wwand-ncm` for those modems, `wwand-mhi` for a PCIe/MHI modem.
|
||||
# The ucode tree ships PRECOMPILED to bytecode (repo-root CMakeLists.txt,
|
||||
@@ -49,12 +49,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=wwand
|
||||
PKG_VERSION:=1.6.6_p22
|
||||
PKG_RELEASE:=34
|
||||
PKG_VERSION:=1.6.6_p28
|
||||
PKG_RELEASE:=36
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
|
||||
PKG_SOURCE_VERSION:=74c78019c425c6cbfb3938885046419e4a2ed9ba
|
||||
PKG_SOURCE_VERSION:=c1a1b0ca732b09c6114d65f9aaa73d7ece02535c
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
@@ -200,6 +200,11 @@ define Package/wwand/install
|
||||
# /etc/config/network now (existing files survive upgrade and are still read).
|
||||
# files/wwand.config is kept in the source tree only as a documented example.
|
||||
$(INSTALL_DIR) $(1)/etc/hotplug.d/usbmisc
|
||||
$(INSTALL_DIR) $(1)/usr/share/acl.d
|
||||
# ubus ACL: lets an unprivileged reader (collectd's exec plugin refuses to
|
||||
# run as root) fetch telemetry. Deliberately NOT `status` — that carries
|
||||
# iccid/imsi/imei/msisdn and a ubus ACL cannot filter a result.
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/wwand-acl.json $(1)/usr/share/acl.d/wwand.json
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/files/wwand.hotplug $(1)/etc/hotplug.d/usbmisc/20-wwand
|
||||
# net hotplug: NCM modems (no cdc-wdm) + re-enumeration after a mode switch
|
||||
$(INSTALL_DIR) $(1)/etc/hotplug.d/net
|
||||
@@ -393,9 +398,18 @@ define Package/wwand-esim
|
||||
# lpac, or our self-contained static wwand-lpac (bundled wolfSSL+libcurl, no
|
||||
# OpenSSL/mbedtls .so — it PROVIDES lpac, so it satisfies +lpac too). lpac
|
||||
# drives the SM-DP+ download/notification (ES9+ HTTPS on the router); the
|
||||
# daemon bridges its stdio APDU protocol inline. APDU access uses the modem
|
||||
# UIM channel, so it also needs the QMI backend.
|
||||
DEPENDS:=+wwand-qmi +lpac
|
||||
# daemon bridges its stdio APDU protocol inline.
|
||||
#
|
||||
# NO BACKEND DEPENDENCY. This carried +wwand-qmi from the days when the QMI
|
||||
# UIM logical channel was the only APDU transport. It is not any more: sim.uc
|
||||
# probes native MBIM UICC -> QMI UIM -> AT (CCHO/CGLA), and all three live in
|
||||
# the base package, so an NCM-only or MBIM-only box can manage profiles with
|
||||
# no QMI backend installed at all. The stale dependency was not free — it made
|
||||
# wwand-esim uninstallable wherever wwand-qmi's kmods do not exist as
|
||||
# packages, e.g. a build whose QMI drivers are compiled in
|
||||
# (ddimension/wwand#26). tools/check-packaging.py now fails on a DEPENDS that
|
||||
# nothing imports, so the next one cannot sit unnoticed.
|
||||
DEPENDS:=+wwand +lpac
|
||||
endef
|
||||
|
||||
define Package/wwand-esim/description
|
||||
|
||||
Reference in New Issue
Block a user