🎨 Sync 2026-08-09 00:31:16

This commit is contained in:
github-actions[bot] committed 2026-08-09 00:31:16 +08:00
1 parent 5d62494863
commit ee62266d6b
117 files changed
+5152 -2651

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=airconnect
PKG_VERSION:=1.11.1
PKG_VERSION:=1.11.2
PKG_RELEASE=1
PKG_SOURCE:=AirConnect-$(PKG_VERSION).zip
+2 -2
View File
@@ -6,9 +6,9 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=dae
PKG_VERSION:=2026.08.05
PKG_RELEASE:=32
PKG_RELEASE:=33
PKG_SOURCE:=dae-src-2026.08.05-d2174ac8b937.tar.gz
PKG_SOURCE:=dae-src-2026.08.05-479f43645df5.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/dae-src
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=skip
+5 -2
View File
@@ -6,9 +6,9 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=daed
PKG_VERSION:=2026.08.05
PKG_RELEASE:=43
PKG_RELEASE:=44
PKG_SOURCE:=daed-src-2026.08.05-4cab0ccb366a.tar.gz
PKG_SOURCE:=daed-src-2026.08.05-c389546b8c1f.tar.gz
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
PKG_HASH:=skip
@@ -142,6 +142,9 @@ define Package/daed/install
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) $(CURDIR)/files/daed.init $(1)/etc/init.d/daed
$(INSTALL_DIR) $(1)/lib/upgrade/keep.d
$(INSTALL_DATA) $(CURDIR)/files/daed.keep $(1)/lib/upgrade/keep.d/daed
endef
$(eval $(call GoBinPackage,daed))
+1
View File
@@ -0,0 +1 @@
/etc/daed/wing.db-wal
@@ -1,6 +1,4 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: kenzok8 <kenzok8@gmail.com>
Date: Tue, 21 Jul 2026 02:53:42 +0800
Subject: [PATCH] daed: strictly reconcile subscription updates
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
@@ -0,0 +1,73 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
Subject: [PATCH] daed: open sqlite database in WAL mode
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
订阅自动更新写库时,WebUI 登录查询被锁 5 秒后报 database is locked。
开 WAL 并把 busy_timeout 提到 30 秒,读不再被写事务阻塞;WAL 附带的两个文件跟主库一样收紧到 0640。
---
db/db.go | 17 ++++++++++++-----
pkg/sqlite/sqlite_mipsarch.go | 4 +++-
pkg/sqlite/sqlite_others.go | 4 +++-
3 files changed, 18 insertions(+), 7 deletions(-)
diff --git a/db/db.go b/db/db.go
index a79022b..6145443 100644
--- a/db/db.go
+++ b/db/db.go
@@ -50,13 +50,20 @@ func InitDatabase(configDir string) (err error) {
); err != nil {
return err
}
- if fi, err := os.Stat(path); err != nil {
- return err
- } else if fi.Mode()&0037 > 0 {
- // Too open, chmod it to 0640.
- if err = os.Chmod(path, 0640); err != nil {
+ for _, p := range []string{path, path + "-wal", path + "-shm"} {
+ fi, err := os.Stat(p)
+ if os.IsNotExist(err) {
+ continue
+ }
+ if err != nil {
return err
}
+ if fi.Mode()&0037 > 0 {
+ // Too open, chmod it to 0640.
+ if err = os.Chmod(p, 0640); err != nil {
+ return err
+ }
+ }
}
return nil
diff --git a/pkg/sqlite/sqlite_mipsarch.go b/pkg/sqlite/sqlite_mipsarch.go
index 7b8c1b4..8c275e5 100644
--- a/pkg/sqlite/sqlite_mipsarch.go
+++ b/pkg/sqlite/sqlite_mipsarch.go
@@ -10,6 +10,8 @@ import (
"gorm.io/gorm"
)
+const pragmas = "?_journal_mode=WAL&_busy_timeout=30000&_synchronous=FULL"
+
func Open(dsn string) gorm.Dialector {
- return sqlite.Open(dsn)
+ return sqlite.Open(dsn + pragmas)
}
diff --git a/pkg/sqlite/sqlite_others.go b/pkg/sqlite/sqlite_others.go
index 4598651..83d8b80 100644
--- a/pkg/sqlite/sqlite_others.go
+++ b/pkg/sqlite/sqlite_others.go
@@ -7,6 +7,8 @@ import (
"gorm.io/gorm"
)
+const pragmas = "?_pragma=journal_mode(WAL)&_pragma=busy_timeout(30000)&_pragma=synchronous(FULL)"
+
func Open(dsn string) gorm.Dialector {
- return sqlite.Open(dsn)
+ return sqlite.Open(dsn + pragmas)
}
@@ -0,0 +1,166 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
Subject: [PATCH] daed: reload dae outside the database transaction
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
配置生效前先提交读事务,reload 完再用短事务写运行状态。
原来整个 reload 都在事务里,最长可占着数据库锁一百多秒;写状态时按 ID 重查仍存在的分组,避免把 reload 期间被删的分组重新插回去。
---
graphql/service/config/mutation_utils.go | 77 ++++++++++++++++--------
1 file changed, 53 insertions(+), 24 deletions(-)
diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go
index feb94d3..59f0ebd 100644
--- a/graphql/service/config/mutation_utils.go
+++ b/graphql/service/config/mutation_utils.go
@@ -270,28 +270,12 @@ func restartAfterReloadTimeout() {
}()
}
-func runTransaction(ctx context.Context, noLoad bool) (n int32, err error) {
- tx := db.BeginTx(ctx)
- if tx.Error != nil {
- return 0, tx.Error
- }
- n, err = runLocked(tx, noLoad)
- if err != nil {
- tx.Rollback()
- return 0, err
- }
- if err = tx.Commit().Error; err != nil {
- return 0, err
- }
- return n, nil
-}
-
func Run(ctx context.Context, noLoad bool) (n int32, err error) {
if ok := runLock.TryLock(); !ok {
return 0, fmt.Errorf("the last request didn't complete; make a cup of tea and take a break")
}
defer runLock.Unlock()
- return runTransaction(ctx, noLoad)
+ return runLocked(ctx, noLoad)
}
func ApplyIfRunning(ctx context.Context) error {
@@ -307,11 +291,11 @@ func ApplyIfRunning(ctx context.Context) error {
if err != nil || !modified {
return err
}
- _, err = runTransaction(ctx, false)
+ _, err = runLocked(ctx, false)
return err
}
-func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
+func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
//// Dry run.
if noLoad {
ch := make(chan error)
@@ -336,18 +320,38 @@ func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
}
// Running -> false
+ tx := db.BeginTx(ctx)
+ if tx.Error != nil {
+ return 0, tx.Error
+ }
var sys db.System
- if err = d.Model(&db.System{}).FirstOrCreate(&sys).Error; err != nil {
+ if err = tx.Model(&db.System{}).FirstOrCreate(&sys).Error; err != nil {
+ tx.Rollback()
return 0, err
}
- if err = d.Model(&sys).Updates(map[string]interface{}{
+ if err = tx.Model(&sys).Updates(map[string]interface{}{
"running": false,
}).Error; err != nil {
+ tx.Rollback()
+ return 0, err
+ }
+ if err = tx.Commit().Error; err != nil {
return 0, err
}
return 1, nil
}
+ d := db.BeginTx(ctx)
+ if d.Error != nil {
+ return 0, d.Error
+ }
+ readTx := d
+ defer func() {
+ if readTx != nil {
+ readTx.Rollback()
+ }
+ }()
+
//// Run selected global+dns+routing.
/// Get them from database and parse them to daeConfig.
var mConfig db.Config
@@ -513,6 +517,11 @@ func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
c.Node = append(c.Node, daeConfig.KeyableString(fmt.Sprintf("%v:%v", node.uniqueName, node.dbNode.Link)))
}
+ if err = readTx.Commit().Error; err != nil {
+ return 0, err
+ }
+ readTx = nil
+
/// Reload with current config.
chReloadCallback := make(chan error)
reloadMsg := &dae.ReloadMessage{
@@ -536,20 +545,37 @@ func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
}
// Save running status
+ tx := db.BeginTx(context.WithoutCancel(ctx))
+ if tx.Error != nil {
+ return 0, tx.Error
+ }
+ defer func() {
+ if err != nil {
+ tx.Rollback()
+ }
+ }()
var sys db.System
- if err = d.Model(&db.System{}).FirstOrCreate(&sys).Error; err != nil {
+ if err = tx.Model(&db.System{}).FirstOrCreate(&sys).Error; err != nil {
return 0, err
}
var gvs uint
var gids []string
+ groupIds := make([]uint, 0, len(groups))
for _, g := range groups {
gvs += g.Version
gids = append(gids, fmt.Sprintf("%x", g.ID))
+ groupIds = append(groupIds, g.ID)
+ }
+ liveGroups := make([]db.Group, 0, len(groupIds))
+ if len(groupIds) > 0 {
+ if err = tx.Where("id IN ?", groupIds).Find(&liveGroups).Error; err != nil {
+ return 0, err
+ }
}
sort.Slice(gids, func(i, j int) bool {
return gids[i] < gids[j]
})
- if err = d.Model(&sys).Updates(map[string]interface{}{
+ if err = tx.Model(&sys).Updates(map[string]interface{}{
"running": true,
"running_config_id": mConfig.ID,
"running_config_version": mConfig.Version,
@@ -562,7 +588,10 @@ func runLocked(d *gorm.DB, noLoad bool) (n int32, err error) {
}).Error; err != nil {
return 0, err
}
- if err = d.Model(&sys).Association("RunningGroups").Replace(groups); err != nil {
+ if err = tx.Model(&sys).Association("RunningGroups").Replace(liveGroups); err != nil {
+ return 0, err
+ }
+ if err = tx.Commit().Error; err != nil {
return 0, err
}
@@ -0,0 +1,68 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
Subject: [PATCH] daed: serialize scheduled subscription updates
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
订阅更新的入口读也纳入同一把锁,整轮更新串行,排到才开始计时。
多个订阅定时任务撞在同一分钟时会互相锁死。
---
.../service/subscription/mutation_utils.go | 29 +++++++++++++------
1 file changed, 20 insertions(+), 9 deletions(-)
diff --git a/graphql/service/subscription/mutation_utils.go b/graphql/service/subscription/mutation_utils.go
index c6bc3e1..e2aec32 100644
--- a/graphql/service/subscription/mutation_utils.go
+++ b/graphql/service/subscription/mutation_utils.go
@@ -225,12 +225,8 @@ func AddUpdateScheduler(ctx context.Context, id uint) {
tag = *sub.Tag
}
logrus.Info("Subscription " + tag + " update task enabled, with exp " + sub.CronExp)
- _, err := s.Cron(sub.CronExp).Do(func() {
- ctx, cancel := context.WithTimeout(context.Background(), scheduledUpdateTimeout)
- defer cancel()
- if _, err := UpdateById(ctx, sub.ID); err != nil {
- logrus.Error(err)
- }
+ _, err := s.Cron(sub.CronExp).SingletonMode().Do(func() {
+ runScheduledUpdate(sub.ID)
})
if err != nil {
logrus.Errorf("Failed to schedule subscription %d update: invalid cron expression '%s': %v", sub.ID, sub.CronExp, err)
@@ -364,9 +360,6 @@ func reconcileSubscriptionNodes(tx *gorm.DB, subId uint, links []string) error {
}
func updateSubscriptionTx(ctx context.Context, m *db.Subscription, subId uint, links []string) (err error) {
- subscriptionUpdateMu.Lock()
- defer subscriptionUpdateMu.Unlock()
-
tx := db.BeginTx(ctx)
if tx.Error != nil {
return tx.Error
@@ -404,7 +397,25 @@ func updateSubscriptionTx(ctx context.Context, m *db.Subscription, subId uint, l
return tx.Commit().Error
}
+func runScheduledUpdate(subId uint) {
+ subscriptionUpdateMu.Lock()
+ defer subscriptionUpdateMu.Unlock()
+
+ ctx, cancel := context.WithTimeout(context.Background(), scheduledUpdateTimeout)
+ defer cancel()
+ if _, err := updateByIdLocked(ctx, subId); err != nil {
+ logrus.Error(err)
+ }
+}
+
func UpdateById(ctx context.Context, subId uint) (sub *db.Subscription, err error) {
+ subscriptionUpdateMu.Lock()
+ defer subscriptionUpdateMu.Unlock()
+
+ return updateByIdLocked(ctx, subId)
+}
+
+func updateByIdLocked(ctx context.Context, subId uint) (sub *db.Subscription, err error) {
var m db.Subscription
if err = db.DB(ctx).Where(&db.Subscription{ID: subId}).First(&m).Error; err != nil {
return nil, err
@@ -0,0 +1,44 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
Subject: [PATCH] daed: apply subscription cron changes without restart
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
改 cron 后先提交事务再重建调度器。
原来在事务里就去重建,调度器另开连接读到的还是旧表达式,必须重启 daed 才生效。
---
graphql/service/subscription/mutation_utils.go | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/graphql/service/subscription/mutation_utils.go b/graphql/service/subscription/mutation_utils.go
index e2aec32..4f6bcfc 100644
--- a/graphql/service/subscription/mutation_utils.go
+++ b/graphql/service/subscription/mutation_utils.go
@@ -561,10 +561,12 @@ func UpdateCron(ctx context.Context, _id graphql.ID, cronExp string, cronEnable
}
tx := db.BeginTx(ctx)
+ if tx.Error != nil {
+ return nil, tx.Error
+ }
+ committed := false
defer func() {
- if err == nil {
- tx.Commit()
- } else {
+ if !committed {
tx.Rollback()
}
}()
@@ -583,6 +585,10 @@ func UpdateCron(ctx context.Context, _id graphql.ID, cronExp string, cronEnable
}).Error; err != nil {
return nil, err
}
+ if err = tx.Commit().Error; err != nil {
+ return nil, err
+ }
+ committed = true
// Update scheduler
RemoveUpdateScheduler(id)
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=ddns-go
PKG_VERSION:=6.17.4
PKG_RELEASE:=21
PKG_VERSION:=6.17.5
PKG_RELEASE:=22
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/jeessy2/ddns-go/tar.gz/v$(PKG_VERSION)?
+1 -1
View File
@@ -7,7 +7,7 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=filebrowser
PKG_VERSION:=1.5.0-stable
PKG_VERSION:=1.5.1-stable
PKG_RELEASE=1
ifeq ($(ARCH),aarch64)
+2 -1
View File
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=gecoosac
PKG_VERSION:=2.2.20251015
PKG_RELEASE:=16
PKG_RELEASE:=17
PKG_MAINTAINER:=Roc Lai <laipeng668@qq.com>
PKG_LICENSE:=AGPL-3.0-only
@@ -44,6 +44,7 @@ define Package/$(PKG_NAME)
TITLE:=gecoosac server (version $(PKG_VERSION))
URL:=http://www.cnrouter.com/
DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util
REPLACES:=luci-app-gecoosac
endef
define Package/$(PKG_NAME)/conffiles
+173 -33
View File
@@ -19,7 +19,18 @@ DEFAULT_LANG=zh
CERT_TIMEOUT=60
init_conf() {
config_load "gecoosac"
local section_type
unset CONFIG_config_TYPE
config_load "gecoosac" || {
logger -t gecoosac "unable to read Gecoos configuration"
return 1
}
config_get section_type config TYPE
[ "$section_type" = "gecoosac" ] || {
logger -t gecoosac "missing Gecoos configuration section"
return 1
}
config_get "db_dir" "config" "db_dir" "$DEFAULT_DB_DIR"
config_get "upload_dir" "config" "upload_dir" "$DEFAULT_UPLOAD_DIR"
config_get "enabled" "config" "enabled" "0"
@@ -76,14 +87,22 @@ normalize_path() {
}
is_safe_upload_dir() {
local path
local path physical
path="$(normalize_path "$1")" || return 1
physical="$(managed_dir_path upload "$path")" || return 1
case "$path" in
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
esac
case "$physical" in
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
esac
case "$path" in
*/gecoosac/upload) ;;
*) return 1 ;;
esac
case "$physical" in
*/gecoosac/upload) return 0 ;;
esac
@@ -145,7 +164,7 @@ is_secure_upload_dir() {
}
is_safe_db_dir() {
local path upload_root
local path upload_root physical physical_upload_root
path="$(normalize_path "$1")" || return 1
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
@@ -155,12 +174,15 @@ is_safe_db_dir() {
*) 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
local path upload_root physical physical_upload_root
path="$(normalize_path "$1")" || return 1
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
@@ -170,6 +192,9 @@ is_safe_pid_dir() {
*) 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
}
@@ -314,6 +339,7 @@ cert_matches_key() {
generate_default_cert() {
local cert_host cert_ip cert_cn cert_san tmp_crt tmp_key
local backup_dir old_crt old_key restore_ok path
if ! command -v openssl >/dev/null 2>&1; then
logger -t gecoosac "openssl is required to generate the default HTTPS certificate"
@@ -338,8 +364,11 @@ generate_default_cert() {
tmp_crt="${DEFAULT_CRT_FILE}.tmp"
tmp_key="${DEFAULT_KEY_FILE}.tmp"
rm -f "$tmp_crt" "$tmp_key"
run_with_timeout "$CERT_TIMEOUT" openssl req \
if ! rm -f "$tmp_crt" "$tmp_key"; then
logger -t gecoosac "unable to prepare default HTTPS certificate files"
return 1
fi
if ! run_with_timeout "$CERT_TIMEOUT" openssl req \
-x509 \
-nodes \
-newkey ec \
@@ -353,17 +382,79 @@ generate_default_cert() {
-addext "basicConstraints=critical,CA:FALSE" \
-addext "keyUsage=digitalSignature" \
-addext "extendedKeyUsage=serverAuth" \
-addext "subjectAltName=$cert_san" </dev/null
-addext "subjectAltName=$cert_san" </dev/null; then
rm -f "$tmp_crt" "$tmp_key"
logger -t gecoosac "failed to generate default HTTPS certificate"
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"; then
mv "$tmp_key" "$DEFAULT_KEY_FILE"
mv "$tmp_crt" "$DEFAULT_CRT_FILE"
chmod 600 "$DEFAULT_KEY_FILE"
chmod 644 "$DEFAULT_CRT_FILE"
if [ ! -s "$tmp_crt" ] || [ ! -s "$tmp_key" ] || \
! cert_matches_san "$tmp_crt" "$cert_host" "$cert_ip" || \
! cert_matches_key "$tmp_crt" "$tmp_key" || \
! chmod 600 "$tmp_key" || ! chmod 644 "$tmp_crt"; then
rm -f "$tmp_crt" "$tmp_key"
logger -t gecoosac "failed to validate default HTTPS certificate"
return 1
fi
for path in "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; do
if [ -e "$path" ] || [ -L "$path" ]; then
[ -f "$path" ] && [ ! -L "$path" ] || {
rm -f "$tmp_crt" "$tmp_key"
logger -t gecoosac "refusing unsafe default HTTPS certificate path: $path"
return 1
}
fi
done
backup_dir="$(mktemp -d /tmp/gecoosac-cert-backup.XXXXXX)" || {
rm -f "$tmp_crt" "$tmp_key"
logger -t gecoosac "unable to back up default HTTPS certificate"
return 1
}
old_crt=0
old_key=0
if [ -f "$DEFAULT_CRT_FILE" ]; then
cp -p "$DEFAULT_CRT_FILE" "$backup_dir/cert" || {
rm -f "$tmp_crt" "$tmp_key"
rm -rf "$backup_dir"
logger -t gecoosac "unable to back up default HTTPS certificate"
return 1
}
old_crt=1
fi
if [ -f "$DEFAULT_KEY_FILE" ]; then
cp -p "$DEFAULT_KEY_FILE" "$backup_dir/key" || {
rm -f "$tmp_crt" "$tmp_key"
rm -rf "$backup_dir"
logger -t gecoosac "unable to back up default HTTPS private key"
return 1
}
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
rm -rf "$backup_dir"
return 0
fi
rm -f "$tmp_crt" "$tmp_key"
restore_ok=1
rm -f "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE" || restore_ok=0
if [ "$old_crt" = "1" ]; then
cp -p "$backup_dir/cert" "$DEFAULT_CRT_FILE" || restore_ok=0
fi
if [ "$old_key" = "1" ]; then
cp -p "$backup_dir/key" "$DEFAULT_KEY_FILE" || restore_ok=0
fi
if [ "$restore_ok" = "1" ]; then
rm -rf "$backup_dir"
else
logger -t gecoosac "unable to restore default HTTPS certificate; backup retained at $backup_dir"
fi
logger -t gecoosac "failed to generate default HTTPS certificate"
return 1
}
@@ -460,21 +551,26 @@ ensure_secure_dir_tree() {
}
managed_dir_path() {
local role="$1" path="$2" anchor
local role="$1" path anchor
case "$role:$path" in
db:/var/lib/gecoosac|db:/var/lib/gecoosac/*)
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
;;
pid:/var/run|pid:/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}"
;;
*) printf '%s\n' "$path" ;;
esac
}
@@ -496,7 +592,7 @@ ensure_managed_dir() {
}
prepare_service() {
init_conf
init_conf || return 1
[ "$enabled" = "1" ] || return 0
normalize_conf || return 1
if [ ! -x "$PROG" ]; then
@@ -522,10 +618,9 @@ prepare_service() {
fi
}
start_service() {
start_prepared_service() {
local runtime_upload_dir runtime_db_dir runtime_piddir
prepare_service || return 1
[ "$enabled" = "1" ] || return 0
runtime_upload_dir="${upload_dir%/}/"
runtime_db_dir="${db_dir%/}/"
@@ -563,20 +658,65 @@ start_service() {
procd_close_instance
}
wait_service_stopped() {
local i
start_service() {
prepare_service || exit 1
start_prepared_service
}
if ! type service_running >/dev/null 2>&1; then
return 1
service_running_state() {
local data instances instance state
data="$(ubus call service list '{"name":"gecoosac"}' 2>/dev/null)" || return 2
json_load "$data" 2>/dev/null || return 2
if ! json_select gecoosac 2>/dev/null; then
json_cleanup
return 0
fi
if ! json_select instances 2>/dev/null; then
json_cleanup
return 0
fi
json_get_keys instances
for instance in $instances; do
if ! json_select "$instance" 2>/dev/null; then
json_cleanup
return 2
fi
json_get_var state running
json_select ..
case "$state" in
1|true)
json_cleanup
return 1
;;
0|false) ;;
*)
json_cleanup
return 2
;;
esac
done
json_cleanup
return 0
}
wait_service_stopped() {
local i state
i=0
while service_running gecoosac; do
[ "$i" -ge 5 ] && return 1
sleep 1
i=$((i + 1))
while :; do
service_running_state
state="$?"
case "$state" in
0) return 0 ;;
1)
[ "$i" -ge 5 ] && return 1
sleep 1
i=$((i + 1))
;;
*) return 1 ;;
esac
done
return 0
}
reload_service() {
@@ -585,7 +725,7 @@ reload_service() {
logger -t gecoosac "unable to stop the existing service for reload"
return 1
fi
start
rc_procd start_prepared_service
}
service_triggers() {
+62 -23
View File
@@ -12,7 +12,7 @@ CONFIG_COMPAT=2
ensure_section() {
uci -q get gecoosac.config >/dev/null && return 0
uci -q set gecoosac.config=gecoosac
uci -q set gecoosac.config=gecoosac || return 1
changed=1
}
@@ -21,7 +21,7 @@ set_default() {
local value="$2"
uci -q get "gecoosac.config.${option}" >/dev/null && return 0
uci -q set "gecoosac.config.${option}=${value}"
uci -q set "gecoosac.config.${option}=${value}" || return 1
changed=1
}
@@ -64,15 +64,48 @@ is_abs_path() {
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
}
is_safe_upload_dir() {
local path
local path physical
path="$(normalize_path "$1")" || return 1
physical="$(managed_dir_path upload "$path")" || return 1
case "$path" in
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
esac
case "$physical" in
/etc/gecoosac|/etc/gecoosac/*) return 1 ;;
esac
case "$path" in
*/gecoosac/upload) ;;
*) return 1 ;;
esac
case "$physical" in
*/gecoosac/upload) return 0 ;;
esac
@@ -91,7 +124,7 @@ is_path_in_dir() {
}
is_safe_db_dir() {
local path upload_root
local path upload_root physical physical_upload_root
path="$(normalize_path "$1")" || return 1
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
@@ -101,12 +134,15 @@ is_safe_db_dir() {
*) 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
local path upload_root physical physical_upload_root
path="$(normalize_path "$1")" || return 1
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
@@ -116,6 +152,9 @@ is_safe_pid_dir() {
*) 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
}
@@ -153,7 +192,7 @@ normalize_dir_option() {
fi
[ "$value" = "$normalized" ] && return 0
uci -q set "gecoosac.config.${option}=${normalized}"
uci -q set "gecoosac.config.${option}=${normalized}" || return 1
changed=1
}
@@ -238,26 +277,26 @@ migrate_config() {
changed=1
}
ensure_section
ensure_section || exit 1
migrate_config || exit 1
normalize_upload_dir || exit 1
normalize_dir_option db_dir "$DEFAULT_DB_DIR" is_safe_db_dir
normalize_dir_option piddir "$DEFAULT_PID_DIR" is_safe_pid_dir
set_default enabled 0
set_default port 60650
set_default isonlyoneprot 1
set_default m_port 8080
set_default https 0
set_default crt_file "$DEFAULT_CRT_FILE"
set_default key_file "$DEFAULT_KEY_FILE"
set_default upload_dir "$DEFAULT_UPLOAD_DIR"
set_default db_dir "$DEFAULT_DB_DIR"
set_default piddir "$DEFAULT_PID_DIR"
set_default lang zh
set_default debug 0
normalize_dir_option db_dir "$DEFAULT_DB_DIR" is_safe_db_dir || exit 1
normalize_dir_option piddir "$DEFAULT_PID_DIR" is_safe_pid_dir || exit 1
set_default enabled 0 || exit 1
set_default port 60650 || exit 1
set_default isonlyoneprot 1 || exit 1
set_default m_port 8080 || exit 1
set_default https 0 || exit 1
set_default crt_file "$DEFAULT_CRT_FILE" || exit 1
set_default key_file "$DEFAULT_KEY_FILE" || exit 1
set_default upload_dir "$DEFAULT_UPLOAD_DIR" || exit 1
set_default db_dir "$DEFAULT_DB_DIR" || exit 1
set_default piddir "$DEFAULT_PID_DIR" || exit 1
set_default lang zh || exit 1
set_default debug 0 || exit 1
# Preserve the previous package behavior for upgraded configs that never had showtip.
set_default showtip 0
set_default log 0
set_default showtip 0 || exit 1
set_default log 0 || exit 1
if [ "$changed" = "1" ]; then
uci -q commit gecoosac || exit 1
+2 -2
View File
@@ -7,11 +7,11 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-gecoosac
PKG_VERSION:=2.2
PKG_RELEASE:=16
PKG_RELEASE:=17
LUCI_TITLE:=LuCI Support for gecoosac
LUCI_DEPENDS:=+luci-base +gecoosac
LUCI_EXTRA_DEPENDS:=gecoosac (>=2.2.20251015-r5)
LUCI_EXTRA_DEPENDS:=gecoosac (>=2.2.20251015-r4)
LUCI_PKGARCH:=all
PKG_LICENSE:=AGPL-3.0-only
@@ -30,6 +30,33 @@ const callClearUpload = rpc.declare({
expect: { '': {} }
});
const callPathPolicy = rpc.declare({
object: 'luci.gecoosac',
method: 'path_policy',
expect: { '': {} },
reject: true
});
const RPC_ERROR_MESSAGES = {
'Unable to query service status': _('Unable to query service status'),
'Invalid service status response': _('Invalid service status response'),
'Expecting an absolute path': _('Expecting an absolute path'),
'Only Gecoos upload directories can be cleared': _('Only Gecoos upload directories can be cleared'),
'Upload directory or its parent is not root-owned and private': _('Upload directory or its parent is not root-owned and private'),
'Unable to resolve upload directory': _('Unable to resolve upload directory'),
'Unable to read Gecoos configuration': _('Unable to read Gecoos configuration'),
'Gecoos configuration changed during cleanup': _('Gecoos configuration changed during cleanup'),
'Unable to prepare upload directory cleanup': _('Unable to prepare upload directory cleanup'),
'Unable to validate upload cleanup stage': _('Unable to validate upload cleanup stage'),
'Upload cleanup stage contains a configured protected path': _('Upload cleanup stage contains a configured protected path'),
'Unable to validate configured paths': _('Unable to validate configured paths'),
'Upload directory contains a configured protected path': _('Upload directory contains a configured protected path'),
'Unable to recreate upload directory': _('Unable to recreate upload directory'),
'Unable to remove upload directory contents': _('Unable to remove upload directory contents'),
'Unable to remove upload cleanup stage': _('Unable to remove upload cleanup stage'),
'Unable to resolve managed path policy': _('Unable to resolve managed path policy')
};
function validPort(value, defaultValue) {
const port = Number(value || defaultValue);
return Number.isInteger(port) && port >= 1 && port <= 65535 ? String(port) : defaultValue;
@@ -106,10 +133,43 @@ function normalizePath(value) {
return '/' + parts.join('/');
}
function validUploadDir(value) {
function managedPath(value, policy) {
const path = normalizePath(value);
return path !== null && path.endsWith('/gecoosac/upload') && !pathInDir(path, CONFIG_BACKUP_DIR);
if (path === null)
return null;
if (path === '/var/run' || path.indexOf('/var/run/') === 0) {
if (!policy || policy.ok !== true || policy.var_run_root !== '/tmp/run')
return null;
return policy.var_run_root + path.substring('/var/run'.length);
}
if (path === '/var' || path.indexOf('/var/') === 0) {
if (!policy || policy.ok !== true || (policy.var_root !== '/var' && policy.var_root !== '/tmp'))
return null;
return policy.var_root + path.substring('/var'.length);
}
return path;
}
function usesManagedPath(value) {
const path = normalizePath(value);
return path === '/var' || path === '/var/run' ||
(path !== null && (path.indexOf('/var/') === 0 || path.indexOf('/var/run/') === 0));
}
function validUploadDir(value, policy) {
const path = normalizePath(value);
const physical = managedPath(value, policy);
return path !== null && physical !== null && path.endsWith('/gecoosac/upload') &&
physical.endsWith('/gecoosac/upload') && !pathInDir(path, CONFIG_BACKUP_DIR) &&
!pathInDir(physical, CONFIG_BACKUP_DIR);
}
function validPathPrefix(value, prefixes) {
@@ -132,12 +192,22 @@ function pathInDir(value, dir) {
return path !== null && root !== null && root !== '/' && (path === root || path.indexOf(root + '/') === 0);
}
function validDbDir(value, uploadDir) {
return validPathPrefix(value, DB_DIR_PREFIXES) && !pathInDir(value, uploadDir || DEFAULT_UPLOAD_DIR);
function validDbDir(value, uploadDir, policy) {
const upload = uploadDir || DEFAULT_UPLOAD_DIR;
const physical = managedPath(value, policy);
const physicalUpload = managedPath(upload, policy);
return validPathPrefix(value, DB_DIR_PREFIXES) && physical !== null && physicalUpload !== null &&
!pathInDir(value, upload) && !pathInDir(physical, physicalUpload);
}
function validPidDir(value, uploadDir) {
return validPathPrefix(value, PID_DIR_PREFIXES) && !pathInDir(value, uploadDir || DEFAULT_UPLOAD_DIR);
function validPidDir(value, uploadDir, policy) {
const upload = uploadDir || DEFAULT_UPLOAD_DIR;
const physical = managedPath(value, policy);
const physicalUpload = managedPath(upload, policy);
return validPathPrefix(value, PID_DIR_PREFIXES) && physical !== null && physicalUpload !== null &&
!pathInDir(value, upload) && !pathInDir(physical, physicalUpload);
}
function serviceRunning(status) {
@@ -188,7 +258,8 @@ function clientUrl() {
function renderStatusContent(status) {
if (status && status.error)
return E('p', { 'class': 'gecoosac-stopped' }, _('Service status unavailable') + ': ' + _(status.error));
return E('p', { 'class': 'gecoosac-stopped' }, _('Service status unavailable') + ': ' +
(RPC_ERROR_MESSAGES[status.error] || _('Unable to query service status')));
const running = serviceRunning(status);
const text = running
@@ -225,7 +296,9 @@ function updateStatus(status) {
}
function clearUploadError(res) {
return res && res.error ? _(res.error) : _('Upload directory was not cleared');
return res && res.error && RPC_ERROR_MESSAGES[res.error]
? RPC_ERROR_MESSAGES[res.error]
: _('Upload directory was not cleared');
}
return view.extend({
@@ -234,6 +307,9 @@ return view.extend({
uci.load('gecoosac'),
callServiceStatus().catch(function() {
return statusFailure();
}),
callPathPolicy().catch(function() {
return { ok: false };
})
]);
},
@@ -242,6 +318,7 @@ return view.extend({
let m, s, o, uploadDirOption;
let portOption, managementPortOption, singlePortOption;
let httpsOption, certificateOption, keyOption;
const pathPolicy = data[2] && data[2].ok === true ? data[2] : null;
m = new form.Map('gecoosac', _('Gecoos AC'),
_('Only supports Gecoos AP firmware 7.6 and above.') + '<br />' +
@@ -362,7 +439,10 @@ return view.extend({
o.datatype = 'directory';
o.rmempty = false;
o.validate = function(section_id, value) {
return validUploadDir(value)
if (usesManagedPath(value) && !pathPolicy)
return _('Unable to validate /var paths on this system.');
return validUploadDir(value, pathPolicy)
? true
: _('Upload directory must be an absolute path ending with /gecoosac/upload and must not be under /etc/gecoosac.');
};
@@ -375,11 +455,13 @@ return view.extend({
o.rmempty = false;
o.validate = function(section_id, value) {
const uploadDir = uploadDirOption.formvalue(section_id) || DEFAULT_UPLOAD_DIR;
if ((usesManagedPath(value) || usesManagedPath(uploadDir)) && !pathPolicy)
return _('Unable to validate /var paths on this system.');
if (!validPathPrefix(value, DB_DIR_PREFIXES))
return _('Database directory must be under /etc/gecoosac, /tmp/gecoosac, or /var/lib/gecoosac.');
return validDbDir(value, uploadDir)
return validDbDir(value, uploadDir, pathPolicy)
? true
: _('Database directory must not be the upload directory or inside it.');
};
@@ -392,11 +474,13 @@ return view.extend({
o.rmempty = false;
o.validate = function(section_id, value) {
const uploadDir = uploadDirOption.formvalue(section_id) || DEFAULT_UPLOAD_DIR;
if ((usesManagedPath(value) || usesManagedPath(uploadDir)) && !pathPolicy)
return _('Unable to validate /var paths on this system.');
if (!validPathPrefix(value, PID_DIR_PREFIXES))
return _('PID directory must be under /var/run or /tmp/gecoosac.');
return validPidDir(value, uploadDir)
return validPidDir(value, uploadDir, pathPolicy)
? true
: _('PID directory must not be the upload directory or inside it.');
};
@@ -433,6 +517,8 @@ return view.extend({
ui.addNotification(null, E('p', {}, _('Saved upload directory cleared')));
else
ui.addNotification(null, E('p', {}, clearUploadError(arguments[0])), 'danger');
}).catch(function() {
ui.addNotification(null, E('p', {}, _('Upload directory was not cleared')), 'danger');
});
};
+24 -3
View File
@@ -15,6 +15,9 @@ msgstr "默认证书文件会在 HTTPS 启动时生成;自定义路径必须
msgid "Gecoos AC"
msgstr "集客AC控制器"
msgid "Grant access for luci-app-gecoosac"
msgstr "授予 luci-app-gecoosac 访问权限"
msgid "Only supports Gecoos AP firmware 7.6 and above."
msgstr "仅支持集客 AP 7.6 及以上版本固件。"
@@ -145,9 +148,6 @@ msgstr "已保存的上传目录已清理"
msgid "Unable to remove upload directory contents"
msgstr "无法删除上传目录内容"
msgid "Upload directory contains configured runtime paths"
msgstr "上传目录包含已配置的运行目录"
msgid "Port must be an integer between 1 and 65535."
msgstr "端口必须是 1 到 65535 之间的整数。"
@@ -172,11 +172,32 @@ msgstr "上传目录包含已配置的受保护路径"
msgid "Unable to validate configured paths"
msgstr "无法验证已配置的路径"
msgid "Unable to read Gecoos configuration"
msgstr "无法读取 Gecoos 配置"
msgid "Gecoos configuration changed during cleanup"
msgstr "清理期间 Gecoos 配置发生变化"
msgid "Upload directory or its parent is not root-owned and private"
msgstr "上传目录或其父目录不是 root 所有且为私有目录"
msgid "Unable to prepare upload directory cleanup"
msgstr "无法准备上传目录清理"
msgid "Unable to validate upload cleanup stage"
msgstr "无法验证上传目录清理暂存区"
msgid "Upload cleanup stage contains a configured protected path"
msgstr "上传目录清理暂存区包含已配置的受保护路径"
msgid "Unable to recreate upload directory"
msgstr "无法重新创建上传目录"
msgid "Unable to remove upload cleanup stage"
msgstr "无法删除上传目录清理暂存区"
msgid "Unable to resolve managed path policy"
msgstr "无法解析受管路径策略"
msgid "Unable to validate /var paths on this system."
msgstr "无法验证此系统上的 /var 路径。"
+24 -3
View File
@@ -15,6 +15,9 @@ msgstr "預設憑證檔案會在 HTTPS 啟動時產生;自訂路徑必須指
msgid "Gecoos AC"
msgstr "集客 AC 控制器"
msgid "Grant access for luci-app-gecoosac"
msgstr "授予 luci-app-gecoosac 存取權限"
msgid "Only supports Gecoos AP firmware 7.6 and above."
msgstr "僅支援集客 AP 7.6 及以上版本韌體。"
@@ -145,9 +148,6 @@ msgstr "已儲存的上傳目錄已清除"
msgid "Unable to remove upload directory contents"
msgstr "無法移除上傳目錄內容"
msgid "Upload directory contains configured runtime paths"
msgstr "上傳目錄包含已設定的執行時路徑"
msgid "Port must be an integer between 1 and 65535."
msgstr "連接埠必須是 1 到 65535 之間的整數。"
@@ -172,11 +172,32 @@ msgstr "上傳目錄包含已設定的受保護路徑"
msgid "Unable to validate configured paths"
msgstr "無法驗證已設定的路徑"
msgid "Unable to read Gecoos configuration"
msgstr "無法讀取 Gecoos 設定"
msgid "Gecoos configuration changed during cleanup"
msgstr "清理期間 Gecoos 設定發生變更"
msgid "Upload directory or its parent is not root-owned and private"
msgstr "上傳目錄或其父目錄不是 root 所有且為私有目錄"
msgid "Unable to prepare upload directory cleanup"
msgstr "無法準備上傳目錄清理"
msgid "Unable to validate upload cleanup stage"
msgstr "無法驗證上傳目錄清理暫存區"
msgid "Upload cleanup stage contains a configured protected path"
msgstr "上傳目錄清理暫存區包含已設定的受保護路徑"
msgid "Unable to recreate upload directory"
msgstr "無法重新建立上傳目錄"
msgid "Unable to remove upload cleanup stage"
msgstr "無法移除上傳目錄清理暫存區"
msgid "Unable to resolve managed path policy"
msgstr "無法解析受管理路徑策略"
msgid "Unable to validate /var paths on this system."
msgstr "無法驗證此系統上的 /var 路徑。"
@@ -1,19 +1,21 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-gecoosac.json" ] && {
cat > /usr/share/ucitrack/luci-app-gecoosac.json << EEOF
[ ! -f "/usr/share/ucitrack/luci-app-add.json" ] && {
cat > /usr/share/ucitrack/luci-app-add.json << EEOF
{
"config": "gecoosac",
"init": "gecoosac"
"config": "add",
"init": "add"
}
EEOF
}
uci -q batch <<-EOF >/dev/null
delete ucitrack.@gecoosac[-1]
add ucitrack gecoosac
set ucitrack.@gecoosac[-1].init=gecoosac
commit ucitrack
EOF
while uci -q get 'ucitrack.@gecoosac[-1]' >/dev/null; do
uci -q delete 'ucitrack.@gecoosac[-1]' || exit 1
done
section="$(uci -q add ucitrack gecoosac)" || exit 1
[ -n "$section" ] || exit 1
uci -q set "ucitrack.${section}.init=gecoosac" || exit 1
uci -q commit ucitrack || exit 1
rm -f /tmp/luci-indexcache
rm -rf /tmp/luci-modulecache
@@ -1,8 +1,13 @@
#!/bin/sh
. /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
json_result() {
local ok="$1"
@@ -53,6 +58,50 @@ normalize_path() {
printf '%s\n' "$normalized"
}
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
unset CONFIG_config_TYPE \
CONFIG_config_upload_dir \
CONFIG_config_db_dir \
CONFIG_config_piddir \
CONFIG_config_crt_file \
CONFIG_config_key_file
config_load gecoosac || return 1
config_get section_type config TYPE
[ "$section_type" = "gecoosac" ] || return 1
config_get clear_upload_dir config upload_dir "$DEFAULT_UPLOAD_DIR"
config_get clear_db_dir config db_dir "$DEFAULT_DB_DIR"
config_get clear_piddir config piddir "$DEFAULT_PID_DIR"
config_get clear_crt_file config crt_file "$DEFAULT_CRT_FILE"
config_get clear_key_file config key_file "$DEFAULT_KEY_FILE"
}
safe_upload_path() {
local path
@@ -124,21 +173,56 @@ is_secure_upload_dir() {
}
configured_path_in_upload() {
local option="$1"
local upload_path="$2" path real_path
local option="$1" checked_root="$2"
local live_logical="${3:-$2}" live_physical="${4:-$2}"
local role path physical real_path suffix mapped
path="$(uci -q get "gecoosac.config.${option}")"
case "$option" 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
path_in_dir "$path" "$upload_path" && return 0
[ -e "$path" ] || [ -L "$path" ] || return 1
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
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 "$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
path_in_dir "$real_path" "$upload_path"
[ "$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" ]
}
status_result() {
@@ -184,6 +268,26 @@ service_status() {
status_result 1 "$running"
}
path_policy() {
local var_root var_run_root
if ! var_root="$(managed_dir_path file /var)" || \
! var_run_root="$(managed_dir_path file /var/run)"; then
json_init
json_add_boolean ok 0
json_add_string error "Unable to resolve managed path policy"
json_dump
json_cleanup
return
fi
json_init
json_add_boolean ok 1
json_add_string var_root "$var_root"
json_add_string var_run_root "$var_run_root"
json_dump
json_cleanup
}
clear_upload_unlock() {
flock -u 9 >/dev/null 2>&1
exec 9<&-
@@ -218,11 +322,28 @@ ensure_live_upload_dir() {
is_secure_upload_dir "$path"
}
restore_staged_upload() {
local stage="$1" live="$2"
if [ -e "$stage/upload" ] || [ -L "$stage/upload" ]; then
[ -d "$stage/upload" ] && [ ! -L "$stage/upload" ] || return 1
if [ -e "$live" ] || [ -L "$live" ]; then
[ -d "$live" ] && [ ! -L "$live" ] && rmdir "$live" 2>/dev/null || return 1
fi
if ! mv "$stage/upload" "$live" 2>/dev/null; then
[ -d "$live" ] && [ ! -L "$live" ] && \
[ ! -e "$stage/upload" ] && [ ! -L "$stage/upload" ] || return 1
fi
fi
is_secure_upload_dir "$live" || return 1
rmdir "$stage" 2>/dev/null
}
validate_clear_protected_paths() {
local root="$1" option
local root="$1" live_logical="${2:-$1}" live_physical="${3:-$1}" option
for option in db_dir piddir crt_file key_file; do
configured_path_in_upload "$option" "$root"
configured_path_in_upload "$option" "$root" "$live_logical" "$live_physical"
case "$?" in
0) return 0 ;;
1) ;;
@@ -233,56 +354,102 @@ validate_clear_protected_paths() {
}
clear_upload() {
local path real parent stage option candidate stages live_exists
local logical physical resolved parent stage candidate stages live_exists current
local clear_upload_dir clear_db_dir clear_piddir clear_crt_file clear_key_file
local initial_upload initial_db initial_pid initial_crt initial_key
path="$(uci -q get gecoosac.config.upload_dir)"
[ -n "$path" ] || path="$DEFAULT_UPLOAD_DIR"
load_clear_config || {
json_result 0 "Unable to read Gecoos configuration"
return
}
logical="$clear_upload_dir"
initial_upload="$clear_upload_dir"
initial_db="$clear_db_dir"
initial_pid="$clear_piddir"
initial_crt="$clear_crt_file"
initial_key="$clear_key_file"
case "$path" in
case "$logical" in
/*) ;;
*) json_result 0 "Expecting an absolute path"; return ;;
esac
path="$(normalize_path "$path")" || { json_result 0 "Expecting an absolute path"; return; }
if ! safe_upload_path "$path"; then
json_result 0 "Only Gecoos upload directories can be cleared" "$path"
logical="$(normalize_path "$logical")" || { json_result 0 "Expecting an absolute path"; return; }
if ! safe_upload_path "$logical"; then
json_result 0 "Only Gecoos upload directories can be cleared" "$logical"
return
fi
parent="${path%/*}"
physical="$(managed_dir_path upload "$logical")" || {
json_result 0 "Unable to resolve upload directory" "$logical"
return
}
if ! safe_upload_path "$physical"; then
json_result 0 "Only Gecoos upload directories can be cleared" "$logical"
return
fi
parent="${physical%/*}"
if [ ! -e "$parent" ] && [ ! -L "$parent" ]; then
json_result 1 "" "$path"
json_result 1 "" "$logical"
return
fi
if ! is_secure_upload_dir "$parent"; then
json_result 0 "Upload directory or its parent is not root-owned and private" "$path"
json_result 0 "Upload directory or its parent is not root-owned and private" "$logical"
return
fi
if [ -e "$path" ] || [ -L "$path" ]; then
real="$(readlink -f "$path" 2>/dev/null)"
else
real="$path"
if [ -e "$logical" ] || [ -L "$logical" ]; then
resolved="$(readlink -f "$logical" 2>/dev/null)"
[ -n "$resolved" ] || { json_result 0 "Unable to resolve upload directory" "$logical"; return; }
resolved="$(normalize_path "$resolved")" || { json_result 0 "Expecting an absolute path"; return; }
[ "$resolved" = "$physical" ] || { json_result 0 "Only Gecoos upload directories can be cleared" "$logical"; return; }
fi
[ -n "$real" ] || { json_result 0 "Unable to resolve upload directory" "$path"; return; }
real="$(normalize_path "$real")" || { json_result 0 "Expecting an absolute path"; return; }
[ "$real" = "$path" ] || { json_result 0 "Only Gecoos upload directories can be cleared" "$real"; return; }
exec 9<"$parent" 2>/dev/null || {
json_result 0 "Unable to prepare upload directory cleanup" "$real"
json_result 0 "Unable to prepare upload directory cleanup" "$logical"
return
}
if ! flock -n 9 >/dev/null 2>&1; then
clear_upload_unlock
json_result 0 "Unable to prepare upload directory cleanup" "$real"
json_result 0 "Unable to prepare upload directory cleanup" "$logical"
return
fi
load_clear_config || {
clear_upload_locked_result 0 "Unable to read Gecoos configuration" "$logical"
return
}
if [ "$clear_upload_dir" != "$initial_upload" ] || \
[ "$clear_db_dir" != "$initial_db" ] || \
[ "$clear_piddir" != "$initial_pid" ] || \
[ "$clear_crt_file" != "$initial_crt" ] || \
[ "$clear_key_file" != "$initial_key" ]; then
clear_upload_locked_result 0 "Gecoos configuration changed during cleanup" "$logical"
return
fi
current="$(managed_dir_path upload "$logical")" || {
clear_upload_locked_result 0 "Unable to resolve upload directory" "$logical"
return
}
[ "$current" = "$physical" ] || {
clear_upload_locked_result 0 "Unable to resolve upload directory" "$logical"
return
}
if [ -e "$logical" ] || [ -L "$logical" ]; then
resolved="$(readlink -f "$logical" 2>/dev/null)" || {
clear_upload_locked_result 0 "Unable to resolve upload directory" "$logical"
return
}
resolved="$(normalize_path "$resolved")" || {
clear_upload_locked_result 0 "Expecting an absolute path" "$logical"
return
}
[ "$resolved" = "$physical" ] || {
clear_upload_locked_result 0 "Only Gecoos upload directories can be cleared" "$logical"
return
}
fi
if ! is_secure_upload_dir "$parent"; then
clear_upload_locked_result 0 "Upload directory or its parent is not root-owned and private" "$real"
clear_upload_locked_result 0 "Upload directory or its parent is not root-owned and private" "$logical"
return
fi
parent="${real%/*}"
stages=0
for candidate in "$parent"/.gecoosac-clear.??????; do
[ "$candidate" != "$parent/.gecoosac-clear.??????" ] || continue
@@ -292,7 +459,7 @@ clear_upload() {
return
}
if [ -e "$candidate/upload" ] || [ -L "$candidate/upload" ]; then
validate_clear_protected_paths "$candidate/upload"
validate_clear_protected_paths "$candidate/upload" "$logical" "$physical"
case "$?" in
0) clear_upload_locked_result 0 "Upload cleanup stage contains a configured protected path" "$candidate"; return ;;
1) ;;
@@ -302,36 +469,89 @@ clear_upload() {
done
live_exists=0
[ -e "$real" ] || [ -L "$real" ] && live_exists=1
if [ "$live_exists" = "1" ] && ! is_secure_upload_dir "$real"; then
clear_upload_locked_result 0 "Upload directory or its parent is not root-owned and private" "$real"
[ -e "$physical" ] || [ -L "$physical" ] && live_exists=1
if [ "$live_exists" = "1" ] && ! is_secure_upload_dir "$physical"; then
clear_upload_locked_result 0 "Upload directory or its parent is not root-owned and private" "$logical"
return
fi
if [ "$live_exists" = "0" ] && [ "$stages" = "0" ]; then
clear_upload_locked_result 1 "" "$real"
clear_upload_locked_result 1 "" "$logical"
return
fi
if [ "$live_exists" = "1" ]; then
validate_clear_protected_paths "$real"
validate_clear_protected_paths "$physical" "$logical" "$physical"
case "$?" in
0) clear_upload_locked_result 0 "Upload directory contains a configured protected path" "$real"; return ;;
0) clear_upload_locked_result 0 "Upload directory contains a configured protected path" "$logical"; return ;;
1) ;;
*) clear_upload_locked_result 0 "Unable to validate configured paths" "$real"; return ;;
*) clear_upload_locked_result 0 "Unable to validate configured paths" "$logical"; return ;;
esac
fi
if [ "$live_exists" = "0" ]; then
ensure_live_upload_dir "$real" || {
clear_upload_locked_result 0 "Unable to recreate upload directory" "$real"
ensure_live_upload_dir "$physical" || {
clear_upload_locked_result 0 "Unable to recreate upload directory" "$logical"
return
}
fi
if [ "$live_exists" = "1" ]; then
stage="$(mktemp -d "$parent/.gecoosac-clear.XXXXXX" 2>/dev/null)" || {
clear_upload_locked_result 0 "Unable to prepare upload directory cleanup" "$logical"
return
}
if ! chmod 0700 "$stage"; then
rmdir "$stage" 2>/dev/null
clear_upload_locked_result 0 "Unable to prepare upload directory cleanup" "$logical"
return
fi
if ! mv "$physical" "$stage/upload"; then
restore_staged_upload "$stage" "$physical" || \
logger -t gecoosac "upload cleanup retained staged data at $stage"
clear_upload_locked_result 0 "Unable to prepare upload directory cleanup" "$logical"
return
fi
if ! ensure_live_upload_dir "$physical"; then
restore_staged_upload "$stage" "$physical" || \
logger -t gecoosac "upload cleanup retained staged data at $stage"
clear_upload_locked_result 0 "Unable to recreate upload directory" "$logical"
return
fi
else
stage=
fi
load_clear_config || {
clear_upload_locked_result 0 "Unable to read Gecoos configuration" "$logical"
return
}
if [ "$clear_upload_dir" != "$initial_upload" ] || \
[ "$clear_db_dir" != "$initial_db" ] || \
[ "$clear_piddir" != "$initial_pid" ] || \
[ "$clear_crt_file" != "$initial_crt" ] || \
[ "$clear_key_file" != "$initial_key" ]; then
clear_upload_locked_result 0 "Gecoos configuration changed during cleanup" "$logical"
return
fi
for candidate in "$parent"/.gecoosac-clear.??????; do
[ "$candidate" != "$parent/.gecoosac-clear.??????" ] || continue
is_safe_clear_stage "$candidate" || {
clear_upload_locked_result 0 "Unable to validate upload cleanup stage" "$candidate"
return
}
if [ -e "$candidate/upload" ] || [ -L "$candidate/upload" ]; then
validate_clear_protected_paths "$candidate/upload" "$logical" "$physical"
case "$?" in
0) clear_upload_locked_result 0 "Upload cleanup stage contains a configured protected path" "$candidate"; return ;;
1) ;;
*) clear_upload_locked_result 0 "Unable to validate configured paths" "$candidate"; return ;;
esac
fi
done
for candidate in "$parent"/.gecoosac-clear.??????; do
[ "$candidate" != "$parent/.gecoosac-clear.??????" ] || continue
if [ -e "$candidate/upload" ] || [ -L "$candidate/upload" ]; then
rm -rf "$candidate/upload" || {
logger -t gecoosac "upload cleanup retained staged data at $candidate"
clear_upload_locked_result 0 "Unable to remove upload directory contents" "$real"
clear_upload_locked_result 0 "Unable to remove upload directory contents" "$logical"
return
}
fi
@@ -341,49 +561,17 @@ clear_upload() {
}
done
if [ "$live_exists" = "1" ]; then
stage="$(mktemp -d "$parent/.gecoosac-clear.XXXXXX" 2>/dev/null)" || {
clear_upload_locked_result 0 "Unable to prepare upload directory cleanup" "$real"
return
}
if ! chmod 0700 "$stage" || ! mv "$real" "$stage/upload"; then
rm -rf "$stage"
clear_upload_locked_result 0 "Unable to prepare upload directory cleanup" "$real"
return
fi
if ! ensure_live_upload_dir "$real"; then
if [ ! -e "$real" ] && [ ! -L "$real" ]; then
mv "$stage/upload" "$real" 2>/dev/null
fi
clear_upload_locked_result 0 "Unable to recreate upload directory" "$real"
return
fi
else
stage=
fi
if [ "$live_exists" = "1" ]; then
if ! rm -rf "$stage/upload"; then
logger -t gecoosac "upload cleanup retained staged data at $stage"
clear_upload_locked_result 0 "Unable to remove upload directory contents" "$real"
return
fi
rmdir "$stage" || {
clear_upload_locked_result 0 "Unable to remove upload cleanup stage" "$stage"
return
}
fi
clear_upload_locked_result 1 "" "$real"
clear_upload_locked_result 1 "" "$logical"
}
case "$1" in
list)
printf '{ "clear_upload": {}, "status": {} }'
printf '{ "clear_upload": {}, "path_policy": {}, "status": {} }'
;;
call)
case "$2" in
status) service_status ;;
path_policy) path_policy ;;
clear_upload) clear_upload ;;
*) json_result 0 "Unknown method" ;;
esac
@@ -3,7 +3,7 @@
"description": "Grant access for luci-app-gecoosac",
"read": {
"ubus": {
"luci.gecoosac": [ "status" ]
"luci.gecoosac": [ "path_policy", "status" ]
},
"uci": [ "gecoosac" ]
},
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.8.1
PKG_RELEASE:=204
PKG_RELEASE:=205
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -0,0 +1,61 @@
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function decodeIfBase64(str) {
try {
let s = str.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const decoded = decodeURIComponent(
atob(s).split('').map(c =>
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
).join('')
);
if (btoa(unescape(encodeURIComponent(decoded))).replace(/=+$/, '') === s.replace(/=+$/, '')) {
return decoded;
}
} catch (e) {}
return str;
}
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function get_current_url() {
return window.location.origin + window.location.pathname;
}
function getOption(config, section, opt) {
let obj;
const id = `cbid.${config}.${section}.${opt}`;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
const combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
const div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
+109 -21
View File
@@ -53,14 +53,14 @@ function index()
entry({"admin", "services", appname, "socks_config"}, cbi(appname .. "/client/socks_config")).leaf = true
entry({"admin", "services", appname, "acl"}, cbi(appname .. "/client/acl"), _("Access control"), 98).leaf = true
entry({"admin", "services", appname, "acl_config"}, cbi(appname .. "/client/acl_config")).leaf = true
entry({"admin", "services", appname, "log"}, form(appname .. "/client/log"), _("Runtime Logs"), 999).leaf = true
entry({"admin", "services", appname, "log"}, template(appname .. "/log/log"), _("Runtime Logs"), 999).leaf = true
--[[ Server ]]
entry({"admin", "services", appname, "server"}, cbi(appname .. "/server/index"), _("Server-Side"), 99).leaf = true
entry({"admin", "services", appname, "server_user"}, cbi(appname .. "/server/user")).leaf = true
--[[ API ]]
entry({"admin", "services", appname, "server_user_update"}, call("server_user_update")).leaf = true
entry({"admin", "services", appname, "server_update_config"}, call("server_update_config")).leaf = true
entry({"admin", "services", appname, "server_user_status"}, call("server_user_status")).leaf = true
entry({"admin", "services", appname, "server_user_log"}, call("server_user_log")).leaf = true
entry({"admin", "services", appname, "server_get_log"}, call("server_get_log")).leaf = true
@@ -81,8 +81,8 @@ function index()
entry({"admin", "services", appname, "connect_status"}, call("connect_status")).leaf = true
entry({"admin", "services", appname, "ping_node"}, call("ping_node")).leaf = true
entry({"admin", "services", appname, "urltest_node"}, call("urltest_node")).leaf = true
entry({"admin", "services", appname, "update_config"}, call("update_config")).leaf = true
entry({"admin", "services", appname, "add_node"}, call("add_node")).leaf = true
entry({"admin", "services", appname, "update_node"}, call("update_node")).leaf = true
entry({"admin", "services", appname, "set_node"}, call("set_node")).leaf = true
entry({"admin", "services", appname, "copy_node"}, call("copy_node")).leaf = true
entry({"admin", "services", appname, "clear_all_nodes"}, call("clear_all_nodes")).leaf = true
@@ -98,6 +98,10 @@ function index()
entry({"admin", "services", appname, "subscribe_manual"}, call("subscribe_manual")).leaf = true
entry({"admin", "services", appname, "subscribe_manual_all"}, call("subscribe_manual_all")).leaf = true
entry({"admin", "services", appname, "flush_set"}, call("flush_set")).leaf = true
entry({"admin", "services", appname, "get_shunt_rules"}, call("get_shunt_rules")).leaf = true
entry({"admin", "services", appname, "add_shunt_rule"}, call("add_shunt_rule")).leaf = true
entry({"admin", "services", appname, "delete_select_shunt_rules"}, call("delete_select_shunt_rules")).leaf = true
entry({"admin", "services", appname, "save_shunt_rule_order"}, call("save_shunt_rule_order")).leaf = true
--[[rule_list]]
entry({"admin", "services", appname, "read_rulelist"}, call("read_rulelist")).leaf = true
@@ -548,6 +552,23 @@ function urltest_node()
http_write_json(e)
end
function update_config()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
local data_t = jsonParse(data) or {}
if next(data_t) then
for k, v in pairs(data_t) do
uci:set(appname, id, k, v)
end
api.uci_save(uci, appname)
http_write_json_ok()
return
end
end
http_write_json_error()
end
function add_node()
local redirect = http.formvalue("redirect")
@@ -570,23 +591,6 @@ function add_node()
end
end
function update_node()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
local data_t = jsonParse(data) or {}
if next(data_t) then
for k, v in pairs(data_t) do
uci:set(appname, id, k, v)
end
api.uci_save(uci, appname)
http_write_json_ok()
return
end
end
http_write_json_error()
end
function set_node()
local protocol = http.formvalue("protocol")
local section = http.formvalue("section")
@@ -857,7 +861,7 @@ function rollback_rules()
http_write_json_ok()
end
function server_user_update()
function server_update_config()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
@@ -1211,3 +1215,87 @@ function fetch_certsha256()
local data = api.fetch_cert_sha256(address, port, sni, timeout, h3)
http_write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
end
function get_shunt_rules()
local id = http.formvalue("id")
local result = {}
if id then
result = uci:get_all(appname, id)
else
local default_items = {}
local other_items = {}
uci:foreach(appname, "shunt_rules", function(t)
if not t.group or t.group == "" then
default_items[#default_items + 1] = t
else
other_items[#other_items + 1] = t
end
end)
for i = 1, #default_items do result[#result + 1] = default_items[i] end
for i = 1, #other_items do result[#result + 1] = other_items[i] end
end
http_write_json(result)
end
function add_shunt_rule()
local add_name = http.formvalue("add_name")
local redirect = http.formvalue("redirect")
local uuid = add_name
if add_name then
local has = uci:get(appname, uuid)
if has then
http_write_json_error({ message = "This ID already exists." })
return
end
else
uuid = api.gen_short_uuid()
end
uci:section(appname, "shunt_rules", uuid)
local group = http.formvalue("group")
if group and group ~= "default" then
uci:set(appname, uuid, "group", group)
end
if redirect == "1" then
api.uci_save(uci, appname)
http.redirect(api.url("shunt_rules", uuid))
else
api.uci_save(uci, appname)
http_write_json_ok({uuid = uuid, redirect_url = api.url("shunt_rules", uuid)})
end
end
function delete_select_shunt_rules()
local ids = http.formvalue("ids")
local redirect = http.formvalue("redirect")
string.gsub(ids, '[^' .. "," .. ']+', function(w)
uci:foreach(appname, "nodes", function(s)
if s["protocol"] and s["protocol"] == "_shunt" then
uci:delete(appname, s[".name"], w)
end
end)
uci:delete(appname, w)
end)
if redirect == "1" then
api.uci_save(uci, appname)
http.redirect(api.url("rule"))
else
api.uci_save(uci, appname, true, true)
end
end
function save_shunt_rule_order()
local ids = http.formvalue("ids") or ""
local new_order = {}
for id in ids:gmatch("([^,]+)") do
new_order[#new_order + 1] = id
end
for idx, name in ipairs(new_order) do
luci.sys.call(string.format("uci -q reorder %s.%s=%d", appname, name, idx - 1))
end
api.sh_uci_commit(appname)
http_write_json({ status = "ok" })
end
@@ -154,6 +154,7 @@ if (has_singbox or has_xray) and #nodes_table > 0 then
if current_node.protocol == "_shunt" then
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s, {
s_cfgid = s:cfgsections()[1],
node_id = current_node_id,
node = current_node,
socks_list = socks_list,
@@ -4,9 +4,17 @@ if not data.node_id or not data.node then
return
end
local s_cfgid = data.s_cfgid
local current_node_id = data.node_id
local node_list = data.node_list or api.get_node_list()
local groups = {}
m.uci:foreach(appname, "shunt_rules", function(s)
if s.group and s.group ~= "" then
groups[s.group] = true
end
end)
local function get_cfgvalue()
return function(self, section)
return m:get(current_node_id, self.option)
@@ -92,15 +100,27 @@ o = add_option(Flag, "fakedns", '<a style="color:#FF8C00">FakeDNS</a>' .. " " ..
translate("Suitable scenarios for let the node servers get the target domain names.") .. "<br>" ..
translate("Such as: DNS unlocking of streaming media, reducing DNS query latency, etc."))
shunt_group = add_option(ListValue, "shunt_group", translate("Shunt Rule Group"))
shunt_group:value("", translate("default"))
for k, v in pairs(groups) do
shunt_group:value(k)
end
local shunt_group_val = m:get(current_node_id, "shunt_group") or ""
shunt_group_val = shunt_group_val:lower()
local shunt_rules = {}
m.uci:foreach(appname, "shunt_rules", function(e)
e.id = e[".name"]
e.remarks = e.remarks or e[".name"]
e["_node_option"] = e[".name"]
e["_node_default"] = ""
e["_fakedns_option"] = e[".name"] .. "_fakedns"
e["_proxy_tag_option"] = e[".name"] .. "_proxy_tag"
table.insert(shunt_rules, e)
local group = e.group or ""
group = group:lower()
if group == shunt_group_val then
e.id = e[".name"]
e.remarks = e.remarks or e[".name"]
e["_node_option"] = e[".name"]
e["_node_default"] = ""
e["_fakedns_option"] = e[".name"] .. "_fakedns"
e["_proxy_tag_option"] = e[".name"] .. "_proxy_tag"
table.insert(shunt_rules, e)
end
end)
table.insert(shunt_rules, {
id = ".default",
@@ -185,6 +205,8 @@ end
local footer = Template(appname .. "/include/shunt_options")
footer.api = api
footer.config = m.config
footer.id = current_node_id
footer.s_cfgid = s_cfgid or current_node_id
footer.normal_list = api.jsonc.stringify(node_list.normal_list)
m:append(footer)
@@ -1,8 +0,0 @@
local api = require "luci.passwall.api"
local appname = "passwall"
f = SimpleForm(appname)
f.reset = false
f.submit = false
f:append(Template(appname .. "/log/log"))
return f
@@ -30,9 +30,6 @@ o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
o.default = o.keylist[3]
-- [[ Add the node via the link ]]--
s:append(Template(appname .. "/node_list/link_add_node"))
m:append(Template(appname .. "/node_list/node_list"))
return api.return_map(m)
@@ -89,9 +89,8 @@ if has_xray or has_singbox then
o.rmempty = false
o.description = "<ul>"
.. "<li>" .. translate("Experimental feature.") .. "</li>"
.. "<li>" .. "1." .. translate("Analyzes and preloads GeoIP/Geosite data to enhance the shunt performance of Sing-box/Xray.") .. "</li>"
.. "<li>" .. "1." .. translate("Parses and preloads GeoIP/Geosite data to improve Sing-box/Xray routing performance.") .. "</li>"
.. "<li>" .. "2." .. translate("Once enabled, the rule list can support GeoIP/Geosite rules.") .. "</li>"
.. "<li>" .. translate("Note: Increases resource usage; Geosite analysis is only supported in ChinaDNS-NG and SmartDNS modes.") .. "</li>"
.. "</ul>"
function o.write(self, section, value)
local old = m:get(section, self.option) or "0"
@@ -146,34 +145,8 @@ end
s:append(Template(appname .. "/rule/rule_version"))
local cfgname = "shunt_rules"
if has_xray or has_singbox then
s = m:section(TypedSection, cfgname, "Sing-Box/Xray " .. translate("Shunt Rule"), "<a style='color: red'>" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "</a>")
s.template = "cbi/tblsection"
s.anonymous = false
s.addremove = true
s.sortable = true
s.extedit = api.url("shunt_rules", "%s")
function s.create(e, t)
TypedSection.create(e, t)
luci.http.redirect(e.extedit:format(t))
end
function s.remove(e, t)
m.uci:foreach(appname, "nodes", function(s)
if s["protocol"] and s["protocol"] == "_shunt" then
m:del(s[".name"], t)
end
end)
TypedSection.remove(e, t)
end
o = s:option(DummyValue, "remarks", translate("Remarks"))
m:append(Template(appname .. "/rule/shunt_rule_list"))
end
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
return api.return_map(m)
@@ -44,6 +44,19 @@ function clean_text(text)
:gsub("[ \t]*\n[ \t]*", "\n")
end
local remarks_lookup = {}
local groups = {}
m.uci:foreach(appname, "shunt_rules", function(s)
if s[".name"] ~= arg[1] then
if s.remarks then
remarks_lookup[s.remarks] = s[".name"]
end
if s.group and s.group ~= "" then
groups[s.group] = true
end
end
end)
s = m:section(NamedSection, arg[1], "shunt_rules", "")
s.addremove = false
s.dynamic = false
@@ -56,9 +69,34 @@ remarks.validate = function(self, value, section)
if value == "" then
return nil, translate("Remark cannot be empty.")
end
if remarks_lookup[value] then
return nil, translate("This remark already exists, please change a new remark.")
end
return value
end
o = s:option(Value, "group", translate("Shunt Rule Group"))
o.default = ""
o:value("", translate("default"))
for k, v in pairs(groups) do
o:value(k)
end
o.write = function(self, section, value)
value = api.trim(value)
local lower = value:lower()
if lower == "" or lower == "default" then
return m:del(section, self.option)
end
for _, v in ipairs(self.keylist or {}) do
if v:lower() == lower then
return m:set(section, self.option, v)
end
end
m:set(section, self.option, value)
end
protocol = s:option(MultiValue, "protocol", translate("Protocol"))
protocol:value("http")
protocol:value("tls")
@@ -141,12 +141,12 @@ o.default = 30
o:depends("enable_autoswitch", true)
o = s:option(Value, "autoswitch_connect_timeout", translate("Timeout seconds"), translate("Units:seconds"))
o.datatype = "min(1)"
o.datatype = "range(3,10)"
o.default = 3
o:depends("enable_autoswitch", true)
o = s:option(Value, "autoswitch_retry_num", translate("Timeout retry num"))
o.datatype = "min(1)"
o.datatype = "range(1,5)"
o.default = 1
o:depends("enable_autoswitch", true)
@@ -1594,11 +1594,15 @@ function gen_config(var)
[".name"] = "GFW_Mode_List",
remarks = "GFW_Mode_List",
domain_list = (domain_list ~= "") and domain_list or nil,
ip_list = (ip_list ~= "") and ip_list or nil
ip_list = (ip_list ~= "") and ip_list or nil,
group = node["shunt_group"]
})
end
end
foreach_shunt_rule(function(e)
if node["shunt_group"] ~= e.group then
return
end
local outboundTag = gen_shunt_node(e[".name"])
if outboundTag and e.remarks then
if outboundTag == "default" then
@@ -1993,17 +1997,6 @@ function gen_config(var)
else default_dns_flag = "direct"
end
end
if default_dns_flag == "remote" then
if remote_dns_fake then
table.insert(dns.rules, {
query_type = { "A", "AAAA" },
server = fakedns_tag,
disable_cache = true,
rewrite_ttl = 30,
strategy = remote_strategy
})
end
end
dns.final = default_dns_flag
--按分流顺序DNS
@@ -2058,6 +2051,28 @@ function gen_config(var)
end
end
end
if default_dns_flag == "remote" then
if remote_dns_fake then
-- When default is not direct and enable fakedns, default DNS use FakeDNS.
local fakedns_dns_rule = {
query_type = {
"A", "AAAA"
},
server = fakedns_tag,
disable_cache = true,
rewrite_ttl = 30,
strategy = remote_strategy,
}
table.insert(dns.rules, fakedns_dns_rule)
else
local remote_dns_rule = {
server = "remote",
disable_cache = true,
strategy = remote_strategy,
}
table.insert(dns.rules, remote_dns_rule)
end
end
local dns_in_inbound = {
type = "direct",
tag = "dns-in",
@@ -1405,11 +1405,15 @@ function gen_config(var)
[".name"] = "GFW_Mode_List",
remarks = "GFW_Mode_List",
domain_list = (domain_list ~= "") and domain_list or nil,
ip_list = (ip_list ~= "") and ip_list or nil
ip_list = (ip_list ~= "") and ip_list or nil,
group = node["shunt_group"]
})
end
end
foreach_shunt_rule(function(e)
if node["shunt_group"] ~= e.group then
return
end
local outbound_tag = gen_shunt_node(e[".name"])
if outbound_tag and e.remarks then
if outbound_tag == "default" then
@@ -1729,7 +1733,7 @@ function gen_config(var)
elseif remote_dns_query_strategy == "UseIPv6" then
table.insert(fakedns, fakedns6)
end
if remote_dns_fake and inner_fakedns ~= "1" then
if remote_dns_fake then
table.insert(dns.servers, 1, _remote_fakedns)
end
end
@@ -1,2 +1,24 @@
<!-- map header -->
<script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script>
<script src="<%=resource%>/view/<%=self.api.appname%>/func.js"></script>
<script type="text/javascript">
//<![CDATA[
function update_config(cfgid, data, redirect_url) {
XHR.get('<%=self.api.url("update_config")%>', {
id: cfgid,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
if (redirect_url) {
window.location.href = redirect_url;
}
}
else {
alert("<%:Error%>");
}
});
}
//]]>
</script>
@@ -256,7 +256,7 @@ local appname = api.appname
let new_val = el.target.value
const new_hasItem = shunt_list.find(element => element.id == new_val);
if (new_hasItem) {
XHR.get('<%=api.url("update_node")%>', {
XHR.get('<%=api.url("update_config")%>', {
id: "<%=self.global_cfgid%>",
data: JSON.stringify({
tcp_node: new_val
@@ -53,6 +53,22 @@
}
}
}
document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="shunt_group"]', function(el) {
let o_val = el.value;
el.addEventListener("change", () => {
el.blur();
if (o_val != el.value) {
let save = true;
if (save) {
update_config("<%=self.id%>", {
shunt_group: getOption("<%=self.config%>", "<%=self.s_cfgid%>", "shunt_group").value,
}, get_current_url());
}
}
});
});
});
document.addEventListener("DOMContentLoaded", () => setTimeout(() => {
refresh_depends();
const table_dom = document.getElementById("cbi-passwall-shunt_option_list");
@@ -1,6 +1,7 @@
<%
local api = require "luci.passwall.api"
-%>
<%+header%>
<script type="text/javascript">
//<![CDATA[
var log_textarea = null;
@@ -43,3 +44,4 @@ local api = require "luci.passwall.api"
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clearlog()" value="<%:Clear logs%>" />
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="40" wrap="off" readonly="readonly"></textarea>
</fieldset>
<%+footer%>
@@ -1,8 +1,8 @@
<%
local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=self.api.url("node_config")%>/' + node_id;
document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
let o_val = el.value;
@@ -11,13 +11,18 @@ local api = self.api
if (o_val != el.value) {
let save = true;
if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click();
update_node({
remarks: getOption("remarks").value,
group: getOption("group").value,
type: getOption("type").value,
/*
const saveBtn = document.getElementsByClassName('cbi-button-save');
if (saveBtn && saveBtn.length > 0) {
saveBtn[saveBtn.length - 1].click();
}
*/
update_config(node_id, {
remarks: getOption("<%=self.config%>", "<%=self.section%>", "remarks").value,
group: getOption("<%=self.config%>", "<%=self.section%>", "group").value,
type: getOption("<%=self.config%>", "<%=self.section%>", "type").value,
//protocol: ''
});
}, node_config_url);
}
}
});
@@ -31,12 +36,17 @@ local api = self.api
let name = name_split[name_split.length - 1];
let save = true;
if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click();
update_node({
remarks: getOption("remarks").value,
group: getOption("group").value,
protocol: getOption(name).value
});
/*
const saveBtn = document.getElementsByClassName('cbi-button-save');
if (saveBtn && saveBtn.length > 0) {
saveBtn[saveBtn.length - 1].click();
}
*/
update_config(node_id, {
remarks: getOption("<%=self.config%>", "<%=self.section%>", "remarks").value,
group: getOption("<%=self.config%>", "<%=self.section%>", "group").value,
protocol: getOption("<%=self.config%>", "<%=self.section%>", name).value,
}, node_config_url);
} else {
window.location.href = node_config_url + "?select_proto=" + el.value;
}
@@ -1,64 +1,11 @@
<%
local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("node_config")%>/' + node_id;
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getOption(opt) {
var obj;
var id = 'cbid.<%=self.config%>.' + node_id + '.' + opt;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
var combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
var div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
function update_node(data) {
XHR.get('<%=api.url("update_node")%>', {
id: node_id,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
window.location.href = node_config_url;
}
else {
alert("<%:Error%>");
}
});
}
function fetchCertSha256(el) {
if (el.getAttribute("data-loading") === "1") return;
el.setAttribute("data-loading", "1");
el.style.pointerEvents = "none";
el.style.color = "red";
XHR.get('<%=api.url("fetch_certsha256")%>', { id: node_id }, function(x, res) {
XHR.get('<%=self.api.url("fetch_certsha256")%>', { id: "<%=self.section%>" }, function(x, res) {
el.setAttribute("data-loading", "0");
el.style.pointerEvents = "";
el.style.color = "";
@@ -1,556 +0,0 @@
<%
local api = require "luci.passwall.api"
-%>
<script type="text/javascript">
//<![CDATA[
function ajax_add_node(link, group) {
const dom = document.getElementById('modal-mask');
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
const chunkSize = 1000; // 分片发送以突破uhttpd的限制,每块1000字符
const totalChunks = Math.ceil(link.length / chunkSize);
let currentChunk = 0;
function sendNextChunk() {
if (currentChunk < totalChunks) {
const chunk = link.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const xhr = new XMLHttpRequest();
xhr.open('POST', '<%=api.url("link_add_node")%>', true);
xhr.onload = function () {
if (xhr.status === 200) {
currentChunk++;
sendNextChunk();
} else {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Error%>");
return;
}
};
xhr.onerror = function () {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Network Error%>");
return;
};
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("chunk_index", currentChunk);
formData.append("total_chunks", totalChunks);
formData.append("group", group);
xhr.send(formData);
} else {
window.location.href = '<%=api.url("node_list")%>';
}
}
sendNextChunk();
}
function decodeIfBase64(str) {
try {
let s = str.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const decoded = decodeURIComponent(
atob(s).split('').map(c =>
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
).join('')
);
if (btoa(unescape(encodeURIComponent(decoded))).replace(/=+$/, '') === s.replace(/=+$/, '')) {
return decoded;
}
} catch (e) {}
return str;
}
function getBg(el) {
if (!el) return null;
const style = getComputedStyle(el);
const bgImage = style.backgroundImage;
const bgColor = style.backgroundColor;
return (bgImage !== 'none' || !/rgba\([^,]+,[^,]+,[^,]+,\s*0\)/.test(bgColor) && bgColor !== 'transparent')
? style.background
: null;
};
function open_add_link_div() {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const addLinkDiv = document.getElementById("add_link_div");
addLinkDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
addLinkDiv.style.display = "block";
if (!addLinkDiv._dropdown_inited) {
addLinkDiv._dropdown_inited = true;
dropdown_list_fun("addlink_group_custom");
}
document.getElementById("nodes_link").focus();
}
function close_add_link_div() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("add_link_div").style.display = "none";
document.body.classList.remove('modal-open');
}
function add_node() {
let nodes_link = document.getElementById("nodes_link").value;
const group = (document.querySelector('#addlink_group_custom input[type="hidden"]')?.value || "default");
nodes_link = nodes_link.replace(/\t/g, "").replace(/\r\n|\r/g, "\n").replace(/<[^>]*>/g, '').trim();
if (nodes_link != "") {
nodes_link = decodeIfBase64(nodes_link);
let s = nodes_link.split('://');
if (s.length > 1) {
close_add_link_div();
ajax_add_node(nodes_link, group);
} else {
alert("<%:Please enter the correct link.%>");
}
} else {
document.getElementById("nodes_link").focus();
}
}
function clear_all_nodes() {
if (confirm('<%:Are you sure to clear all nodes?%>') == true){
XHR.get('<%=api.url("clear_all_nodes")%>', null,
function(x, data) {
if(x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
function open_reassign_group_div() {
const ids = [];
const visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const reassignGroupDiv = document.getElementById("reassign_group_div");
reassignGroupDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
reassignGroupDiv.style.display = "block";
if (!reassignGroupDiv._dropdown_inited) {
reassignGroupDiv._dropdown_inited = true;
dropdown_list_fun("reassign_group_custom");
}
} else {
alert("<%:You no select nodes !%>");
}
}
}
function close_reassign_group_div() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("reassign_group_div").style.display = "none";
document.body.classList.remove('modal-open');
}
function reassign_group() {
const ids = [];
const group = (document.querySelector('#reassign_group_custom input[type="hidden"]')?.value || "default");
const visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
XHR.get('<%=api.url("reassign_group")%>', {
group: group,
ids: ids.join(",")
},
function(x, data) {
if (x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
}
//自定义分组下拉列表事件
function dropdown_list_fun(div_id) {
const dropdown = document.getElementById(div_id);
if (!dropdown) return;
const display = dropdown.querySelector(".selected-display");
const displayText = display.querySelector(".text");
const list = dropdown.querySelector(".dropdown-list");
const hidden = dropdown.querySelector('input[type="hidden"]');
const input = dropdown.querySelector(".create-item-input");
display.addEventListener("click", function() {
list.style.display = list.style.display === "none" ? "block" : "none";
input.value = "";
//input.focus();
});
function selectItem(li) {
list.querySelectorAll(".dropdown-item").forEach(function(el){
el.classList.remove("selected");
});
li.classList.add("selected");
hidden.value = li.dataset.value;
displayText.textContent = li.dataset.value || "<%:default%>";
list.style.display = "none";
}
list.addEventListener("click", function(e){
const li = e.target.closest(".dropdown-item");
if (!li || li.classList.contains("custom-input")) return;
selectItem(li);
});
input.addEventListener("keydown", function(e){
const isEnter = e.key === "Enter" || e.keyCode === 13;
if (!isEnter) return;
e.stopPropagation();
e.preventDefault();
const val = input.value.trim();
if (!val) return;
if (val.toLowerCase() === "default") {
const emptyLi = Array.from(list.querySelectorAll(".dropdown-item"))
.find(function(el){ return !el.dataset.value; });
if (emptyLi) selectItem(emptyLi);
input.value = "";
return;
}
let li = Array.from(list.querySelectorAll(".dropdown-item")).find(function(el){
return el.dataset.value && el.dataset.value.toLowerCase() === val.toLowerCase();
});
if (!li) {
li = document.createElement("li");
li.className = "dropdown-item";
li.dataset.value = val;
li.textContent = val;
list.insertBefore(li, input.parentNode);
}
input.value = "";
selectItem(li);
});
// 从tab中读取分组名称
function InsertGroup(list, input) {
const tabs = document.querySelectorAll(".cbi-tabmenu li");
if (!tabs.length) {
setTimeout(() => InsertGroup(list, input), 50);
return;
}
tabs.forEach(function(li){
const group = li.id.split('.').pop();
if(group === "default") return;
if(Array.from(list.querySelectorAll(".dropdown-item")).some(el => el.dataset.value === group)) return;
const newLi = document.createElement("li");
newLi.className = "dropdown-item";
newLi.dataset.value = group;
newLi.textContent = group;
list.insertBefore(newLi, input.parentNode);
});
}
InsertGroup(list, input);
// 点击外部时自动收起
document.addEventListener("click", function(e) {
if (!dropdown.contains(e.target)) {
list.style.display = "none";
}
});
}
//]]>
</script>
<div id="modal-mask"></div>
<div id="add_link_div">
<div id="add_link_modal_container">
<h3><%:Add the node via the link%></h3>
<div class="value-custom">
<textarea id="nodes_link" rows="10"></textarea>
<p id="nodes_link_text"><%:Enter share links, one per line. Subscription links are not supported!%></p>
</div>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="addlink_group_custom"><%:Group Name%></label>
<div id="addlink_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="addlink_group" value="">
</div>
</div>
</div>
<div id="add_link_button_container">
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_add_link_div()" value="<%:Close%>" />
</div>
</div>
</div>
<div id="reassign_group_div">
<div id="reassign_group_modal_container">
<h3><%:Reassign Node Group%></h3>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="reassign_group_custom"><%:Group Name%></label>
<div id="reassign_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="to_group" value="">
</div>
</div>
</div>
<div id="reassign_group_button_container">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group()" value="<%:Save%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_reassign_group_div()" value="<%:Close%>" />
</div>
</div>
</div>
<div class="pw-toolbar">
<div class="pw-toolbar-field">
<input class="btn cbi-button cbi-button-add" type="button" onclick="to_add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-add" type="button" onclick="open_add_link_div()" value="<%:Add the node via the link%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clear_all_nodes()" value="<%:Clear all nodes%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="delete_select_nodes()" value="<%:Delete select nodes%>" />
<input class="btn cbi-button cbi-button-edit" type="button" id="select_all_btn" onclick="checked_all_node(this)" value="<%:Select all%>" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="open_reassign_group_div()" value="<%:Reassign Group%>" />
<input class="btn cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" />
<!--<input class="btn cbi-button cbi-button-save" type="submit" name="cbi.save" value="<%:Save%>" />-->
<!--<input class="btn cbi-button cbi-button-reset" type="button" value="<%:Reset%>" onclick="location.href='<%=REQUEST_URI%>'" />-->
</div>
</div>
<style>
.pw-toolbar {
width: 100%;
text-align: center;
}
.pw-toolbar-field {
width: 100%;
text-align: center;
display: inline-block;
padding: 5px 0 5px;
}
#modal-mask {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.4);
z-index: 999;
}
body.modal-open {
overflow: hidden;
pointer-events: none;
}
body.modal-open #add_link_div,
body.modal-open #reassign_group_div {
pointer-events: auto;
}
#add_link_div, #reassign_group_div {
display: none;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
width: 90%;
max-width: 500px;
}
#add_link_modal_container, #reassign_group_modal_container {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20px;
}
#nodes_link {
width: 100%;
height: 180px;
resize: vertical;
font-family: monospace;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
#add_link_div h3,
#reassign_group_div h3 {
background: inherit;
}
#nodes_link_text {
color: red;
font-size: 14px;
margin-top: 5px;
text-align: center;
width: 100%;
}
#add_link_button_container, #reassign_group_button_container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 300px;
margin-top: 10px;
}
.value-custom {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin: 10px 0;
padding: 0px 5px 0px 5px;
}
.value-field-custom {
display: inline-flex;
align-items: center;
gap: 10px;
}
.value-title-custom {
font-size: 13px;
line-height: 28px;
white-space: nowrap;
text-align: right;
}
.custom-dropdown {
position: relative;
border: 1px solid #d9d9d9;
border-radius: 2px;
width: 180px;
height: 28px;
font-size: 13px;
background: #fff;
cursor: pointer;
box-sizing: border-box;
display: block;
}
.selected-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 12px;
color: #666;
}
.selected-display:hover {
background-color: #f7f7f7;
}
.dropdown-list {
position: absolute;
top: calc(100% + 2px);
left: -1px;
width: calc(100% + 2px);
border: 1px solid #d9d9d9;
border-radius: 2px;
border-top: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
background: #fff;
list-style: none;
margin: 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
z-index: 100;
box-sizing: border-box;
color: #666;
}
.dropdown-item {
padding: 4px 8px;
line-height: 20px;
cursor: pointer;
}
.dropdown-item.selected {
background-color: #1e90ff;
color: #fff;
}
.create-item-input::placeholder {
text-align: center;
}
.dropdown-item.custom-input input {
width: 100%;
box-sizing: border-box;
padding: 3px;
font-size: 13px;
line-height: 20px;
border: 1px solid #ccc;
text-align: left;
}
.arrow-down-small {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #555;
display: inline-block;
vertical-align: middle;
}
</style>
@@ -166,8 +166,205 @@ table td, .table .td {
}
</style>
<% if api.is_js_luci() then -%>
<style>
.pw-toolbar {
width: 100%;
text-align: center;
}
.pw-toolbar-field {
width: 100%;
text-align: center;
display: inline-block;
padding: 5px 0 5px;
}
#modal-mask {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.4);
z-index: 999;
}
body.modal-open {
overflow: hidden;
pointer-events: none;
}
body.modal-open #add_link_div,
body.modal-open #reassign_group_div {
pointer-events: auto;
}
#add_link_div, #reassign_group_div {
display: none;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
width: 90%;
max-width: 500px;
}
#add_link_modal_container, #reassign_group_modal_container {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20px;
}
#nodes_link {
width: 100%;
height: 180px;
resize: vertical;
font-family: monospace;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
#add_link_div h3,
#reassign_group_div h3 {
background: inherit;
}
#nodes_link_text {
color: red;
font-size: 14px;
margin-top: 5px;
text-align: center;
width: 100%;
}
#add_link_button_container, #reassign_group_button_container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 300px;
margin-top: 10px;
}
.value-custom {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin: 10px 0;
padding: 0px 5px 0px 5px;
}
.value-field-custom {
display: inline-flex;
align-items: center;
gap: 10px;
}
.value-title-custom {
font-size: 13px;
line-height: 28px;
white-space: nowrap;
text-align: right;
}
.custom-dropdown {
position: relative;
border: 1px solid #d9d9d9;
border-radius: 2px;
width: 180px;
height: 28px;
font-size: 13px;
background: #fff;
cursor: pointer;
box-sizing: border-box;
display: block;
}
.selected-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 12px;
color: #666;
}
.selected-display:hover {
background-color: #f7f7f7;
}
.dropdown-list {
position: absolute;
top: calc(100% + 2px);
left: -1px;
width: calc(100% + 2px);
border: 1px solid #d9d9d9;
border-radius: 2px;
border-top: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
background: #fff;
list-style: none;
margin: 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
z-index: 100;
box-sizing: border-box;
color: #666;
}
.dropdown-item {
padding: 4px 8px;
line-height: 20px;
cursor: pointer;
}
.dropdown-item.selected {
background-color: #1e90ff;
color: #fff;
}
.create-item-input::placeholder {
text-align: center;
}
.dropdown-item.custom-input input {
width: 100%;
box-sizing: border-box;
padding: 3px;
font-size: 13px;
line-height: 20px;
border: 1px solid #ccc;
text-align: left;
}
.arrow-down-small {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #555;
display: inline-block;
vertical-align: middle;
}
</style>
<script type="text/javascript">
<% if api.is_js_luci() then -%>
var cbi_t = [];
function cbi_t_add(section, tab) {
var t = document.getElementById('tab.' + section + '.' + tab);
@@ -200,9 +397,7 @@ table td, .table .td {
}
return false
}
</script>
<%- else %>
<script type="text/javascript">
(function() {
if (typeof(cbi_t_switch) === "function") {
var old_switch = cbi_t_switch;
@@ -212,16 +407,16 @@ table td, .table .td {
};
}
})();
</script>
<%- end %>
</script>
<script type="text/javascript">
//<![CDATA[
let auto_detection_time = "<%=api.uci_get_type("global_other", "auto_detection_time", "0")%>"
let show_node_info = "<%=api.uci_get_type("global_other", "show_node_info", "0")%>"
var ajaxList = [];
var ajax = {
list: [],
post: function(url, data, fn_success, timeout, fn_timeout) {
var xhr = new XMLHttpRequest();
var code = ajax.encode(data);
@@ -250,7 +445,7 @@ table td, .table .td {
fn_success(xhr, json);
}
};
ajaxList.push(xhr);
this.list.push(xhr);
xhr.send(code);
},
encode: function(obj) {
@@ -270,65 +465,63 @@ table td, .table .td {
}
return obj;
},
abortAll: function() {
this.list.forEach(xhr => xhr.abort());
this.list = [];
}
}
function ajaxAbortAll() {
ajaxList.forEach(xhr => xhr.abort());
ajaxList = [];
}
function to_add_node() {
ajaxAbortAll();
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
window.location.href = '<%=api.url("add_node")%>?redirect=1&group=' + current_group;
}
function to_edit_node(cbi_id) {
ajaxAbortAll();
location.href='<%=api.url("node_config")%>/' + cbi_id;
}
function copy_node(cbi_id) {
ajaxAbortAll();
window.location.href = '<%=api.url("copy_node")%>' + "?section=" + cbi_id;
}
function del_node(cbi_id) {
ajaxAbortAll();
window.location.href = '<%=api.url("delete_select_nodes")%>' + "?redirect=1&ids=" + cbi_id;
}
var section = "";
function open_set_node_div(cbi_id) {
section = cbi_id;
document.getElementById("set_node_div").style.display="block";
var node_name = document.getElementById("cbid.<%=appname%>." + cbi_id + ".remarks").value;
document.getElementById("set_node_name").innerHTML = node_name;
}
function close_set_node_div() {
document.getElementById("set_node_div").style.display="none";
document.getElementById("set_node_name").innerHTML = "";
}
function row_top(btn, group) {
const row = btn.closest("tr");
if (!row) return;
const parent = row.parentNode;
let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)");
if (firstDataRow && firstDataRow !== row) {
parent.insertBefore(row, firstDataRow);
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_node_order(group);
if (!arraysEqual(new_order, origin_group_node_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
function node_action(action, cbi_id, a) {
ajax.abortAll();
switch (action) {
case "add":
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
window.location.href='<%=api.url("add_node")%>?redirect=1&group=' + current_group;
break;
case "copy":
window.location.href = '<%=api.url("copy_node")%>' + "?section=" + cbi_id;
break;
case "edit":
window.location.href='<%=api.url("node_config")%>/' + cbi_id;
break;
case "del":
window.location.href = '<%=api.url("delete_select_nodes")%>' + "?redirect=1&ids=" + cbi_id;
break;
case "row_top":
let btn = a.btn;
let group = a.group;
const row = btn.closest("tr");
if (!row) return;
const parent = row.parentNode;
let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)");
if (firstDataRow && firstDataRow !== row) {
parent.insertBefore(row, firstDataRow);
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_node_order(group);
if (!arraysEqual(new_order, origin_group_node_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
}
}
}
}
break;
case "del_all":
if (confirm('<%:Are you sure to clear all nodes?%>') == true){
XHR.get('<%=api.url("clear_all_nodes")%>', null,
function(x, data) {
if(x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
break;
default:
}
}
@@ -403,6 +596,7 @@ table td, .table .td {
if (confirm('<%:Are you sure to delete select nodes?%>') == true) {
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
ajax.abortAll();
XHR.get('<%=api.url("delete_select_nodes")%>', {
ids: ids.join()
},
@@ -416,7 +610,7 @@ table td, .table .td {
box.remove();
}
*/
ajaxAbortAll();
ajax.abortAll();
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
@@ -430,13 +624,6 @@ table td, .table .td {
}
}
function set_node(protocol) {
if (confirm('<%:Are you sure set to%> ' + protocol.toUpperCase() + '<%:the server?%>')==true){
ajaxAbortAll();
window.location.href = '<%=api.url("set_node")%>?protocol=' + protocol + '&section=' + section;
}
}
function get_address_full(id) {
var address = (document.getElementById("cbid.<%=appname%>." + id + ".address") || {}).value || "";
var port = (document.getElementById("cbid.<%=appname%>." + id + ".port") || {}).value || "";
@@ -484,6 +671,7 @@ table td, .table .td {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
ajax.abortAll();
XHR.get('<%=api.url("save_node_order")%>', {
group: group,
ids: ids.join(",")
@@ -741,16 +929,6 @@ table td, .table .td {
}
}
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
//列表拖动重排
function initSortableForTable(table) {
if (!table) return null;
@@ -821,6 +999,292 @@ table td, .table .td {
}
</script>
<script type="text/javascript">
//<![CDATA[
//Pop-up logic
// Custom group dropdown list events
function dropdown_list_fun(div_id) {
const dropdown = document.getElementById(div_id);
if (!dropdown) return;
const display = dropdown.querySelector(".selected-display");
const displayText = display.querySelector(".text");
const list = dropdown.querySelector(".dropdown-list");
const hidden = dropdown.querySelector('input[type="hidden"]');
const input = dropdown.querySelector(".create-item-input");
display.addEventListener("click", function() {
list.style.display = list.style.display === "none" ? "block" : "none";
input.value = "";
//input.focus();
});
function selectItem(li) {
list.querySelectorAll(".dropdown-item").forEach(function(el){
el.classList.remove("selected");
});
li.classList.add("selected");
hidden.value = li.dataset.value;
displayText.textContent = li.dataset.value || "<%:default%>";
list.style.display = "none";
}
list.addEventListener("click", function(e){
const li = e.target.closest(".dropdown-item");
if (!li || li.classList.contains("custom-input")) return;
selectItem(li);
});
input.addEventListener("keydown", function(e){
const isEnter = e.key === "Enter" || e.keyCode === 13;
if (!isEnter) return;
e.stopPropagation();
e.preventDefault();
const val = input.value.trim();
if (!val) return;
if (val.toLowerCase() === "default") {
const emptyLi = Array.from(list.querySelectorAll(".dropdown-item"))
.find(function(el){ return !el.dataset.value; });
if (emptyLi) selectItem(emptyLi);
input.value = "";
return;
}
let li = Array.from(list.querySelectorAll(".dropdown-item")).find(function(el){
return el.dataset.value && el.dataset.value.toLowerCase() === val.toLowerCase();
});
if (!li) {
li = document.createElement("li");
li.className = "dropdown-item";
li.dataset.value = val;
li.textContent = val;
list.insertBefore(li, input.parentNode);
}
input.value = "";
selectItem(li);
});
// Read group names from tab
function InsertGroup(list, input) {
const tabs = document.querySelectorAll(".cbi-tabmenu li");
if (!tabs.length) {
setTimeout(() => InsertGroup(list, input), 50);
return;
}
tabs.forEach(function(li){
const group = li.id.split('.').pop();
if(group === "default") return;
if(Array.from(list.querySelectorAll(".dropdown-item")).some(el => el.dataset.value === group)) return;
const newLi = document.createElement("li");
newLi.className = "dropdown-item";
newLi.dataset.value = group;
newLi.textContent = group;
list.insertBefore(newLi, input.parentNode);
});
}
InsertGroup(list, input);
// Automatically collapses when clicking on the outside.
document.addEventListener("click", function(e) {
if (!dropdown.contains(e.target)) {
list.style.display = "none";
}
});
}
function getBg(el) {
if (!el) return null;
const style = getComputedStyle(el);
const bgImage = style.backgroundImage;
const bgColor = style.backgroundColor;
return (bgImage !== 'none' || !/rgba\([^,]+,[^,]+,[^,]+,\s*0\)/.test(bgColor) && bgColor !== 'transparent')
? style.background
: null;
}
const add_link_div = {
open() {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const addLinkDiv = document.getElementById("add_link_div");
addLinkDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
addLinkDiv.style.display = "block";
if (!addLinkDiv._dropdown_inited) {
addLinkDiv._dropdown_inited = true;
dropdown_list_fun("addlink_group_custom");
}
document.getElementById("nodes_link").focus();
},
close() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("add_link_div").style.display = "none";
document.body.classList.remove('modal-open');
},
add_node() {
let nodes_link = document.getElementById("nodes_link").value;
const group = (document.querySelector('#addlink_group_custom input[type="hidden"]')?.value || "default");
nodes_link = nodes_link.replace(/\t/g, "").replace(/\r\n|\r/g, "\n").replace(/<[^>]*>/g, '').trim();
if (nodes_link != "") {
nodes_link = decodeIfBase64(nodes_link);
let s = nodes_link.split('://');
if (s.length > 1) {
this.close();
this.ajax_add_node(nodes_link, group);
} else {
alert("<%:Please enter the correct link.%>");
}
} else {
document.getElementById("nodes_link").focus();
}
},
ajax_add_node(link, group) {
const dom = document.getElementById('modal-mask');
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
const chunkSize = 1000; // Fragmented sending to overcome uhttpd's limitations, with each fragment containing 1000 characters.
const totalChunks = Math.ceil(link.length / chunkSize);
let currentChunk = 0;
function sendNextChunk() {
if (currentChunk < totalChunks) {
const chunk = link.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const xhr = new XMLHttpRequest();
xhr.open('POST', '<%=api.url("link_add_node")%>', true);
xhr.onload = function () {
if (xhr.status === 200) {
currentChunk++;
sendNextChunk();
} else {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Error%>");
return;
}
};
xhr.onerror = function () {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Network Error%>");
return;
};
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("chunk_index", currentChunk);
formData.append("total_chunks", totalChunks);
formData.append("group", group);
ajax.abortAll();
xhr.send(formData);
} else {
window.location.href = '<%=api.url("node_list")%>';
}
}
sendNextChunk();
}
}
const reassign_group_div = {
open() {
const ids = [];
const visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const reassignGroupDiv = document.getElementById("reassign_group_div");
reassignGroupDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
reassignGroupDiv.style.display = "block";
if (!reassignGroupDiv._dropdown_inited) {
reassignGroupDiv._dropdown_inited = true;
dropdown_list_fun("reassign_group_custom");
}
} else {
alert("<%:You no select nodes !%>");
}
}
},
close() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("reassign_group_div").style.display = "none";
document.body.classList.remove('modal-open');
},
action() {
const ids = [];
const group = (document.querySelector('#reassign_group_custom input[type="hidden"]')?.value || "default");
const visibleContainer = document.querySelector('#cbi-passwall-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
ajax.abortAll();
XHR.get('<%=api.url("reassign_group")%>', {
group: group,
ids: ids.join(",")
},
function(x, data) {
if (x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
}
}
const set_node_div = {
section: '',
open(cbi_id) {
this.section = cbi_id;
document.getElementById("set_node_div").style.display="block";
const node_name = document.getElementById("cbid.<%=appname%>." + cbi_id + ".remarks").value;
document.getElementById("set_node_name").innerHTML = node_name;
},
close() {
document.getElementById("set_node_div").style.display="none";
document.getElementById("set_node_name").innerHTML = "";
},
set(protocol) {
if (confirm('<%:Are you sure set to%> ' + protocol.toUpperCase() + ' <%:the server?%>') == true){
ajax.abortAll();
window.location.href = '<%=api.url("set_node")%>?protocol=' + protocol + '&section=' + this.section;
}
}
}
//]]>
</script>
<div class="pw-toolbar">
<div class="pw-toolbar-field">
<input class="btn cbi-button cbi-button-add" type="button" onclick="node_action('add')" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_link_div.open()" value="<%:Add the node via the link%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="node_action('del_all')" value="<%:Clear all nodes%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="delete_select_nodes()" value="<%:Delete select nodes%>" />
<input class="btn cbi-button cbi-button-edit" type="button" id="select_all_btn" onclick="checked_all_node(this)" value="<%:Select all%>" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group_div.open()" value="<%:Reassign Group%>" />
<input class="btn cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" />
<!--<input class="btn cbi-button cbi-button-save" type="submit" name="cbi.save" value="<%:Save%>" />-->
<!--<input class="btn cbi-button cbi-button-reset" type="button" value="<%:Reset%>" onclick="location.href='<%=REQUEST_URI%>'" />-->
</div>
</div>
<script type="text/template" id="nodes-table-template">
<fieldset class="cbi-section cbi-tblsection" id="cbi-<%=appname%>-nodes-{{group}}-fieldset">
<table class="table cbi-section-table" id="cbi-<%=appname%>-nodes-{{group}}-table" style="">
@@ -837,7 +1301,7 @@ table td, .table .td {
{{node-tr}}
</table>
<div class="cbi-section-create cbi-tblsection-create">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="to_add_node()">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="node_action('add')">
<input class="cbi-button cbi-button-apply" style="display: none" type="button" id="save_order_btn_{{group}}" value="<%:Save Order%>" onclick="save_current_page_order('{{group}}')">
</div>
</fieldset>
@@ -857,11 +1321,11 @@ table td, .table .td {
<td class="td cbi-value-field pw-urltest">{{url_test}}</td>
<td class="td cbi-section-table-cell nowrap cbi-section-actions pw-actions">
<div class="node-wrapper">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="row_top(this, '{{group}}')" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Use%>" id="apply_{{id}}" onclick="open_set_node_div('{{id}}')"/>
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Copy%>" onclick="copy_node('{{id}}')"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="to_edit_node('{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="del_node('{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="node_action('row_top', null, {btn:this, group:'{{group}}'})" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Use%>" id="apply_{{id}}" onclick="set_node_div.open('{{id}}')"/>
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Copy%>" onclick="node_action('copy', '{{id}}')"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="node_action('edit', '{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="node_action('del', '{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<span class="drag-handle center" title="<%:Drag to reorder%>">⠿</span>
</div>
</td>
@@ -872,6 +1336,8 @@ table td, .table .td {
</fieldset>
<script type="text/javascript">
//init logic
function get_remarks_name(o) {
let str = "";
let remarks = o["remarks"] || "";
@@ -948,9 +1414,8 @@ table td, .table .td {
group_nodes[_node.group].push(_node)
}
var tab_ul_html = '<ul class="cbi-tabmenu">'
var tab_ul_li_html = ''
var tab_content_html = '<fieldset class="cbi-section-node cbi-section-node-tabbed" id="cbi-<%=appname%>-nodes">'
var tab_content_html = ''
var nodes_table_template = document.getElementById("nodes-table-template");
var node_template = document.getElementById("node-tr-template");
var default_group = null
@@ -1025,11 +1490,21 @@ table td, .table .td {
'</div>'
}
tab_ul_html += tab_ul_li_html + '</ul>'
tab_content_html += '</fieldset>'
var tab_html = tab_ul_html + tab_content_html
function addTabDOM(li, content) {
let tab_ul_html = '<ul class="cbi-tabmenu">';
let tab_content_html = '<fieldset class="cbi-section-shunt_rule cbi-section-shunt_rule-tabbed" id="cbi-<%=appname%>-nodes">';
if (li) {
tab_ul_html += li;
}
if (content) {
tab_content_html += content;
}
tab_ul_html += '</ul>';
tab_content_html += '</fieldset>';
return tab_ul_html + tab_content_html;
}
document.getElementById("node_list").innerHTML = tab_html
document.getElementById("node_list").innerHTML = addTabDOM(tab_ul_li_html, tab_content_html);
for (let group in group_nodes) {
cbi_t_add("<%=appname%>.nodes", group)
@@ -1071,20 +1546,8 @@ table td, .table .td {
//Node list option saving logic
document.addEventListener("DOMContentLoaded", function () {
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function onChange(option, value) {
ajax.abortAll();
XHR.get('<%=api.url("save_node_list_opt")%>', {
option: option,
value: value
@@ -1118,23 +1581,88 @@ table td, .table .td {
links.forEach(link => {
link.addEventListener('click', (e) => {
if (link.getAttribute('target') !== '_blank') {
ajaxAbortAll();
ajax.abortAll();
}
});
});
window.addEventListener('beforeunload', (event) => {
ajax.abortAll();
});
});
//]]>
</script>
<div id="modal-mask"></div>
<div id="add_link_div">
<div id="add_link_modal_container">
<h3><%:Add the node via the link%></h3>
<div class="value-custom">
<textarea id="nodes_link" rows="10"></textarea>
<p id="nodes_link_text"><%:Enter share links, one per line. Subscription links are not supported!%></p>
</div>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="addlink_group_custom"><%:Group Name%></label>
<div id="addlink_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="addlink_group" value="">
</div>
</div>
</div>
<div id="add_link_button_container">
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_link_div.add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="add_link_div.close()" value="<%:Close%>" />
</div>
</div>
</div>
<div id="reassign_group_div">
<div id="reassign_group_modal_container">
<h3><%:Reassign Node Group%></h3>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="reassign_group_custom"><%:Group Name%></label>
<div id="reassign_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="to_group" value="">
</div>
</div>
</div>
<div id="reassign_group_button_container">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group_div.action()" value="<%:Save%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="reassign_group_div.close()" value="<%:Close%>" />
</div>
</div>
</div>
<div style="display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center;">
<div id="set_node_div" style="text-align: center;">
<div class="cbi-value" style="display: flex; justify-content: center; align-items: center; margin: 8px 0;">
<font color=#666><%:You choose node is:%></font><a style="color: red" id="set_node_name"></a>
</div>
<div class="cbi-value" style="display: flex; justify-content: center; gap: 8px; margin: 8px 0;">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node('tcp')" value="TCP" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node('udp')" value="UDP" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_set_node_div()" value="<%:Close%>" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node_div.set('tcp')" value="TCP" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node_div.set('udp')" value="UDP" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="set_node_div.close()" value="<%:Close%>" />
</div>
</div>
</div>
@@ -0,0 +1,465 @@
<%
local api = require "luci.passwall.api"
local appname = api.appname
local uci = api.uci
-%>
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
<style>
table th, .table .th {
text-align: center;
}
table td, .table .td {
text-align: center;
/* white-space: nowrap; */
word-break: keep-all;
}
.td.cbi-section-actions {
text-align: right !important;
}
.shunt_rule-wrapper {
align-items: center;
display: inline-flex !important;
gap: 4px;
}
.cbi-tabmenu > li {
margin-right: 2px !important;
}
.cbi-tabmenu > li:last-child {
margin-right: 0 !important;
}
.shunt_rule-wrapper .drag-handle {
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.shunt_rule-wrapper .drag-handle:hover {
background: transparent;
}
/* enable flex for small screens*/
@media screen and (max-width: 1152px) {
.cbi-section-table-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.cbi-section-table-titles {
display: none !important;
}
.td-actions {
padding-top: 0 !important;
border-top-width: 0 !important;
flex: 1 1 350px;
}
}
/* shrink actionbar even further for mobile devices */
@media screen and (max-width: 500px) {
.shunt_rule-wrapper {
gap: 0;
}
.cbi-button {
margin-left: 0 !important;
margin-right: 1px !important;
}
.td-actions {
padding-left: 5px!important;
padding-right: 5px !important;
}
}
.sortable-chosen {
background-color: rgba(220, 235, 245, 0.4) !important;
opacity: 0.7;
}
.sortable-ghost {
background: #cce5ff !important;
height: 3px !important;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
<% if api.is_js_luci() then -%>
<script type="text/javascript">
var cbi_t = [];
function cbi_t_add(section, tab) {
var t = document.getElementById('tab.' + section + '.' + tab);
var c = document.getElementById('container.' + section + '.' + tab);
if( t && c ) {
cbi_t[section] = (cbi_t[section] || [ ]);
cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
}
}
function cbi_t_switch(section, tab) {
if( cbi_t[section] && cbi_t[section][tab] ) {
// Before switching tabs, first deselect all currently active tabs.
var o = cbi_t[section][tab];
var h = document.getElementById('tab.' + section);
for( var tid in cbi_t[section] ) {
var o2 = cbi_t[section][tid];
if( o.tab.id != o2.tab.id ) {
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
o2.container.style.display = 'none';
}
else {
if(h) h.value = tab;
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
o2.container.style.display = 'block';
}
}
}
return false
}
</script>
<%- end %>
<script type="text/javascript">
//<![CDATA[
function toPage(action, cbi_id) {
if (action == "add") {
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
const add_name_val = document.getElementById("add_name_" + current_group).value;
if (add_name_val.length > 1) {
XHR.get('<%=api.url("add_shunt_rule")%>', {
add_name: add_name_val,
group: current_group
},
function(x, result) {
if (x && x.status === 200) {
if (result.code == 1) {
window.location.href = result.data.redirect_url;
} else {
alert(result.data.message);
}
} else {
alert("<%:Save failed!%>");
}
}
);
}
}
if (action == "edit") {
window.location.href='<%=api.url("shunt_rules")%>/' + cbi_id;
}
if (action == "del") {
window.location.href = '<%=api.url("delete_select_shunt_rules")%>' + "?redirect=1&ids=" + cbi_id;
}
}
function row_top(btn, group) {
const row = btn.closest("tr");
if (!row) return;
const parent = row.parentNode;
let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)");
if (firstDataRow && firstDataRow !== row) {
parent.insertBefore(row, firstDataRow);
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_shunt_rule_order(group);
if (!arraysEqual(new_order, origin_group_shunt_rule_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
}
}
}
}
function get_shunt_rule_order(group) {
let table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
if (!table) {
return;
}
let rows = table.querySelectorAll("tr.cbi-section-table-row");
if (!rows || rows.length === 0) {
return;
}
var ids = [];
rows.forEach(function(row) {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
return ids;
}
function save_current_page_order(group) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
if (!table) {
alert("<%:No table!%>");
return;
}
var rows = table.querySelectorAll("tr.cbi-section-table-row");
if (!rows || rows.length === 0) {
alert("<%:No shunt_rules!%>");
return;
}
var btn = document.getElementById("save_order_btn_" + group);
if (btn) {
btn.style.display = "none";
btn.disabled = true;
}
var ids = [];
rows.forEach(function(row) {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
XHR.get('<%=api.url("save_shunt_rule_order")%>', {
group: group,
ids: ids.join(",")
},
function(x, result) {
if (btn) {
btn.style.display = null;
btn.disabled = false;
}
if (x && x.status === 200) {
origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
alert("<%:Saved current page order successfully.%>");
if (btn) {
btn.style.display = "none";
}
} else {
alert("<%:Save failed!%>");
}
}
);
}
// List drag and rearrange
function initSortableForTable(table) {
if (!table) return null;
let group = table.id.replace("cbi-<%=appname%>-shunt_rules-", "").replace("-table", "")
var root = table.querySelector('tbody') || table;
if (root._sortable_initialized) return root._sortable_instance;
root._sortable_initialized = true;
var opts = {
handle: ".drag-handle",
draggable: "tr.cbi-section-table-row",
animation: 150,
ghostClass: "dragging-row",
fallbackOnBody: true,
forceFallback: false,
swapThreshold: 0.65,
onEnd: function (evt) {
//save_current_page_order(group); // Auto save
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_shunt_rule_order(group);
if (!arraysEqual(new_order, origin_group_shunt_rule_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
}
}
}
};
try {
var instance = Sortable.create(root, opts);
root._sortable_instance = instance;
return instance;
} catch (err) {
root._sortable_initialized = false;
console.error("Sortable init failed:", err);
return null;
}
}
function initAllSortable(group_shunt_rules) {
if (typeof Sortable === 'undefined') {
var retries = 0;
var maxRetries = 25;
var t = setInterval(function () {
retries++;
if (typeof Sortable !== 'undefined') {
clearInterval(t);
for (var group in group_shunt_rules) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
initSortableForTable(table);
}
} else if (retries >= maxRetries) {
clearInterval(t);
}
}, 200);
} else {
for (var group in group_shunt_rules) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
initSortableForTable(table);
}
}
}
function escape_html(s) {
return s.replace(/[&<>"']/g, c => ({
"&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;"
}[c]));
}
</script>
<script type="text/template" id="shunt_rules-table-template">
<fieldset class="cbi-section cbi-tblsection" id="cbi-<%=appname%>-shunt_rules-{{group}}-fieldset">
<table class="table cbi-section-table" id="cbi-<%=appname%>-shunt_rules-{{group}}-table" style="">
<tr class="tr cbi-section-table-titles anonymous">
<th class="th cbi-section-table-cell" style="width:30%">ID</th>
<th class="th cbi-section-table-cell" style="width:30%"><%:Remarks%></th>
<th class="th cbi-section-table-cell cbi-section-actions"></th>
</tr>
{{shunt_rule-tr}}
</table>
<div class="cbi-section-create cbi-tblsection-create">
<input type="text" id="add_name_{{group}}">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="toPage('add')">
<input class="cbi-button cbi-button-apply" style="display: none" type="button" id="save_order_btn_{{group}}" value="<%:Save Order%>" onclick="save_current_page_order('{{group}}')">
</div>
</fieldset>
</script>
<script type="text/template" id="shunt_rule-tr-template">
<tr class="tr cbi-section-table-row" id="cbi-<%=appname%>-{{id}}">
<td class="td cbi-value-field"><b>{{id}}</b></td>
<td class="td cbi-value-field">{{remarks}}</td>
<td class="td cbi-section-table-cell nowrap cbi-section-actions td-actions">
<div class="shunt_rule-wrapper">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="row_top(this, '{{group}}')" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="toPage('edit', '{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="toPage('del', '{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<span class="drag-handle center" title="<%:Drag to reorder%>">⠿</span>
</div>
</td>
</tr>
</script>
<div class="cbi-section">
<h3>Sing-Box/Xray <%:Shunt Rule%></h3>
<div class="cbi-section-descr"><a style="color: red"><%:Please note attention to the priority, the higher the order, the higher the priority.%></a></div>
<div id="shunt_rule_list"></div>
</div>
<script type="text/javascript">
function loadNodeList() {
XHR.get('<%=api.url("get_shunt_rules")%>', null, function(x, result) {
var shunt_rule_list = result
var group_shunt_rules = {}
for (let i = 0; i < shunt_rule_list.length; i++) {
let _shunt_rule = shunt_rule_list[i]
if (!_shunt_rule.group || _shunt_rule.group === "") {
_shunt_rule.group = "default"
}
if (!group_shunt_rules[_shunt_rule.group]) {
group_shunt_rules[_shunt_rule.group] = []
}
group_shunt_rules[_shunt_rule.group].push(_shunt_rule)
}
var tab_ul_li_html = '';
var tab_content_html = '';
var shunt_rules_table_template = document.getElementById("shunt_rules-table-template");
var shunt_rule_template = document.getElementById("shunt_rule-tr-template");
var default_group = null
for (let group in group_shunt_rules) {
if (default_group == null)
default_group = group
var table_html = "";
if (true) {
var new_shunt_rules_table_dom = shunt_rules_table_template.cloneNode(true);
var _html = new_shunt_rules_table_dom.innerHTML;
_html = _html.split("{{group}}").join(group);
var shunt_rule_tr_html = "";
for (var i = 0; i < group_shunt_rules[group].length; i++) {
let o = group_shunt_rules[group][i]
var newDom = shunt_rule_template.cloneNode(true);
newDom.classList.add("cbi-rowstyle-" + (i % 2 + 1));
var innerHTML = newDom.innerHTML;
innerHTML = innerHTML.split("{{id}}").join(o[".name"]);
innerHTML = innerHTML.split("{{group}}").join(o["group"] || "");
innerHTML = innerHTML.split("{{remarks}}").join(o["remarks"] || "");
shunt_rule_tr_html += innerHTML
}
_html = _html.split("{{shunt_rule-tr}}").join(shunt_rule_tr_html);
table_html = _html;
}
var group_name = group
if (group === "default") {
group_name = "<%:default%>"
}
tab_ul_li_html +=
'<li group_name="' + group + '" id="tab.<%=appname%>.shunt_rules.' + group + '" class="cbi-tab">' +
'<a onclick="this.blur(); return cbi_t_switch(\'<%=appname%>.shunt_rules\', \'' + group + '\')" href="<%=REQUEST_URI%>?tab.<%=appname%>.shunt_rules=' + group + '">' + escape_html(group_name) + " | " + "<font style='color: red'>" + group_shunt_rules[group].length + '</font></a>' +
'</li>'
tab_content_html +=
'<div class="cbi-tabcontainer" id="container.<%=appname%>.shunt_rules.' + group + '" style="display: none;">' +
'' + table_html +
'</div>'
}
function addTabDOM(li, content) {
let tab_ul_html = '<ul class="cbi-tabmenu">';
let tab_content_html = '<fieldset class="cbi-section-shunt_rule cbi-section-shunt_rule-tabbed" id="cbi-<%=appname%>-shunt_rules">';
if (li) {
tab_ul_html += li;
}
if (content) {
tab_content_html += content;
}
tab_ul_html += '</ul>';
tab_content_html += '</fieldset>';
return tab_ul_html + tab_content_html;
}
document.getElementById("shunt_rule_list").innerHTML = addTabDOM(tab_ul_li_html, tab_content_html);
for (let group in group_shunt_rules) {
cbi_t_add("<%=appname%>.shunt_rules", group)
}
if (default_group) {
cbi_t_switch("<%=appname%>.shunt_rules", default_group)
}
origin_group_shunt_rule_order = {};
for (let group in group_shunt_rules) {
origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
}
initAllSortable(group_shunt_rules);
});
}
loadNodeList();
//]]>
</script>
@@ -3,6 +3,9 @@ local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("server_user")%>/' + node_id;
document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
let o_val = el.value;
@@ -12,12 +15,12 @@ local api = self.api
let save = true;
if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click();
update_node({
enable: getOption("enable").value,
remarks: getOption("remarks").value,
type: getOption("type").value,
server_update_config(node_id, {
enable: getOption("<%=self.config%>", node_id, "enable").value,
remarks: getOption("<%=self.config%>", node_id, "remarks").value,
type: getOption("<%=self.config%>", node_id, "type").value,
protocol: ''
});
}, node_config_url);
}
}
});
@@ -1,51 +1,15 @@
<%
local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("server_user")%>/' + node_id;
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getOption(opt) {
var obj;
var id = 'cbid.<%=self.config%>.' + node_id + '.' + opt;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
var combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
var div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
function update_node(data) {
XHR.get('<%=api.url("server_user_update")%>', {
id: node_id,
function server_update_config(cfgid, data, redirect_url) {
XHR.get('<%=self.api.url("server_update_config")%>', {
id: cfgid,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
window.location.href = node_config_url;
if (redirect_url) {
window.location.href = redirect_url;
}
}
else {
alert("<%:Error%>");
+5 -5
View File
@@ -1127,18 +1127,18 @@ msgstr "启用此选项后手动更新时,即使没有新版本也会从现有 G
msgid "Enable Geo Data Parsing"
msgstr "开启 Geo 数据解析"
msgid "Analyzes and preloads GeoIP/Geosite data to enhance the shunt performance of Sing-box/Xray."
msgstr "分析和预加载 GeoIP/Geosite 数据,以增强 Sing-box/Xray 的分流效果。"
msgid "Parses and preloads GeoIP/Geosite data to improve Sing-box/Xray routing performance."
msgstr "解析和预加载 GeoIP/Geosite 数据,以增强 Sing-box/Xray 的分流效果。"
msgid "Once enabled, the rule list can support GeoIP/Geosite rules."
msgstr "启用后,规则列表可以支持 GeoIP/Geosite 规则。"
msgid "Note: Increases resource usage; Geosite analysis is only supported in ChinaDNS-NG and SmartDNS modes."
msgstr "注:会增加一些系统资源的开销,仅在 ChinaDNS-NG 和 SmartDNS 模式下支持分析 Geosite 。"
msgid "Shunt Rule"
msgstr "分流规则"
msgid "Shunt Rule Group"
msgstr "分流规则组"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "请注意优先级问题,排序越上面优先级越高。"
@@ -227,7 +227,7 @@ if USE_DIRECT_LIST == "1" and not fs.access(file_direct_host) then
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") then
if line ~= "" and not line:find("#") and not line:find(":") then
insert_unique(direct_domain, line, lookup_direct_domain)
end
end
@@ -277,7 +277,7 @@ if USE_PROXY_LIST == "1" and not fs.access(file_proxy_host) then
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") then
if line ~= "" and not line:find("#") and not line:find(":") then
insert_unique(proxy_domain, line, lookup_proxy_domain)
end
end
@@ -391,14 +391,14 @@ if IS_SHUNT_NODE then
local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" then
if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then
if _node_id == "_default" then
_node_id = default_node_id
end
local domain_list = s.domain_list or ""
for line in string.gmatch(domain_list, "[^\r\n]+") do
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") then
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") and not line:find("rule-set:") and not line:find("rs:") then
if line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
if _node_id == "_direct" then
@@ -181,10 +181,15 @@ function add_rule(var)
local CACHE_TEXT_FILE = CACHE_DNS_PATH .. ".txt"
local USE_CHINADNS_NG = "0"
local IS_SHUNT_NODE = uci:get(appname, TCP_NODE, "protocol") == "_shunt"
local USE_GEOVIEW = uci:get(appname, "@global_rules[0]", "enable_geoview")
local list1 = {}
local excluded_domain = {}
if not api.is_finded("geoview") then
USE_GEOVIEW = "0"
end
local function log(...)
if NO_LOGIC_LOG == "1" then
return
@@ -301,6 +306,21 @@ function add_rule(var)
return false
end
local function foreach_geosite(list_arg, callback)
local geosite_path = uci:get(appname, "@global_rules[0]", "v2ray_location_asset") or "/usr/share/v2ray/"
geosite_path = geosite_path:match("^(.*)/") .. "/geosite.dat"
if not fs.access(geosite_path) then return end
local bin = api.finded_com("geoview")
if not (bin and list_arg) then return end
local cmd = string.format("%q -type geosite -action extract -input %q -list %q -lowmem=true", bin, geosite_path, list_arg)
local pipe = io.popen(cmd)
if not pipe then return end
for line in pipe:lines() do
if line ~= "" then callback(line) end
end
pipe:close()
end
local cache_text = ""
local nodes_address_md5 = sys.exec("echo -n $(uci show passwall | grep '\\.address') | md5sum")
local new_rules = sys.exec("echo -n $(find /usr/share/passwall/rules -type f | xargs md5sum)")
@@ -347,16 +367,28 @@ function add_rule(var)
--屏蔽列表
if USE_CHINADNS_NG == "0" and USE_BLOCK_LIST == "1" then
local geosite_arg = ""
local f = io.open("/usr/share/passwall/rules/block_host")
if f then
for line in f:lines() do
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
set_domain_address(line, "")
if not line:find("#") and line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
set_domain_address(line, "")
end
end
end
f:close()
end
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
foreach_geosite(geosite_arg, function(line)
set_domain_address(line, "")
end)
log(" - 解析[屏蔽列表] Geosite 到屏蔽域名表(blocklist)完成")
end
end
local fwd_dns
@@ -400,88 +432,124 @@ function add_rule(var)
--直连(白名单)列表
if USE_DIRECT_LIST == "1" then
if fs.access("/usr/share/passwall/rules/direct_host") then
fwd_dns = LOCAL_DNS
if USE_CHINADNS_NG == "1" then
fwd_dns = nil
end
if fwd_dns then
local sets = {
setflag_4 .. "psw_white",
setflag_6 .. "psw_white6"
}
--始终用国内DNS解析直连(白名单)列表
for line in io.lines("/usr/share/passwall/rules/direct_host") do
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
add_excluded_domain(line)
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
fwd_dns = LOCAL_DNS
if USE_CHINADNS_NG == "1" then
fwd_dns = nil
end
if fwd_dns then
local sets = {
setflag_4 .. "psw_white",
setflag_6 .. "psw_white6"
}
--始终用国内DNS解析直连(白名单)列表
local geosite_arg = ""
local f = io.open("/usr/share/passwall/rules/direct_host")
if f then
for line in f:lines() do
if not line:find("#") and line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
add_excluded_domain(line)
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
end
end
end
f:close()
log(string.format(" - 域名白名单(whitelist):%s", fwd_dns or "默认"))
end
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
foreach_geosite(geosite_arg, function(line)
add_excluded_domain(line)
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
end)
log(" - 解析[直连列表] Geosite 到域名白名单(whitelist)完成")
end
end
end
--代理(黑名单)列表
if USE_PROXY_LIST == "1" then
if fs.access("/usr/share/passwall/rules/proxy_host") then
fwd_dns = TUN_DNS
if USE_CHINADNS_NG == "1" then
fwd_dns = nil
fwd_dns = TUN_DNS
if USE_CHINADNS_NG == "1" then
fwd_dns = nil
end
if fwd_dns then
local set_name = "psw_black"
local set6_name = "psw_black6"
if FLAG ~= "default" then
set_name = "psw_" .. FLAG .. "_black"
set6_name = "psw_" .. FLAG .. "_black6"
end
if fwd_dns then
local set_name = "psw_black"
local set6_name = "psw_black6"
if FLAG ~= "default" then
set_name = "psw_" .. FLAG .. "_black"
set6_name = "psw_" .. FLAG .. "_black6"
end
local sets = {
setflag_4 .. set_name
}
if NO_PROXY_IPV6 ~= "1" then
table.insert(sets, setflag_6 .. set6_name)
end
--始终使用远程DNS解析代理(黑名单)列表
for line in io.lines("/usr/share/passwall/rules/proxy_host") do
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
add_excluded_domain(line)
if NO_PROXY_IPV6 == "1" then
set_domain_address(line, "::")
local sets = {
setflag_4 .. set_name
}
if NO_PROXY_IPV6 ~= "1" then
table.insert(sets, setflag_6 .. set6_name)
end
--始终使用远程DNS解析代理(黑名单)列表
local geosite_arg = ""
local f = io.open("/usr/share/passwall/rules/proxy_host")
if f then
for line in f:lines() do
if not line:find("#") and line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") and not line:find(":") then
add_excluded_domain(line)
if NO_PROXY_IPV6 == "1" then
set_domain_address(line, "::")
end
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
end
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
end
end
f:close()
log(string.format(" - 代理域名表(blacklist):%s", fwd_dns or "默认"))
end
if USE_GEOVIEW == "1" and geosite_arg ~= "" then
foreach_geosite(geosite_arg, function(line)
add_excluded_domain(line)
if NO_PROXY_IPV6 == "1" then
set_domain_address(line, "::")
end
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
end)
log(" - 解析[代理列表] Geosite 到代理域名表(blacklist)完成")
end
end
end
--GFW列表
if USE_GFW_LIST == "1" then
if fs.access("/usr/share/passwall/rules/gfwlist") then
fwd_dns = TUN_DNS
if USE_CHINADNS_NG == "1" then
fwd_dns = nil
fwd_dns = TUN_DNS
if USE_CHINADNS_NG == "1" then
fwd_dns = nil
end
if fwd_dns then
local set_name = "psw_gfw"
local set6_name = "psw_gfw6"
if FLAG ~= "default" then
set_name = "psw_" .. FLAG .. "_gfw"
set6_name = "psw_" .. FLAG .. "_gfw6"
end
if fwd_dns then
local set_name = "psw_gfw"
local set6_name = "psw_gfw6"
if FLAG ~= "default" then
set_name = "psw_" .. FLAG .. "_gfw"
set6_name = "psw_" .. FLAG .. "_gfw6"
end
local sets = {
setflag_4 .. set_name
}
if NO_PROXY_IPV6 ~= "1" then
table.insert(sets, setflag_6 .. set6_name)
end
for line in io.lines("/usr/share/passwall/rules/gfwlist") do
local sets = {
setflag_4 .. set_name
}
if NO_PROXY_IPV6 ~= "1" then
table.insert(sets, setflag_6 .. set6_name)
end
local f = io.open("/usr/share/passwall/rules/gfwlist")
if f then
for line in f:lines() do
if line ~= "" and not line:find("#") and not check_excluded_domain(line) then
if NO_PROXY_IPV6 == "1" then
set_domain_address(line, "::")
@@ -494,6 +562,7 @@ function add_rule(var)
set_domain_ipset(line, table.concat(sets, ","))
end
end
f:close()
log(string.format(" - 防火墙域名表(gfwlist):%s", fwd_dns or "默认"))
end
end
@@ -501,30 +570,31 @@ function add_rule(var)
--中国列表
if CHN_LIST ~= "0" then
if fs.access("/usr/share/passwall/rules/chnlist") then
fwd_dns = nil
if CHN_LIST == "direct" then
fwd_dns = LOCAL_DNS
end
if CHN_LIST == "proxy" then
fwd_dns = TUN_DNS
end
if USE_CHINADNS_NG == "1" then
fwd_dns = nil
if CHN_LIST == "direct" then
fwd_dns = LOCAL_DNS
end
end
if fwd_dns then
local sets = {
setflag_4 .. "psw_chn",
setflag_6 .. "psw_chn6"
}
if CHN_LIST == "proxy" then
fwd_dns = TUN_DNS
end
if USE_CHINADNS_NG == "1" then
fwd_dns = nil
end
if fwd_dns then
local sets = {
setflag_4 .. "psw_chn",
setflag_6 .. "psw_chn6"
}
if CHN_LIST == "proxy" then
if NO_PROXY_IPV6 == "1" then
sets = {
setflag_4 .. "psw_chn"
}
end
if NO_PROXY_IPV6 == "1" then
sets = {
setflag_4 .. "psw_chn"
}
end
for line in io.lines("/usr/share/passwall/rules/chnlist") do
end
local f = io.open("/usr/share/passwall/rules/chnlist")
if f then
for line in f:lines() do
if line ~= "" and not line:find("#") and not check_excluded_domain(line) then
if CHN_LIST == "proxy" and NO_PROXY_IPV6 == "1" then
set_domain_address(line, "::")
@@ -537,6 +607,7 @@ function add_rule(var)
set_domain_ipset(line, table.concat(sets, ","))
end
end
f:close()
log(string.format(" - 中国域名表(chnroute):%s", fwd_dns or "默认"))
end
end
@@ -548,7 +619,7 @@ function add_rule(var)
local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" then
if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then
if _node_id == "_default" then
_node_id = default_node_id
end
@@ -590,21 +661,40 @@ function add_rule(var)
end
local domain_list = s.domain_list or ""
local geosite_arg = ""
for line in string.gmatch(domain_list, "[^\r\n]+") do
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("geosite:") and not line:find("ext:") then
if line:find("domain:") or line:find("full:") then
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") and not line:find("rule-set:") and not line:find("rs:") then
if line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
end
line = api.get_std_domain(line)
add_excluded_domain(line)
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
if line:find("domain:") or line:find("full:") then
line = string.match(line, ":([^:]+)$")
end
line = api.get_std_domain(line)
add_excluded_domain(line)
if no_ipv6 then
set_domain_address(line, "::")
end
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
end
end
end
if USE_GFW_LIST == "1" and CHN_LIST == "0" and USE_GEOVIEW == "1" and geosite_arg ~= "" then --仅GFW模式解析geosite
foreach_geosite(geosite_arg, function(line)
add_excluded_domain(line)
if no_ipv6 then
set_domain_address(line, "::")
end
set_domain_dns(line, fwd_dns)
set_domain_ipset(line, table.concat(sets, ","))
end
end)
log(string.format(" - 解析分流规则(%s) Geosite 完成", s.remarks))
end
if _node_id ~= "_direct" then
log(string.format(" - Sing-Box/Xray分流规则(%s):%s", s.remarks, fwd_dns or "默认"))
end
@@ -388,7 +388,7 @@ if USE_DIRECT_LIST == "1" and not fs.access(file_direct_host) then
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") then
if line ~= "" and not line:find("#") and not line:find(":") then
insert_unique(direct_domain, line, lookup_direct_domain)
end
end
@@ -440,7 +440,7 @@ if USE_PROXY_LIST == "1" and not fs.access(file_proxy_host) then
geosite_arg = geosite_arg .. (geosite_arg ~= "" and "," or "") .. line
else
line = api.get_std_domain(line)
if line ~= "" and not line:find("#") then
if line ~= "" and not line:find("#") and not line:find(":") then
insert_unique(proxy_domain, line, lookup_proxy_domain)
end
end
@@ -559,14 +559,14 @@ if IS_SHUNT_NODE then
local default_node_id = t["default_node"] or "_direct"
uci:foreach(appname, "shunt_rules", function(s)
local _node_id = t[s[".name"]]
if _node_id and _node_id ~= "_blackhole" then
if _node_id and _node_id ~= "_blackhole" and t["shunt_group"] == s.group then
if _node_id == "_default" then
_node_id = default_node_id
end
local domain_list = s.domain_list or ""
for line in string.gmatch(domain_list, "[^\r\n]+") do
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") then
if line ~= "" and not line:find("#") and not line:find("regexp:") and not line:find("ext:") and not line:find("rule-set:") and not line:find("rs:") then
if line:find("geosite:") then
line = string.match(line, ":([^:]+)$")
if _node_id == "_direct" then
@@ -952,7 +952,14 @@ add_firewall_rule() {
[ "$USE_SHUNT_NODE" = "1" ] && {
local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
local shunt_group
if [ "${USE_SHUNT_TCP}" = "1" ]; then
shunt_group=$(config_n_get $_TCP_NODE shunt_group)
elif [ "${USE_SHUNT_UDP}" = "1" ]; then
shunt_group=$(config_n_get $_UDP_NODE shunt_group)
fi
for shunt_id in $shunt_ids; do
[ "${shunt_group}" != "$(config_n_get ${shunt_id} group)" ] && continue
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d" | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | sed -e "s/^/add $IPSET_SHUNT &/g" -e "s/$/ timeout 0/g" | ipset -! -R
config_n_get $shunt_id ip_list | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d" | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | sed -e "s/^/add $IPSET_SHUNT6 &/g" -e "s/$/ timeout 0/g" | ipset -! -R
[ "$USE_GEOVIEW" = "1" ] && {
@@ -1054,7 +1054,14 @@ add_firewall_rule() {
[ "$USE_SHUNT_NODE" = "1" ] && {
local GEOIP_CODE=""
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
local shunt_group
if [ "${USE_SHUNT_TCP}" = "1" ]; then
shunt_group=$(config_n_get $_TCP_NODE shunt_group)
elif [ "${USE_SHUNT_UDP}" = "1" ]; then
shunt_group=$(config_n_get $_UDP_NODE shunt_group)
fi
for shunt_id in $shunt_ids; do
[ "${shunt_group}" != "$(config_n_get ${shunt_id} group)" ] && continue
config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_SHUNT_STATIC
config_n_get $shunt_id ip_list | sed 's/#.*//' | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_SHUNT6_STATIC
[ "$USE_GEOVIEW" = "1" ] && {
@@ -12,21 +12,31 @@ check_process() {
}
test_url() {
local url=$1
local try=1
[ -n "$2" ] && try=$2
local timeout=2
[ -n "$3" ] && timeout=$3
local extra_params=$4
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
extra_params="--retry-all-errors ${extra_params}"
local url="$1"
local try="${2:-1}"
local timeout="${3:-2}"
local extra_params="$4"
local repeat="$5"
if [ -z "$curl_retry_all_errors" ]; then
if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
curl_retry_all_errors=1
fi
fi
[ "$curl_retry_all_errors" = "1" ] && extra_params="--retry-all-errors ${extra_params}"
local max_time=$((timeout * (try + 1) + try + 3))
curl_test() {
/usr/bin/curl -skIL -o /dev/null ${extra_params} --max-time ${max_time} --connect-timeout ${timeout} --retry ${try} --retry-delay 1 -w "%{http_code}" "$url"
}
local status=$(curl_test)
[ "$status" = "204" ] && status=200
if [ "$status" = "200" ] && [ "$repeat" = "1" ]; then
sleep 3s
status=$(curl_test)
[ "$status" = "204" ] && status=200
fi
local status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout ${timeout} --retry ${try} -w %{http_code} "$url")
case "$status" in
204)
status=200
;;
esac
echo $status
}
@@ -59,7 +69,7 @@ test_node() {
NO_REC_PROCESS=1 $APP_FILE run_socks flag="test_node_${node_id}" node=${node_id} bind=127.0.0.1 socks_port=${_tmp_port} config_file=test_node_${node_id}.json
sleep 2s
local curlx="socks5h://127.0.0.1:${_tmp_port}"
local _proxy_status=$(test_url "${probe_url}" ${retry_num} ${connect_timeout} "-x $curlx")
local _proxy_status=$(test_url "${probe_url}" ${retry_num} ${connect_timeout} "-x $curlx" 1)
# 结束 SS 插件进程
local pid_file="/tmp/etc/${CONFIG}/test_node_${node_id}_plugin.pid"
[ -s "$pid_file" ] && kill -9 "$(head -n 1 "$pid_file")" >/dev/null 2>&1
@@ -72,6 +82,61 @@ test_node() {
return 1
}
try_switch_backup() {
local b_nodes="$1"
local now_node="$2"
local total tried
local new_node msg
local first_node found node
# 只有一个后备节点时,与主节点轮询
if [ "$backup_node_num" -eq 1 ]; then
b_nodes="$b_nodes $main_node"
fi
total=$(printf "%s\n" "$b_nodes" | wc -w)
tried=0
while [ "$tried" -lt "$total" ]; do
new_node=""
first_node=""
found=""
for node in $b_nodes; do
[ -z "$first_node" ] && first_node="$node" # 记录第一个节点
[ "$found" = "1" ] && { new_node="$node"; break; } # 找到当前节点后取下一个
[ "$node" = "$now_node" ] && found=1 # 标记找到当前节点
done
# 如果没找到当前节点,或者当前节点是最后一个,就取第一个节点
[ -z "$new_node" ] && new_node="$first_node"
local node_role node_type node_remarks
if [ "$new_node" = "$main_node" ]; then
node_role="主节点"
else
node_role="后备节点"
fi
node_type=$(config_n_get $new_node type)
node_remarks=$(config_n_get $new_node remarks)
echolog "Socks切换检测:端口[${socks_port}] 尝试${node_role}【${node_type}:[$node_remarks]】"
if test_node ${new_node}; then
check_process
echolog "Socks切换检测:端口[${socks_port}]【${node_type}:[$node_remarks]】正常,切换到此节点!"
NO_REC_PROCESS=1 $APP_FILE socks_node_switch flag=${id} new_node=${new_node}
[ $? -eq 0 ] && {
echolog "Socks切换检测:端口[${socks_port}] 节点切换完毕!"
}
return 0
fi
echolog "Socks切换检测:端口[${socks_port}]【${node_type}:[$node_remarks]】异常。"
now_node="$new_node"
tried=$((tried + 1))
done
echolog "Socks切换检测:端口[${socks_port}] 所有节点均不可用!"
return 1
}
test_auto_switch() {
flag=$((flag + 1))
local b_nodes=$1
@@ -85,9 +150,7 @@ test_auto_switch() {
fi
}
[ $flag -le 1 ] && {
main_node=$now_node
}
[ $flag -le 1 ] && main_node=$now_node
local status=$(test_proxy)
if [ "$status" = "2" ]; then
@@ -100,55 +163,21 @@ test_auto_switch() {
test_node ${main_node}
[ $? -eq 0 ] && {
check_process
#主节点正常,切换到主节点
echolog "Socks切换检测:端口[${socks_port}] 主节点【$(config_n_get $main_node type):[$(config_n_get $main_node remarks)]】正常,切换到主节点!"
NO_REC_PROCESS=1 $APP_FILE socks_node_switch flag=${id} new_node=${main_node}
[ $? -eq 0 ] && {
echolog "Socks切换检测:端口[${socks_port}] 节点切换完毕!"
}
[ $? -eq 0 ] && echolog "Socks切换检测:端口[${socks_port}] 节点切换完毕!"
return 0
}
fi
if [ "$status" = "0" ]; then
[ "$status" = "0" ] && {
#echolog "Socks切换检测:${id}【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】正常。"
return 0
elif [ "$status" = "1" ]; then
local new_node msg
if [ "$backup_node_num" -gt 1 ]; then
# 有多个后备节点时
local first_node found node
for node in $b_nodes; do
[ -z "$first_node" ] && first_node="$node" # 记录第一个节点
[ "$found" = "1" ] && { new_node="$node"; break; } # 找到当前节点后取下一个
[ "$node" = "$now_node" ] && found=1 # 标记找到当前节点
done
# 如果没找到当前节点,或者当前节点是最后一个,就取第一个节点
[ -z "$new_node" ] && new_node="$first_node"
if [ "$new_node" = "$main_node" ]; then
msg="切换到主节点检测!"
else
msg="切换到$([ "$now_node" = "$main_node" ] && echo 备用节点 || echo 下一个备用节点)检测!"
fi
else
# 只有一个后备节点时,与主节点轮询
new_node=$([ "$now_node" = "$main_node" ] && echo "$b_nodes" || echo "$main_node")
msg="切换到$([ "$now_node" = "$main_node" ] && echo 备用节点 || echo 主节点)检测!"
fi
echolog "Socks切换检测:端口[${socks_port}]【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】异常,$msg"
test_node ${new_node}
if [ $? -eq 0 ]; then
check_process
echolog "Socks切换检测:端口[${socks_port}]【$(config_n_get $new_node type):[$(config_n_get $new_node remarks)]】正常,切换到此节点!"
NO_REC_PROCESS=1 $APP_FILE socks_node_switch flag=${id} new_node=${new_node}
[ $? -eq 0 ] && {
echolog "Socks切换检测:端口[${socks_port}] 节点切换完毕!"
}
return 0
else
test_auto_switch "${b_nodes}" ${new_node}
fi
fi
}
echolog "Socks切换检测:端口[${socks_port}]【$(config_n_get $now_node type):[$(config_n_get $now_node remarks)]】异常。"
try_switch_backup "$b_nodes" "$now_node"
return $?
}
start() {
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.7.16
PKG_RELEASE:=68
PKG_VERSION:=26.8.7
PKG_RELEASE:=69
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
@@ -0,0 +1,61 @@
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
function decodeIfBase64(str) {
try {
let s = str.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const decoded = decodeURIComponent(
atob(s).split('').map(c =>
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
).join('')
);
if (btoa(unescape(encodeURIComponent(decoded))).replace(/=+$/, '') === s.replace(/=+$/, '')) {
return decoded;
}
} catch (e) {}
return str;
}
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function get_current_url() {
return window.location.origin + window.location.pathname;
}
function getOption(config, section, opt) {
let obj;
const id = `cbid.${config}.${section}.${opt}`;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
const combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
const div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
@@ -32,6 +32,7 @@ function index()
end
e.dependent = true
e.acl_depends = { "luci-app-passwall2" }
entry({"admin", "services", appname, "ip"}, call('check_ip')).leaf = true
--[[ Client ]]
entry({"admin", "services", appname, "settings"}, cbi(appname .. "/client/global"), _("Basic Settings"), 1).dependent = true
entry({"admin", "services", appname, "node_list"}, cbi(appname .. "/client/node_list"), _("Node List"), 2).dependent = true
@@ -51,14 +52,14 @@ function index()
entry({"admin", "services", appname, "socks_config"}, cbi(appname .. "/client/socks_config")).leaf = true
entry({"admin", "services", appname, "acl"}, cbi(appname .. "/client/acl"), _("Access control"), 98).leaf = true
entry({"admin", "services", appname, "acl_config"}, cbi(appname .. "/client/acl_config")).leaf = true
entry({"admin", "services", appname, "log"}, form(appname .. "/client/log"), _("Runtime Logs"), 999).leaf = true
entry({"admin", "services", appname, "log"}, template(appname .. "/log/log"), _("Runtime Logs"), 999).leaf = true
--[[ Server ]]
entry({"admin", "services", appname, "server"}, cbi(appname .. "/server/index"), _("Server-Side"), 99).leaf = true
entry({"admin", "services", appname, "server_user"}, cbi(appname .. "/server/user")).leaf = true
--[[ API ]]
entry({"admin", "services", appname, "server_user_update"}, call("server_user_update")).leaf = true
entry({"admin", "services", appname, "server_update_config"}, call("server_update_config")).leaf = true
entry({"admin", "services", appname, "server_user_status"}, call("server_user_status")).leaf = true
entry({"admin", "services", appname, "server_user_log"}, call("server_user_log")).leaf = true
entry({"admin", "services", appname, "server_get_log"}, call("server_get_log")).leaf = true
@@ -79,8 +80,8 @@ function index()
entry({"admin", "services", appname, "connect_status"}, call("connect_status")).leaf = true
entry({"admin", "services", appname, "ping_node"}, call("ping_node")).leaf = true
entry({"admin", "services", appname, "urltest_node"}, call("urltest_node")).leaf = true
entry({"admin", "services", appname, "update_config"}, call("update_config")).leaf = true
entry({"admin", "services", appname, "add_node"}, call("add_node")).leaf = true
entry({"admin", "services", appname, "update_node"}, call("update_node")).leaf = true
entry({"admin", "services", appname, "set_node"}, call("set_node")).leaf = true
entry({"admin", "services", appname, "copy_node"}, call("copy_node")).leaf = true
entry({"admin", "services", appname, "clear_all_nodes"}, call("clear_all_nodes")).leaf = true
@@ -96,7 +97,10 @@ function index()
entry({"admin", "services", appname, "subscribe_manual"}, call("subscribe_manual")).leaf = true
entry({"admin", "services", appname, "subscribe_manual_all"}, call("subscribe_manual_all")).leaf = true
entry({"admin", "services", appname, "flush_set"}, call("flush_set")).leaf = true
entry({"admin", "services", appname, "ip"}, call('check_ip')).leaf = true
entry({"admin", "services", appname, "get_shunt_rules"}, call("get_shunt_rules")).leaf = true
entry({"admin", "services", appname, "add_shunt_rule"}, call("add_shunt_rule")).leaf = true
entry({"admin", "services", appname, "delete_select_shunt_rules"}, call("delete_select_shunt_rules")).leaf = true
entry({"admin", "services", appname, "save_shunt_rule_order"}, call("save_shunt_rule_order")).leaf = true
--[[Components update]]
entry({"admin", "services", appname, "check_passwall2"}, call("app_check")).leaf = true
@@ -119,6 +123,21 @@ function index()
entry({"admin", "services", appname, "fetch_certsha256"}, call("fetch_certsha256")).leaf = true
end
local function http_write_json(content)
http.prepare_content("application/json")
http.write(jsonStringify(content or {code = 1}))
end
local function http_write_json_ok(data)
http.prepare_content("application/json")
http.write(jsonStringify({code = 1, data = data}))
end
local function http_write_json_error(data)
http.prepare_content("application/json")
http.write(jsonStringify({code = 0, data = data}))
end
function check_site(host, port)
local nixio = require "nixio"
local socket = nixio.socket("inet", "stream")
@@ -174,21 +193,6 @@ function check_ip()
luci.http.write_json(e)
end
local function http_write_json(content)
http.prepare_content("application/json")
http.write(jsonStringify(content or {code = 1}))
end
local function http_write_json_ok(data)
http.prepare_content("application/json")
http.write(jsonStringify({code = 1, data = data}))
end
local function http_write_json_error(data)
http.prepare_content("application/json")
http.write(jsonStringify({code = 0, data = data}))
end
function reset_config()
uci:revert(appname)
luci.sys.call("echo '' > /tmp/log/passwall2.log")
@@ -313,7 +317,7 @@ end
function get_socks_log()
local name = http.formvalue("name")
local path = "/tmp/etc/passwall2/SOCKS_" .. name .. ".log"
local path = "/tmp/etc/passwall2/" .. name .. ".log"
if nixio.fs.access(path) then
local content = luci.sys.exec("tail -n 5000 ".. path)
content = content:gsub("\n", "<br />")
@@ -361,7 +365,7 @@ function socks_status()
local index = http.formvalue("index")
local id = http.formvalue("id")
e.index = index
e.socks_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' | grep 'SOCKS_' > /dev/null", appname, id)) == 0
e.socks_status = luci.sys.call(string.format("/bin/busybox top -bn1 | grep -v -E 'grep|acl/|acl_' | grep '%s/bin/' | grep '%s' > /dev/null", appname, id)) == 0
local use_http = uci:get(appname, id, "http_port") or 0
e.use_http = 0
if tonumber(use_http) > 0 then
@@ -425,6 +429,23 @@ function urltest_node()
http_write_json(e)
end
function update_config()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
local data_t = jsonParse(data) or {}
if next(data_t) then
for k, v in pairs(data_t) do
uci:set(appname, id, k, v)
end
api.uci_save(uci, appname)
http_write_json_ok()
return
end
end
http_write_json_error()
end
function add_node()
local redirect = http.formvalue("redirect")
@@ -447,23 +468,6 @@ function add_node()
end
end
function update_node()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
local data_t = jsonParse(data) or {}
if next(data_t) then
for k, v in pairs(data_t) do
uci:set(appname, id, k, v)
end
api.uci_save(uci, appname)
http_write_json_ok()
return
end
end
http_write_json_error()
end
function set_node()
local type = http.formvalue("type")
local config = http.formvalue("config")
@@ -491,7 +495,7 @@ end
function clear_all_nodes()
uci:set(appname, '@global[0]', "enabled", "0")
uci:set(appname, '@global[0]', "socks_enabled", "0")
uci:set(appname, '@haproxy_config[0]', "balancing_enable", "0")
uci:set(appname, '@global_haproxy[0]', "balancing_enable", "0")
uci:delete(appname, '@global[0]', "node")
uci:foreach(appname, "socks", function(t)
uci:delete(appname, t[".name"])
@@ -704,7 +708,7 @@ function rollback_rules()
http_write_json_ok()
end
function server_user_update()
function server_update_config()
local id = http.formvalue("id") -- Node id
local data = http.formvalue("data") -- json new Data
if id and data then
@@ -1024,3 +1028,87 @@ function fetch_certsha256()
local data = api.fetch_cert_sha256(address, port, sni, 5)
http_write_json(data ~= "" and { code = 1, data = data } or { code = 0 })
end
function get_shunt_rules()
local id = http.formvalue("id")
local result = {}
if id then
result = uci:get_all(appname, id)
else
local default_items = {}
local other_items = {}
uci:foreach(appname, "shunt_rules", function(t)
if not t.group or t.group == "" then
default_items[#default_items + 1] = t
else
other_items[#other_items + 1] = t
end
end)
for i = 1, #default_items do result[#result + 1] = default_items[i] end
for i = 1, #other_items do result[#result + 1] = other_items[i] end
end
http_write_json(result)
end
function add_shunt_rule()
local add_name = http.formvalue("add_name")
local redirect = http.formvalue("redirect")
local uuid = add_name
if add_name then
local has = uci:get(appname, uuid)
if has then
http_write_json_error({ message = "This ID already exists." })
return
end
else
uuid = api.gen_short_uuid()
end
uci:section(appname, "shunt_rules", uuid)
local group = http.formvalue("group")
if group and group ~= "default" then
uci:set(appname, uuid, "group", group)
end
if redirect == "1" then
api.uci_save(uci, appname)
http.redirect(api.url("shunt_rules", uuid))
else
api.uci_save(uci, appname)
http_write_json_ok({uuid = uuid, redirect_url = api.url("shunt_rules", uuid)})
end
end
function delete_select_shunt_rules()
local ids = http.formvalue("ids")
local redirect = http.formvalue("redirect")
string.gsub(ids, '[^' .. "," .. ']+', function(w)
uci:foreach(appname, "nodes", function(s)
if s["protocol"] and s["protocol"] == "_shunt" then
uci:delete(appname, s[".name"], w)
end
end)
uci:delete(appname, w)
end)
if redirect == "1" then
api.uci_save(uci, appname)
http.redirect(api.url("rule"))
else
api.uci_save(uci, appname, true, true)
end
end
function save_shunt_rule_order()
local ids = http.formvalue("ids") or ""
local new_order = {}
for id in ids:gmatch("([^,]+)") do
new_order[#new_order + 1] = id
end
for idx, name in ipairs(new_order) do
luci.sys.call(string.format("uci -q reorder %s.%s=%d", appname, name, idx - 1))
end
api.sh_uci_commit(appname)
http_write_json({ status = "ok" })
end
@@ -23,8 +23,10 @@ s.anonymous = true
s.addremove = true
s.extedit = api.url("acl_config", "%s")
function s.create(e, t)
t = TypedSection.create(e, t)
luci.http.redirect(e.extedit:format(t))
local uuid = api.gen_short_uuid(5)
uuid = "acl_" .. uuid
TypedSection.create(e, uuid)
luci.http.redirect(e.extedit:format(uuid))
end
---- Enable
@@ -43,7 +43,7 @@ local socks_list = {}
m.uci:foreach(appname, "socks", function(s)
if s.enabled == "1" and s.node then
socks_list[#socks_list + 1] = {
id = "Socks_" .. s[".name"],
id = s[".name"],
remark = translate("Socks Config") .. " [" .. s.port .. translate("Port") .. "]",
group = "Socks"
}
@@ -101,6 +101,7 @@ if (has_singbox or has_xray) and #nodes_table > 0 then
if current_node.protocol == "_shunt" then
local shunt_lua = loadfile("/usr/lib/lua/luci/model/cbi/passwall2/client/include/shunt_options.lua")
setfenv(shunt_lua, getfenv(1))(m, s, {
s_cfgid = s:cfgsections()[1],
node_id = current_node_id,
node = current_node,
socks_list = socks_list,
@@ -311,10 +312,10 @@ s2.anonymous = true
s2.addremove = true
s2.extedit = api.url("socks_config", "%s")
function s2.create(e, t)
local uuid = api.gen_short_uuid()
t = uuid
TypedSection.create(e, t)
luci.http.redirect(e.extedit:format(t))
local uuid = api.gen_short_uuid(5)
uuid = "socks_" .. uuid
TypedSection.create(e, uuid)
luci.http.redirect(e.extedit:format(uuid))
end
o = s2:option(DummyValue, "status", translate("Status"))
@@ -335,7 +336,7 @@ o.group = {}
o = s2:option(DummyValue, "now_node", translate("Current Node"))
o.rawhtml = true
o.cfgvalue = function(_, n)
local current_node = api.get_cache_var("socks_" .. n)
local current_node = api.get_cache_var(n)
if current_node then
local node = m:get(current_node)
if node then
@@ -113,7 +113,9 @@ s.anonymous = true
s.addremove = true
s.create = function(e, t)
TypedSection.create(e, api.gen_short_uuid())
local uuid = api.gen_short_uuid(5)
uuid = "haproxy_" .. uuid
TypedSection.create(e, uuid)
end
s.remove = function(self, section)
@@ -4,9 +4,17 @@ if not data.node_id or not data.node then
return
end
local s_cfgid = data.s_cfgid
local current_node_id = data.node_id
local node_list = data.node_list or api.get_node_list()
local groups = {}
m.uci:foreach(appname, "shunt_rules", function(s)
if s.group and s.group ~= "" then
groups[s.group] = true
end
end)
local function get_cfgvalue()
return function(self, section)
return m:get(current_node_id, self.option)
@@ -106,15 +114,27 @@ if api.is_finded("geoview") then
.. "</ul>"
end
shunt_group = add_option(ListValue, "shunt_group", translate("Shunt Rule Group"))
shunt_group:value("", translate("default"))
for k, v in pairs(groups) do
shunt_group:value(k)
end
local shunt_group_val = m:get(current_node_id, "shunt_group") or ""
shunt_group_val = shunt_group_val:lower()
local shunt_rules = {}
m.uci:foreach(appname, "shunt_rules", function(e)
e.id = e[".name"]
e.remarks = e.remarks or e[".name"]
e["_node_option"] = e[".name"]
e["_node_default"] = ""
e["_fakedns_option"] = e[".name"] .. "_fakedns"
e["_proxy_tag_option"] = e[".name"] .. "_proxy_tag"
table.insert(shunt_rules, e)
local group = e.group or ""
group = group:lower()
if group == shunt_group_val then
e.id = e[".name"]
e.remarks = e.remarks or e[".name"]
e["_node_option"] = e[".name"]
e["_node_default"] = ""
e["_fakedns_option"] = e[".name"] .. "_fakedns"
e["_proxy_tag_option"] = e[".name"] .. "_proxy_tag"
table.insert(shunt_rules, e)
end
end)
table.insert(shunt_rules, {
id = ".default",
@@ -199,6 +219,8 @@ end
local footer = Template(appname .. "/include/shunt_options")
footer.api = api
footer.config = m.config
footer.id = current_node_id
footer.s_cfgid = s_cfgid or current_node_id
footer.normal_list = api.jsonc.stringify(node_list.normal_list)
m:append(footer)
@@ -1,8 +0,0 @@
local appname = "passwall2"
f = SimpleForm(appname)
f.reset = false
f.submit = false
f:append(Template(appname .. "/log/log"))
return f
@@ -30,9 +30,6 @@ o:value("https://connectivitycheck.platform.hicloud.com/generate_204", "HiCloud
o:value("https://wifi.vivo.com.cn/generate_204", "VIVO (CN)")
o.default = o.keylist[3]
-- [[ Add the node via the link ]]--
s:append(Template(appname .. "/node_list/link_add_node"))
m:append(Template(appname .. "/node_list/node_list"))
return api.return_map(m)
@@ -134,8 +134,12 @@ s.sortable = true
s.template = "cbi/tblsection"
s.extedit = api.url("node_subscribe_config", "%s")
function s.create(e, t)
local id = TypedSection.create(e, t)
luci.http.redirect(e.extedit:format(id))
local uuid = api.gen_short_uuid(5)
uuid = "sub_" .. uuid
TypedSection.create(e, uuid)
m:set(uuid, "hysteria_up_mbps", "100")
m:set(uuid, "hysteria_down_mbps", "100")
luci.http.redirect(e.extedit:format(uuid))
end
o = s:option(Value, "remark", translate("Remarks"))
@@ -223,11 +223,9 @@ if #hysteria2_type > 0 then
o = s:option(Value, "hysteria_up_mbps", "Hy/Hy2 " .. translate("Max upload Mbps"))
o.datatype = "uinteger"
o.default = "100"
o = s:option(Value, "hysteria_down_mbps", "Hy/Hy2 " .. translate("Max download Mbps"))
o.datatype = "uinteger"
o.default = "100"
end
o = s:option(Flag, "boot_update", translate("Update Once on Boot"), translate("Updates the subscription the first time runs automatically after each system boot."))
@@ -263,7 +263,9 @@ if has_xray then
s_xray_noise.addremove = true
s_xray_noise.create = function(e, t)
TypedSection.create(e, api.gen_short_uuid())
local uuid = api.gen_short_uuid(5)
uuid = "xray_noise_" .. uuid
TypedSection.create(e, uuid)
end
s_xray_noise.remove = function(self, section)
@@ -88,31 +88,6 @@ end
s:append(Template(appname .. "/rule/rule_version"))
local cfgname = "shunt_rules"
s = m:section(TypedSection, cfgname, "Sing-Box/Xray " .. translate("Shunt Rule"), "<a style='color: red'>" .. translate("Please note attention to the priority, the higher the order, the higher the priority.") .. "</a>")
s.template = "cbi/tblsection"
s.anonymous = false
s.addremove = true
s.sortable = true
s.extedit = api.url("shunt_rules", "%s")
function s.create(e, t)
TypedSection.create(e, t)
luci.http.redirect(e.extedit:format(t))
end
function s.remove(e, t)
m.uci:foreach(appname, "nodes", function(s)
if s["protocol"] and s["protocol"] == "_shunt" then
m:del(s[".name"], t)
end
end)
TypedSection.remove(e, t)
end
o = s:option(DummyValue, "remarks", translate("Remarks"))
local sortable = Template(appname .. "/cbi/sortable")
sortable.api = api
sortable.target_cfgname = cfgname
m:append(sortable)
m:append(Template(appname .. "/rule/shunt_rule_list"))
return api.return_map(m)
@@ -45,6 +45,19 @@ function clean_text(text)
:gsub("[ \t]*\n[ \t]*", "\n")
end
local remarks_lookup = {}
local groups = {}
m.uci:foreach(appname, "shunt_rules", function(s)
if s[".name"] ~= arg[1] then
if s.remarks then
remarks_lookup[s.remarks] = s[".name"]
end
if s.group and s.group ~= "" then
groups[s.group] = true
end
end
end)
s = m:section(NamedSection, arg[1], "shunt_rules", "")
s.addremove = false
s.dynamic = false
@@ -57,9 +70,34 @@ remarks.validate = function(self, value, section)
if value == "" then
return nil, translate("Remark cannot be empty.")
end
if remarks_lookup[value] then
return nil, translate("This remark already exists, please change a new remark.")
end
return value
end
o = s:option(Value, "group", translate("Shunt Rule Group"))
o.default = ""
o:value("", translate("default"))
for k, v in pairs(groups) do
o:value(k)
end
o.write = function(self, section, value)
value = api.trim(value)
local lower = value:lower()
if lower == "" or lower == "default" then
return m:del(section, self.option)
end
for _, v in ipairs(self.keylist or {}) do
if v:lower() == lower then
return m:set(section, self.option, v)
end
end
m:set(section, self.option, value)
end
protocol = s:option(MultiValue, "protocol", translate("Protocol"))
protocol:value("http")
protocol:value("tls")
@@ -31,7 +31,7 @@ o.default = 1
o.rmempty = false
local auto_switch_tip
local current_node = api.get_cache_var("socks_" .. arg[1])
local current_node = api.get_cache_var(arg[1])
if current_node then
local n = m:get(current_node)
if n then
@@ -109,10 +109,12 @@ o.rewrite_option = o.option
o = s:option(Value, _n("recv_window_conn"), translate("QUIC connection receive window"))
o.rewrite_option = o.option
o = s:option(Value, _n("idle_timeout"), translate("Idle Timeout"), translate("Example:") .. "30s (4s~120s)")
o = s:option(Value, _n("idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o.rewrite_option = o.option
o = s:option(Value, _n("keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Example:") .. "10s (2s~60s)")
o = s:option(Value, _n("keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o.rewrite_option = o.option
o = s:option(Flag, _n("disable_mtu_discovery"), translate("Disable MTU detection"))
@@ -394,10 +394,12 @@ o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Example:") .. "30s (4s~120s)")
o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Value, _n("hysteria2_keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Example:") .. "10s (2s~60s)")
o = s:option(Value, _n("hysteria2_keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Flag, _n("hysteria2_disable_mtu_discovery"), translate("Disable MTU detection"))
@@ -516,6 +518,9 @@ o = s:option(Flag, _n("use_mldsa65Verify"), translate("ML-DSA-65"))
o.default = "0"
o:depends({ [_n("tls")] = true, [_n("reality")] = true })
o = s:option(Value, _n("cipherSuites"), translate("Cipher Suites"), '<a href="https://go.dev/src/crypto/tls/cipher_suites.go#L44" target="_blank">***</a>' .. " " .. translate("Configures the list of supported cipher suites, separated by :"))
o:depends({ [_n("tls")] = true, [_n("reality")] = false })
o = s:option(TextValue, _n("reality_mldsa65Verify"), "ML-DSA-65 " .. translate("Public key"))
o.default = ""
o.rows = 5
@@ -803,6 +808,9 @@ o:value("", translate("Auto"))
o:value("UseIPv4", translate("IPv4 Only"))
o:value("UseIPv6", translate("IPv6 Only"))
o = s:option(Flag, _n("happy_eyeballs"), translate("Enable Happy Eyeballs"), translate("Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection."))
o.default = 0
local protocols = s.fields[_n("protocol")].keylist
if #protocols > 0 then
for i, v in ipairs(protocols) do
@@ -814,7 +822,11 @@ if #protocols > 0 then
s.fields[_n("address")]:depends(depends_condition)
s.fields[_n("port")]:depends(depends_condition)
s.fields[_n("domain_resolver")]:depends(depends_condition)
s.fields[_n("domain_strategy")]:depends(depends_condition)
s.fields[_n("happy_eyeballs")]:depends(depends_condition)
local strategy_depends = api.clone(depends_condition)
strategy_depends[_n("happy_eyeballs")] = false
s.fields[_n("domain_strategy")]:depends(strategy_depends)
if v ~= "hysteria2" then
s.fields[_n("tcp_fast_open")]:depends({ [_n("protocol")] = v })
@@ -457,10 +457,12 @@ if singbox_tags:find("with_quic") then
o = s:option(Value, _n("hysteria2_down_mbps"), translate("Max download Mbps"))
o:depends({ [_n("protocol")] = "hysteria2" })
o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Example:") .. "30s (4s~120s)")
o = s:option(Value, _n("hysteria2_idle_timeout"), translate("Idle Timeout"), translate("Units:seconds") .. " (4~120)")
o.datatype = "range(4,120)"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Value, _n("hysteria2_keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Example:") .. "10s (2s~60s)")
o = s:option(Value, _n("hysteria2_keep_alive_period"), translate("QUIC KeepAlive interval"), translate("Units:seconds") .. " (2~60)")
o.datatype = "range(2,60)"
o:depends({ [_n("protocol")] = "hysteria2"})
o = s:option(Flag, _n("hysteria2_disable_mtu_discovery"), translate("Disable MTU detection"))
@@ -629,6 +631,10 @@ if singbox_tags:find("with_utls") then
o:depends({ [_n("reality")] = true })
end
o = s:option(Flag, _n("anytls_disable_reuse"), translate("Disable TLS Reuse"))
o.default = 0
o:depends({ [_n("protocol")] = "anytls" })
o = s:option(ListValue, _n("transport"), translate("Transport"))
o:value("tcp", "TCP")
o:value("http", "HTTP")
+5 -6
View File
@@ -583,7 +583,7 @@ function get_node_list()
uci:foreach(appname, "socks", function(s)
if s.enabled == "1" and s.node then
node_list.socks_list[#node_list.socks_list + 1] = {
id = "Socks_" .. s[".name"],
id = s[".name"],
remark = i18n.translate("Socks Config") .. " [" .. s.port .. i18n.translate("Port") .. "]",
group = "Socks"
}
@@ -677,8 +677,9 @@ function gen_uuid(format)
return uuid
end
function gen_short_uuid()
return sys.exec("echo -n $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 8)")
function gen_short_uuid(length)
if not length then length = 8 end
return sys.exec("echo -n $(head /dev/urandom | tr -dc A-Za-z0-9 | head -c %s)" % length)
end
function uci_get_type(type, config, default)
@@ -1170,7 +1171,7 @@ function to_extract(app_name, file, subfix)
exec("/bin/rm", {"-f", file})
return {
code = 1,
error = i18n.translate("Not installed %s, Can't unzip!" % { tools_name })
error = i18n.translatef("Not installed %s, Can't unzip!", tools_name)
}
end
end
@@ -1383,8 +1384,6 @@ function set_apply_on_parse(map)
if old then old(self) end
map:set("@global[0]", "timestamp", os.time())
end
local cbi = require "luci.cbi"
map:append(cbi.Template(appname .. "/cbi/optimize_cbi_ui"))
end
end
end
@@ -647,6 +647,8 @@ function gen_outbound(flag, node, tag, proxy_table)
idle_session_check_interval = "30s",
idle_session_timeout = "30s",
min_idle_session = 5,
disable_reuse = (node.anytls_disable_reuse == "1") and true or nil,
client_metadata = api.compare_versions(local_version, ">=", "1.13.16") and "anytls/0.0.13" or nil,
tls = tls
}
end
@@ -1306,36 +1308,25 @@ function gen_config(var)
default_node_port = server_port
end
function gen_socks_config_node(node_id, socks_id, remarks)
if node_id then
socks_id = node_id:sub(1 + #"Socks_")
end
local result
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
if not remarks then
remarks = socks_node.port
end
result = {
[".name"] = "Socksid_" .. socks_id,
remarks = remarks,
function get_node_by_id(node_id)
if not node_id or node_id == "" or node_id == "nil" then return nil end
local section = uci:get_all(appname, node_id) or {}
if section[".type"] == "socks" then
local result = {
[".name"] = node_id,
remarks = "socks[%s]" % section.port,
type = "sing-box",
protocol = "socks",
address = "127.0.0.1",
port = socks_node.port,
port = section.port,
uot = "1"
}
return result
end
return result
end
function get_node_by_id(node_id)
if not node_id or node_id == "" or node_id == "nil" then return nil end
if node_id:find("Socks_") then
return gen_socks_config_node(node_id)
else
return uci:get_all(appname, node_id)
if section[".type"] == "nodes" then
return section
end
return nil
end
function gen_urltest_outbound(_node)
@@ -1599,6 +1590,9 @@ function gen_config(var)
--shunt rule
uci:foreach(appname, "shunt_rules", function(e)
if node["shunt_group"] ~= e.group then
return
end
local outboundTag = gen_shunt_node(e[".name"])
if outboundTag and e.remarks then
if outboundTag == "default" then
@@ -170,9 +170,15 @@ function gen_outbound(flag, node, tag, proxy_table)
mark = 255,
domainStrategy = node.domain_strategy or "UseIP",
tcpFastOpen = (node.tcp_fast_open == "1") and true or nil,
tcpMptcp = (node.tcpMptcp == "1") and true or nil
tcpMptcp = (node.tcpMptcp == "1") and true or nil,
happyEyeballs = (node.happy_eyeballs == "1") and {
TryDelayMs = 250,
PrioritizeIPv6 = false,
Interleave = 1,
MaxConcurrentTry = 4
} or nil
},
network = node.transport,
[(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network"
security = node.stream_security,
tlsSettings = (node.stream_security == "tls") and {
serverName = node.tls_serverName,
@@ -183,7 +189,8 @@ function gen_outbound(flag, node, tag, proxy_table)
certificates = (node.tls_certificate == "1" and node.tls_certificate_pem ~= "") and {
certificate = api.split(node.tls_certificate_pem, "\n"),
usage = "verify"
} or nil
} or nil,
cipherSuites = node.cipherSuites or nil
} or nil,
realitySettings = (node.stream_security == "reality") and {
serverName = node.tls_serverName,
@@ -659,7 +666,7 @@ function gen_config_server(node)
protocol = node.protocol,
settings = settings,
streamSettings = {
network = node.transport,
[(api.compare_versions(xray_version, "<", "26.7.11")) and "network" or "method"] = node.transport, -- Todo: Remove version check and "network"
security = "none",
tlsSettings = ("1" == node.tls) and {
disableSystemRoot = false,
@@ -988,37 +995,26 @@ function gen_config(var)
table.insert(inbounds, inbound)
end
function gen_socks_config_node(node_id, socks_id, remarks)
if node_id then
socks_id = node_id:sub(1 + #"Socks_")
end
local result
local socks_node = uci:get_all(appname, socks_id) or nil
if socks_node then
if not remarks then
remarks = socks_node.port
end
result = {
[".name"] = "Socksid_" .. socks_id,
remarks = remarks,
function get_node_by_id(node_id)
if not node_id or node_id == "" or node_id == "nil" then return nil end
local section = uci:get_all(appname, node_id) or {}
if section[".type"] == "socks" then
local result = {
[".name"] = node_id,
remarks = "socks[%s]" % section.port,
type = "Xray",
protocol = "socks",
address = "127.0.0.1",
port = socks_node.port,
port = section.port,
transport = "tcp",
stream_security = "none"
}
return result
end
return result
end
function get_node_by_id(node_id)
if not node_id or node_id == "" or node_id == "nil" then return nil end
if node_id:find("Socks_") then
return gen_socks_config_node(node_id)
else
return uci:get_all(appname, node_id)
if section[".type"] == "nodes" then
return section
end
return nil
end
function gen_loopback(outbound_tag, loopback_dst)
@@ -1073,6 +1069,10 @@ function gen_config(var)
if outboundTag then
valid_nodes[#valid_nodes + 1] = outboundTag
end
-- Check if balancing node duplicates fallback node
if _node.fallback_node == blc_node_id then
_node.fallback_node = nil
end
end
end
if #valid_nodes == 0 then return nil end
@@ -1392,6 +1392,9 @@ function gen_config(var)
--shunt rule
uci:foreach(appname, "shunt_rules", function(e)
if node["shunt_group"] ~= e.group then
return
end
local outboundTag = gen_shunt_node(e[".name"])
if outboundTag and e.remarks then
if outboundTag == "default" then
@@ -1 +1,18 @@
<!-- map footer -->
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function () {
setTimeout(updateElements, 200);
function updateElements() {
// Remove extra line breaks between the value control and its description.
document.querySelectorAll("div.cbi-value-description").forEach(function(descDiv) {
var prev = descDiv.previousSibling;
while (prev && prev.nodeType === Node.TEXT_NODE && prev.textContent.trim() === "") {
prev = prev.previousSibling;
}
if (prev && prev.nodeType === Node.ELEMENT_NODE && prev.tagName === "BR") {
prev.remove();
}
});
}
});
</script>
@@ -1,2 +1,24 @@
<!-- map header -->
<script src="<%=resource%>/view/<%=self.api.appname%>/cbi.js"></script>
<script src="<%=resource%>/view/<%=self.api.appname%>/func.js"></script>
<script type="text/javascript">
//<![CDATA[
function update_config(cfgid, data, redirect_url) {
XHR.get('<%=self.api.url("update_config")%>', {
id: cfgid,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
if (redirect_url) {
window.location.href = redirect_url;
}
}
else {
alert("<%:Error%>");
}
});
}
//]]>
</script>
@@ -1,22 +0,0 @@
<script type="text/javascript">
document.addEventListener("DOMContentLoaded", function () {
setTimeout(updateElements, 200);
function updateElements() {
document.querySelectorAll("input.btn.cbi-button.cbi-button-up").forEach(function(btn) {
btn.value = "<%:Move up%>";
});
document.querySelectorAll("input.btn.cbi-button.cbi-button-down").forEach(function(btn) {
btn.value = "<%:Move down%>";
});
document.querySelectorAll("div.cbi-value-description").forEach(function(descDiv) {
var prev = descDiv.previousSibling;
while (prev && prev.nodeType === Node.TEXT_NODE && prev.textContent.trim() === "") {
prev = prev.previousSibling;
}
if (prev && prev.nodeType === Node.ELEMENT_NODE && prev.tagName === "BR") {
prev.remove();
}
});
}
});
</script>
@@ -55,8 +55,8 @@ local appname = api.appname
return;
}
let to_url = '<%=api.url("node_config")%>/' + node_select_value;
if (node_select_value.indexOf("Socks_") === 0) {
to_url = '<%=api.url("socks_config")%>/' + node_select_value.substring("Socks_".length);
if (node_select_value.indexOf("socks_") === 0) {
to_url = '<%=api.url("socks_config")%>/' + node_select_value;
}
location.href = to_url;
}
@@ -207,7 +207,7 @@ local appname = api.appname
let new_val = el.target.value
const new_hasItem = shunt_list.find(element => element.id == new_val);
if (new_hasItem) {
XHR.get('<%=api.url("update_node")%>', {
XHR.get('<%=api.url("update_config")%>', {
id: "<%=self.global_cfgid%>",
data: JSON.stringify({
node: new_val
@@ -30,7 +30,7 @@
const selectNode = document.getElementById(dom_id + "_node");
val = selectNode.value;
}
if (val == "" || val.startsWith("_") || val.startsWith("Socks_") || !normal_list.find(element => element.id == val)) {
if (val == "" || val.startsWith("_") || val.startsWith("socks_") || !normal_list.find(element => element.id == val)) {
const hiddenSelect = document.getElementById(cbid);
const panel = document.getElementById(cbid + ".panel");
const display = document.getElementById(cbid + ".display");
@@ -53,6 +53,22 @@
}
}
}
document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="shunt_group"]', function(el) {
let o_val = el.value;
el.addEventListener("change", () => {
el.blur();
if (o_val != el.value) {
let save = true;
if (save) {
update_config("<%=self.id%>", {
shunt_group: getOption("<%=self.config%>", "<%=self.s_cfgid%>", "shunt_group").value,
}, get_current_url());
}
}
});
});
});
document.addEventListener("DOMContentLoaded", () => setTimeout(() => {
refresh_depends();
const table_dom = document.getElementById("cbi-passwall2-shunt_option_list");
@@ -1,6 +1,7 @@
<%
local api = require "luci.passwall2.api"
-%>
<%+header%>
<script type="text/javascript">
//<![CDATA[
var log_textarea = null;
@@ -42,3 +43,4 @@ local api = require "luci.passwall2.api"
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clearlog()" value="<%:Clear logs%>" />
<textarea id="log_textarea" class="cbi-input-textarea" style="width: 100%;margin-top: 10px;" data-update="change" rows="40" wrap="off" readonly="readonly"></textarea>
</fieldset>
<%+footer%>
@@ -1,8 +1,8 @@
<%
local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=self.api.url("node_config")%>/' + node_id;
document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
let o_val = el.value;
@@ -11,13 +11,18 @@ local api = self.api
if (o_val != el.value) {
let save = true;
if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click();
update_node({
remarks: getOption("remarks").value,
group: getOption("group").value,
type: getOption("type").value,
/*
const saveBtn = document.getElementsByClassName('cbi-button-save');
if (saveBtn && saveBtn.length > 0) {
saveBtn[saveBtn.length - 1].click();
}
*/
update_config(node_id, {
remarks: getOption("<%=self.config%>", "<%=self.section%>", "remarks").value,
group: getOption("<%=self.config%>", "<%=self.section%>", "group").value,
type: getOption("<%=self.config%>", "<%=self.section%>", "type").value,
protocol: ''
});
}, node_config_url);
}
}
});
@@ -31,12 +36,17 @@ local api = self.api
let name = name_split[name_split.length - 1];
let save = true;
if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click();
update_node({
remarks: getOption("remarks").value,
group: getOption("group").value,
protocol: getOption(name).value
});
/*
const saveBtn = document.getElementsByClassName('cbi-button-save');
if (saveBtn && saveBtn.length > 0) {
saveBtn[saveBtn.length - 1].click();
}
*/
update_config(node_id, {
remarks: getOption("<%=self.config%>", "<%=self.section%>", "remarks").value,
group: getOption("<%=self.config%>", "<%=self.section%>", "group").value,
protocol: getOption("<%=self.config%>", "<%=self.section%>", name).value,
}, node_config_url);
} else {
window.location.href = node_config_url + "?select_proto=" + el.value;
}
@@ -1,64 +1,11 @@
<%
local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("node_config")%>/' + node_id;
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getOption(opt) {
var obj;
var id = 'cbid.<%=self.config%>.' + node_id + '.' + opt;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
var combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
var div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
function update_node(data) {
XHR.get('<%=api.url("update_node")%>', {
id: node_id,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
window.location.href = node_config_url;
}
else {
alert("<%:Error%>");
}
});
}
function fetchCertSha256(el) {
if (el.getAttribute("data-loading") === "1") return;
el.setAttribute("data-loading", "1");
el.style.pointerEvents = "none";
el.style.color = "red";
XHR.get('<%=api.url("fetch_certsha256")%>', { id: node_id }, function(x, res) {
XHR.get('<%=self.api.url("fetch_certsha256")%>', { id: "<%=self.section%>" }, function(x, res) {
el.setAttribute("data-loading", "0");
el.style.pointerEvents = "";
el.style.color = "";
@@ -1750,7 +1750,7 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'tls', true);
opt.set(dom_prefix + 'reality', false);
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || queryParam.peer || '');
opt.set(dom_prefix + 'tls_allowInsecure', !((queryParam.allowinsecure ?? '0') === '0' && (queryParam.insecure ?? '0') === '0'));
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
@@ -1761,7 +1761,7 @@ local current_node = map:get(section)
opt.set(dom_prefix + 'tls', true);
opt.set(dom_prefix + 'reality', true);
opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || '');
opt.set(dom_prefix + 'tls_serverName', queryParam.sni || queryParam.peer || '');
if (queryParam.fp && queryParam.fp.trim() != "") {
opt.set(dom_prefix + 'utls', true);
opt.set(dom_prefix + 'fingerprint', queryParam.fp);
@@ -1,556 +0,0 @@
<%
local api = require "luci.passwall2.api"
-%>
<script type="text/javascript">
//<![CDATA[
function ajax_add_node(link, group) {
const dom = document.getElementById('modal-mask');
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
const chunkSize = 1000; // Fragmented sending to overcome uhttpd's limitations, with each fragment containing 1000 characters.
const totalChunks = Math.ceil(link.length / chunkSize);
let currentChunk = 0;
function sendNextChunk() {
if (currentChunk < totalChunks) {
const chunk = link.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const xhr = new XMLHttpRequest();
xhr.open('POST', '<%=api.url("link_add_node")%>', true);
xhr.onload = function () {
if (xhr.status === 200) {
currentChunk++;
sendNextChunk();
} else {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Error%>");
return;
}
};
xhr.onerror = function () {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Network Error%>");
return;
};
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("chunk_index", currentChunk);
formData.append("total_chunks", totalChunks);
formData.append("group", group);
xhr.send(formData);
} else {
window.location.href = '<%=api.url("node_list")%>';
}
}
sendNextChunk();
}
function decodeIfBase64(str) {
try {
let s = str.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const decoded = decodeURIComponent(
atob(s).split('').map(c =>
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
).join('')
);
if (btoa(unescape(encodeURIComponent(decoded))).replace(/=+$/, '') === s.replace(/=+$/, '')) {
return decoded;
}
} catch (e) {}
return str;
}
function getBg(el) {
if (!el) return null;
const style = getComputedStyle(el);
const bgImage = style.backgroundImage;
const bgColor = style.backgroundColor;
return (bgImage !== 'none' || !/rgba\([^,]+,[^,]+,[^,]+,\s*0\)/.test(bgColor) && bgColor !== 'transparent')
? style.background
: null;
};
function open_add_link_div() {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const addLinkDiv = document.getElementById("add_link_div");
addLinkDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
addLinkDiv.style.display = "block";
if (!addLinkDiv._dropdown_inited) {
addLinkDiv._dropdown_inited = true;
dropdown_list_fun("addlink_group_custom");
}
document.getElementById("nodes_link").focus();
}
function close_add_link_div() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("add_link_div").style.display = "none";
document.body.classList.remove('modal-open');
}
function add_node() {
let nodes_link = document.getElementById("nodes_link").value;
const group = (document.querySelector('#addlink_group_custom input[type="hidden"]')?.value || "default");
nodes_link = nodes_link.replace(/\t/g, "").replace(/\r\n|\r/g, "\n").replace(/<[^>]*>/g, '').trim();
if (nodes_link != "") {
nodes_link = decodeIfBase64(nodes_link);
let s = nodes_link.split('://');
if (s.length > 1) {
close_add_link_div();
ajax_add_node(nodes_link, group);
} else {
alert("<%:Please enter the correct link.%>");
}
} else {
document.getElementById("nodes_link").focus();
}
}
function clear_all_nodes() {
if (confirm('<%:Are you sure to clear all nodes?%>') == true){
XHR.get('<%=api.url("clear_all_nodes")%>', null,
function(x, data) {
if(x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
function open_reassign_group_div() {
const ids = [];
const visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const reassignGroupDiv = document.getElementById("reassign_group_div");
reassignGroupDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
reassignGroupDiv.style.display = "block";
if (!reassignGroupDiv._dropdown_inited) {
reassignGroupDiv._dropdown_inited = true;
dropdown_list_fun("reassign_group_custom");
}
} else {
alert("<%:You no select nodes !%>");
}
}
}
function close_reassign_group_div() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("reassign_group_div").style.display = "none";
document.body.classList.remove('modal-open');
}
function reassign_group() {
const ids = [];
const group = (document.querySelector('#reassign_group_custom input[type="hidden"]')?.value || "default");
const visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
XHR.get('<%=api.url("reassign_group")%>', {
group: group,
ids: ids.join(",")
},
function(x, data) {
if (x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
}
// Custom group dropdown list events
function dropdown_list_fun(div_id) {
const dropdown = document.getElementById(div_id);
if (!dropdown) return;
const display = dropdown.querySelector(".selected-display");
const displayText = display.querySelector(".text");
const list = dropdown.querySelector(".dropdown-list");
const hidden = dropdown.querySelector('input[type="hidden"]');
const input = dropdown.querySelector(".create-item-input");
display.addEventListener("click", function() {
list.style.display = list.style.display === "none" ? "block" : "none";
input.value = "";
//input.focus();
});
function selectItem(li) {
list.querySelectorAll(".dropdown-item").forEach(function(el){
el.classList.remove("selected");
});
li.classList.add("selected");
hidden.value = li.dataset.value;
displayText.textContent = li.dataset.value || "<%:default%>";
list.style.display = "none";
}
list.addEventListener("click", function(e){
const li = e.target.closest(".dropdown-item");
if (!li || li.classList.contains("custom-input")) return;
selectItem(li);
});
input.addEventListener("keydown", function(e){
const isEnter = e.key === "Enter" || e.keyCode === 13;
if (!isEnter) return;
e.stopPropagation();
e.preventDefault();
const val = input.value.trim();
if (!val) return;
if (val.toLowerCase() === "default") {
const emptyLi = Array.from(list.querySelectorAll(".dropdown-item"))
.find(function(el){ return !el.dataset.value; });
if (emptyLi) selectItem(emptyLi);
input.value = "";
return;
}
let li = Array.from(list.querySelectorAll(".dropdown-item")).find(function(el){
return el.dataset.value && el.dataset.value.toLowerCase() === val.toLowerCase();
});
if (!li) {
li = document.createElement("li");
li.className = "dropdown-item";
li.dataset.value = val;
li.textContent = val;
list.insertBefore(li, input.parentNode);
}
input.value = "";
selectItem(li);
});
// Read group names from tab
function InsertGroup(list, input) {
const tabs = document.querySelectorAll(".cbi-tabmenu li");
if (!tabs.length) {
setTimeout(() => InsertGroup(list, input), 50);
return;
}
tabs.forEach(function(li){
const group = li.id.split('.').pop();
if(group === "default") return;
if(Array.from(list.querySelectorAll(".dropdown-item")).some(el => el.dataset.value === group)) return;
const newLi = document.createElement("li");
newLi.className = "dropdown-item";
newLi.dataset.value = group;
newLi.textContent = group;
list.insertBefore(newLi, input.parentNode);
});
}
InsertGroup(list, input);
// Automatically collapses when clicking on the outside.
document.addEventListener("click", function(e) {
if (!dropdown.contains(e.target)) {
list.style.display = "none";
}
});
}
//]]>
</script>
<div id="modal-mask"></div>
<div id="add_link_div">
<div id="add_link_modal_container">
<h3><%:Add the node via the link%></h3>
<div class="value-custom">
<textarea id="nodes_link" rows="10"></textarea>
<p id="nodes_link_text"><%:Enter share links, one per line. Subscription links are not supported!%></p>
</div>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="addlink_group_custom"><%:Group Name%></label>
<div id="addlink_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="addlink_group" value="">
</div>
</div>
</div>
<div id="add_link_button_container">
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_add_link_div()" value="<%:Close%>" />
</div>
</div>
</div>
<div id="reassign_group_div">
<div id="reassign_group_modal_container">
<h3><%:Reassign Node Group%></h3>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="reassign_group_custom"><%:Group Name%></label>
<div id="reassign_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="to_group" value="">
</div>
</div>
</div>
<div id="reassign_group_button_container">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group()" value="<%:Save%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_reassign_group_div()" value="<%:Close%>" />
</div>
</div>
</div>
<div class="pw-toolbar">
<div class="pw-toolbar-field">
<input class="btn cbi-button cbi-button-add" type="button" onclick="to_add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-add" type="button" onclick="open_add_link_div()" value="<%:Add the node via the link%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="clear_all_nodes()" value="<%:Clear all nodes%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="delete_select_nodes()" value="<%:Delete select nodes%>" />
<input class="btn cbi-button cbi-button-edit" type="button" id="select_all_btn" onclick="checked_all_node(this)" value="<%:Select all%>" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="open_reassign_group_div()" value="<%:Reassign Group%>" />
<input class="btn cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" />
<!--<input class="btn cbi-button cbi-button-save" type="submit" name="cbi.save" value="<%:Save%>" />-->
<!--<input class="btn cbi-button cbi-button-reset" type="button" value="<%:Reset%>" onclick="location.href='<%=REQUEST_URI%>'" />-->
</div>
</div>
<style>
.pw-toolbar {
width: 100%;
text-align: center;
}
.pw-toolbar-field {
width: 100%;
text-align: center;
display: inline-block;
padding: 5px 0 5px;
}
#modal-mask {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.4);
z-index: 999;
}
body.modal-open {
overflow: hidden;
pointer-events: none;
}
body.modal-open #add_link_div,
body.modal-open #reassign_group_div {
pointer-events: auto;
}
#add_link_div, #reassign_group_div {
display: none;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
width: 90%;
max-width: 500px;
}
#add_link_modal_container, #reassign_group_modal_container {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20px;
}
#nodes_link {
width: 100%;
height: 180px;
resize: vertical;
font-family: monospace;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
#add_link_div h3,
#reassign_group_div h3 {
background: inherit;
}
#nodes_link_text {
color: red;
font-size: 14px;
margin-top: 5px;
text-align: center;
width: 100%;
}
#add_link_button_container, #reassign_group_button_container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 300px;
margin-top: 10px;
}
.value-custom {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin: 10px 0;
padding: 0px 5px 0px 5px;
}
.value-field-custom {
display: inline-flex;
align-items: center;
gap: 10px;
}
.value-title-custom {
font-size: 13px;
line-height: 28px;
white-space: nowrap;
text-align: right;
}
.custom-dropdown {
position: relative;
border: 1px solid #d9d9d9;
border-radius: 2px;
width: 180px;
height: 28px;
font-size: 13px;
background: #fff;
cursor: pointer;
box-sizing: border-box;
display: block;
}
.selected-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 12px;
color: #666;
}
.selected-display:hover {
background-color: #f7f7f7;
}
.dropdown-list {
position: absolute;
top: calc(100% + 2px);
left: -1px;
width: calc(100% + 2px);
border: 1px solid #d9d9d9;
border-radius: 2px;
border-top: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
background: #fff;
list-style: none;
margin: 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
z-index: 100;
box-sizing: border-box;
color: #666;
}
.dropdown-item {
padding: 4px 8px;
line-height: 20px;
cursor: pointer;
}
.dropdown-item.selected {
background-color: #1e90ff;
color: #fff;
}
.create-item-input::placeholder {
text-align: center;
}
.dropdown-item.custom-input input {
width: 100%;
box-sizing: border-box;
padding: 3px;
font-size: 13px;
line-height: 20px;
border: 1px solid #ccc;
text-align: left;
}
.arrow-down-small {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #555;
display: inline-block;
vertical-align: middle;
}
</style>
@@ -9,10 +9,11 @@ local node = api.uci_get_type("global", "node")
if node then
local node_type = api.uci_get_type_id(node, "type")
local node_protocol = api.uci_get_type_id(node, "protocol")
if node_type == "Xray" and node_protocol == "_shunt" then
if (node_type == "Xray" or node_type == "sing-box") and node_protocol == "_shunt" then
default_node_type = node_protocol
local node_shunt_group = api.uci_get_type_id(node, "shunt_group")
uci:foreach(appname, "shunt_rules", function(e)
if e[".name"] and e.remarks then
if e[".name"] and e.remarks and e.group == node_shunt_group then
shunt_rule_list[#shunt_rule_list + 1] = e
end
end)
@@ -36,12 +37,13 @@ table td, .table .td {
display: none;
width: 30rem;
position: fixed;
top:50%;
padding-top: 30px;
top:45%;
z-index: 99;
text-align: center;
background: white;
box-shadow: darkgrey 10px 10px 30px 5px;
text-align: center;
padding: 0.5em;
}
._now_use_bg {
@@ -183,8 +185,205 @@ table td, .table .td {
}
</style>
<% if api.is_js_luci() then -%>
<style>
.pw-toolbar {
width: 100%;
text-align: center;
}
.pw-toolbar-field {
width: 100%;
text-align: center;
display: inline-block;
padding: 5px 0 5px;
}
#modal-mask {
display: none;
position: fixed;
left: 0;
top: 0;
width: 100vw;
height: 100vh;
background: rgba(0,0,0,0.4);
z-index: 999;
}
body.modal-open {
overflow: hidden;
pointer-events: none;
}
body.modal-open #add_link_div,
body.modal-open #reassign_group_div {
pointer-events: auto;
}
#add_link_div, #reassign_group_div {
display: none;
position: fixed;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background: white;
padding: 20px;
border: 2px solid #ccc;
box-shadow: 0 0 10px rgba(0,0,0,0.5);
z-index: 1000;
width: 90%;
max-width: 500px;
}
#add_link_modal_container, #reassign_group_modal_container {
width: 100%;
text-align: center;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 20px;
}
#nodes_link {
width: 100%;
height: 180px;
resize: vertical;
font-family: monospace;
padding: 5px;
border: 1px solid #ccc;
border-radius: 5px;
}
#add_link_div h3,
#reassign_group_div h3 {
background: inherit;
}
#nodes_link_text {
color: red;
font-size: 14px;
margin-top: 5px;
text-align: center;
width: 100%;
}
#add_link_button_container, #reassign_group_button_container {
display: flex;
justify-content: space-between;
width: 100%;
max-width: 300px;
margin-top: 10px;
}
.value-custom {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
margin: 10px 0;
padding: 0px 5px 0px 5px;
}
.value-field-custom {
display: inline-flex;
align-items: center;
gap: 10px;
}
.value-title-custom {
font-size: 13px;
line-height: 28px;
white-space: nowrap;
text-align: right;
}
.custom-dropdown {
position: relative;
border: 1px solid #d9d9d9;
border-radius: 2px;
width: 180px;
height: 28px;
font-size: 13px;
background: #fff;
cursor: pointer;
box-sizing: border-box;
display: block;
}
.selected-display {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 8px;
width: 100%;
height: 100%;
box-sizing: border-box;
font-size: 12px;
color: #666;
}
.selected-display:hover {
background-color: #f7f7f7;
}
.dropdown-list {
position: absolute;
top: calc(100% + 2px);
left: -1px;
width: calc(100% + 2px);
border: 1px solid #d9d9d9;
border-radius: 2px;
border-top: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.15);
background: #fff;
list-style: none;
margin: 0;
padding: 0;
max-height: 200px;
overflow-y: auto;
overflow-x: hidden;
z-index: 100;
box-sizing: border-box;
color: #666;
}
.dropdown-item {
padding: 4px 8px;
line-height: 20px;
cursor: pointer;
}
.dropdown-item.selected {
background-color: #1e90ff;
color: #fff;
}
.create-item-input::placeholder {
text-align: center;
}
.dropdown-item.custom-input input {
width: 100%;
box-sizing: border-box;
padding: 3px;
font-size: 13px;
line-height: 20px;
border: 1px solid #ccc;
text-align: left;
}
.arrow-down-small {
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 5px solid #555;
display: inline-block;
vertical-align: middle;
}
</style>
<script type="text/javascript">
<% if api.is_js_luci() then -%>
var cbi_t = [];
function cbi_t_add(section, tab) {
var t = document.getElementById('tab.' + section + '.' + tab);
@@ -217,9 +416,7 @@ table td, .table .td {
}
return false
}
</script>
<%- else %>
<script type="text/javascript">
(function() {
if (typeof(cbi_t_switch) === "function") {
var old_switch = cbi_t_switch;
@@ -229,16 +426,16 @@ table td, .table .td {
};
}
})();
</script>
<%- end %>
</script>
<script type="text/javascript">
//<![CDATA[
let auto_detection_time = "<%=api.uci_get_type("global_other", "auto_detection_time", "0")%>"
let show_node_info = "<%=api.uci_get_type("global_other", "show_node_info", "0")%>"
var ajaxList = [];
var ajax = {
list: [],
post: function(url, data, fn_success, timeout, fn_timeout) {
var xhr = new XMLHttpRequest();
var code = ajax.encode(data);
@@ -267,7 +464,7 @@ table td, .table .td {
fn_success(xhr, json);
}
};
ajaxList.push(xhr);
this.list.push(xhr);
xhr.send(code);
},
encode: function(obj) {
@@ -287,65 +484,63 @@ table td, .table .td {
}
return obj;
},
abortAll: function() {
this.list.forEach(xhr => xhr.abort());
this.list = [];
}
}
function ajaxAbortAll() {
ajaxList.forEach(xhr => xhr.abort());
ajaxList = [];
}
function to_add_node() {
ajaxAbortAll();
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
window.location.href='<%=api.url("add_node")%>?redirect=1&group=' + current_group;
}
function to_edit_node(cbi_id) {
ajaxAbortAll();
location.href='<%=api.url("node_config")%>/' + cbi_id;
}
function copy_node(cbi_id) {
ajaxAbortAll();
window.location.href = '<%=api.url("copy_node")%>' + "?section=" + cbi_id;
}
function del_node(cbi_id) {
ajaxAbortAll();
window.location.href = '<%=api.url("delete_select_nodes")%>' + "?redirect=1&ids=" + cbi_id;
}
var section = "";
function open_set_node_div(cbi_id) {
section = cbi_id;
document.getElementById("set_node_div").style.display="block";
var node_name = document.getElementById("cbid.<%=appname%>." + cbi_id + ".remarks").value;
document.getElementById("set_node_name").innerHTML = node_name;
}
function close_set_node_div() {
document.getElementById("set_node_div").style.display="none";
document.getElementById("set_node_name").innerHTML = "";
}
function row_top(btn, group) {
const row = btn.closest("tr");
if (!row) return;
const parent = row.parentNode;
let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)");
if (firstDataRow && firstDataRow !== row) {
parent.insertBefore(row, firstDataRow);
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_node_order(group);
if (!arraysEqual(new_order, origin_group_node_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
function node_action(action, cbi_id, a) {
ajax.abortAll();
switch (action) {
case "add":
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
window.location.href='<%=api.url("add_node")%>?redirect=1&group=' + current_group;
break;
case "copy":
window.location.href = '<%=api.url("copy_node")%>' + "?section=" + cbi_id;
break;
case "edit":
window.location.href='<%=api.url("node_config")%>/' + cbi_id;
break;
case "del":
window.location.href = '<%=api.url("delete_select_nodes")%>' + "?redirect=1&ids=" + cbi_id;
break;
case "row_top":
let btn = a.btn;
let group = a.group;
const row = btn.closest("tr");
if (!row) return;
const parent = row.parentNode;
let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)");
if (firstDataRow && firstDataRow !== row) {
parent.insertBefore(row, firstDataRow);
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_node_order(group);
if (!arraysEqual(new_order, origin_group_node_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
}
}
}
}
break;
case "del_all":
if (confirm('<%:Are you sure to clear all nodes?%>') == true){
XHR.get('<%=api.url("clear_all_nodes")%>', null,
function(x, data) {
if(x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
break;
default:
}
}
@@ -420,6 +615,7 @@ table td, .table .td {
if (confirm('<%:Are you sure to delete select nodes?%>') == true) {
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
ajax.abortAll();
XHR.get('<%=api.url("delete_select_nodes")%>', {
ids: ids.join()
},
@@ -433,7 +629,7 @@ table td, .table .td {
box.remove();
}
*/
ajaxAbortAll();
ajax.abortAll();
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
@@ -446,14 +642,7 @@ table td, .table .td {
alert("<%:You no select nodes !%>");
}
}
function set_node(type, config) {
if (confirm('<%:Are you sure set this server?%>')==true){
ajaxAbortAll();
window.location.href = '<%=api.url("set_node")%>?type=' + type + '&config=' + config + '&section=' + section;
}
}
function get_address_full(id) {
var address = (document.getElementById("cbid.<%=appname%>." + id + ".address") || {}).value || "";
var port = (document.getElementById("cbid.<%=appname%>." + id + ".port") || {}).value || "";
@@ -501,6 +690,7 @@ table td, .table .td {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
ajax.abortAll();
XHR.get('<%=api.url("save_node_order")%>', {
group: group,
ids: ids.join(",")
@@ -741,16 +931,6 @@ table td, .table .td {
}
}
function arraysEqual(a, b) {
if (a === b) return true;
if (a == null || b == null) return false;
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false;
}
return true;
}
// List drag and rearrange
function initSortableForTable(table) {
if (!table) return null;
@@ -821,6 +1001,291 @@ table td, .table .td {
}
</script>
<script type="text/javascript">
//<![CDATA[
//Pop-up logic
// Custom group dropdown list events
function dropdown_list_fun(div_id) {
const dropdown = document.getElementById(div_id);
if (!dropdown) return;
const display = dropdown.querySelector(".selected-display");
const displayText = display.querySelector(".text");
const list = dropdown.querySelector(".dropdown-list");
const hidden = dropdown.querySelector('input[type="hidden"]');
const input = dropdown.querySelector(".create-item-input");
display.addEventListener("click", function() {
list.style.display = list.style.display === "none" ? "block" : "none";
input.value = "";
//input.focus();
});
function selectItem(li) {
list.querySelectorAll(".dropdown-item").forEach(function(el){
el.classList.remove("selected");
});
li.classList.add("selected");
hidden.value = li.dataset.value;
displayText.textContent = li.dataset.value || "<%:default%>";
list.style.display = "none";
}
list.addEventListener("click", function(e){
const li = e.target.closest(".dropdown-item");
if (!li || li.classList.contains("custom-input")) return;
selectItem(li);
});
input.addEventListener("keydown", function(e){
const isEnter = e.key === "Enter" || e.keyCode === 13;
if (!isEnter) return;
e.stopPropagation();
e.preventDefault();
const val = input.value.trim();
if (!val) return;
if (val.toLowerCase() === "default") {
const emptyLi = Array.from(list.querySelectorAll(".dropdown-item"))
.find(function(el){ return !el.dataset.value; });
if (emptyLi) selectItem(emptyLi);
input.value = "";
return;
}
let li = Array.from(list.querySelectorAll(".dropdown-item")).find(function(el){
return el.dataset.value && el.dataset.value.toLowerCase() === val.toLowerCase();
});
if (!li) {
li = document.createElement("li");
li.className = "dropdown-item";
li.dataset.value = val;
li.textContent = val;
list.insertBefore(li, input.parentNode);
}
input.value = "";
selectItem(li);
});
// Read group names from tab
function InsertGroup(list, input) {
const tabs = document.querySelectorAll(".cbi-tabmenu li");
if (!tabs.length) {
setTimeout(() => InsertGroup(list, input), 50);
return;
}
tabs.forEach(function(li){
const group = li.id.split('.').pop();
if(group === "default") return;
if(Array.from(list.querySelectorAll(".dropdown-item")).some(el => el.dataset.value === group)) return;
const newLi = document.createElement("li");
newLi.className = "dropdown-item";
newLi.dataset.value = group;
newLi.textContent = group;
list.insertBefore(newLi, input.parentNode);
});
}
InsertGroup(list, input);
// Automatically collapses when clicking on the outside.
document.addEventListener("click", function(e) {
if (!dropdown.contains(e.target)) {
list.style.display = "none";
}
});
}
function getBg(el) {
if (!el) return null;
const style = getComputedStyle(el);
const bgImage = style.backgroundImage;
const bgColor = style.backgroundColor;
return (bgImage !== 'none' || !/rgba\([^,]+,[^,]+,[^,]+,\s*0\)/.test(bgColor) && bgColor !== 'transparent')
? style.background
: null;
}
const add_link_div = {
open() {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const addLinkDiv = document.getElementById("add_link_div");
addLinkDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
addLinkDiv.style.display = "block";
if (!addLinkDiv._dropdown_inited) {
addLinkDiv._dropdown_inited = true;
dropdown_list_fun("addlink_group_custom");
}
document.getElementById("nodes_link").focus();
},
close() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("add_link_div").style.display = "none";
document.body.classList.remove('modal-open');
},
add_node() {
let nodes_link = document.getElementById("nodes_link").value;
const group = (document.querySelector('#addlink_group_custom input[type="hidden"]')?.value || "default");
nodes_link = nodes_link.replace(/\t/g, "").replace(/\r\n|\r/g, "\n").replace(/<[^>]*>/g, '').trim();
if (nodes_link != "") {
nodes_link = decodeIfBase64(nodes_link);
let s = nodes_link.split('://');
if (s.length > 1) {
this.close();
this.ajax_add_node(nodes_link, group);
} else {
alert("<%:Please enter the correct link.%>");
}
} else {
document.getElementById("nodes_link").focus();
}
},
ajax_add_node(link, group) {
const dom = document.getElementById('modal-mask');
dom.style.display = 'block';
dom.innerHTML = '<div style="position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);color:#fff;"><%:Processing, please wait…%></div>';
const chunkSize = 1000; // Fragmented sending to overcome uhttpd's limitations, with each fragment containing 1000 characters.
const totalChunks = Math.ceil(link.length / chunkSize);
let currentChunk = 0;
function sendNextChunk() {
if (currentChunk < totalChunks) {
const chunk = link.substring(currentChunk * chunkSize, (currentChunk + 1) * chunkSize);
const xhr = new XMLHttpRequest();
xhr.open('POST', '<%=api.url("link_add_node")%>', true);
xhr.onload = function () {
if (xhr.status === 200) {
currentChunk++;
sendNextChunk();
} else {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Error%>");
return;
}
};
xhr.onerror = function () {
dom.innerHTML = '';
dom.style.display = 'none';
alert("<%:Network Error%>");
return;
};
const formData = new FormData();
formData.append("chunk", chunk);
formData.append("chunk_index", currentChunk);
formData.append("total_chunks", totalChunks);
formData.append("group", group);
ajax.abortAll();
xhr.send(formData);
} else {
window.location.href = '<%=api.url("node_list")%>';
}
}
sendNextChunk();
}
}
const reassign_group_div = {
open() {
const ids = [];
const visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
document.body.classList.add('modal-open');
document.getElementById('modal-mask').style.display = 'block';
const reassignGroupDiv = document.getElementById("reassign_group_div");
reassignGroupDiv.style.background = getBg(document.querySelector('.cbi-section')) || getBg(document.body) || '';
reassignGroupDiv.style.display = "block";
if (!reassignGroupDiv._dropdown_inited) {
reassignGroupDiv._dropdown_inited = true;
dropdown_list_fun("reassign_group_custom");
}
} else {
alert("<%:You no select nodes !%>");
}
}
},
close() {
document.getElementById('modal-mask').style.display = 'none';
document.getElementById("reassign_group_div").style.display = "none";
document.body.classList.remove('modal-open');
},
action() {
const ids = [];
const group = (document.querySelector('#reassign_group_custom input[type="hidden"]')?.value || "default");
const visibleContainer = document.querySelector('#cbi-passwall2-nodes > .cbi-tabcontainer[style*="display:block"], #cbi-passwall2-nodes > .cbi-tabcontainer[style*="display: block"]');
if (!visibleContainer) return;
const doms = visibleContainer.getElementsByClassName("nodes_select");
if (doms && doms.length > 0) {
for (let i = 0 ; i < doms.length; i++) {
if (doms[i].checked) {
ids.push(doms[i].getAttribute("cbid"))
}
}
if (ids.length > 0) {
ajax.abortAll();
XHR.get('<%=api.url("reassign_group")%>', {
group: group,
ids: ids.join(",")
},
function(x, data) {
if (x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
} else {
alert("<%:Error%>");
}
});
}
}
}
}
const set_node_div = {
section: '',
open(cbi_id) {
this.section = cbi_id;
document.getElementById("set_node_div").style.display="block";
const node_name = document.getElementById("cbid.<%=appname%>." + cbi_id + ".remarks").value;
document.getElementById("set_node_name").innerHTML = node_name;
},
close() {
document.getElementById("set_node_div").style.display="none";
document.getElementById("set_node_name").innerHTML = "";
},
set(type, config) {
if (confirm('<%:Are you sure set this node?%>') == true){
ajax.abortAll();
window.location.href = '<%=api.url("set_node")%>?type=' + type + '&config=' + config + '&section=' + section;
}
}
}
//]]>
</script>
<div class="pw-toolbar">
<div class="pw-toolbar-field">
<input class="btn cbi-button cbi-button-add" type="button" onclick="node_action('add')" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_link_div.open()" value="<%:Add the node via the link%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="node_action('del_all')" value="<%:Clear all nodes%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="delete_select_nodes()" value="<%:Delete select nodes%>" />
<input class="btn cbi-button cbi-button-edit" type="button" id="select_all_btn" onclick="checked_all_node(this)" value="<%:Select all%>" />
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group_div.open()" value="<%:Reassign Group%>" />
<input class="btn cbi-button cbi-button-apply" type="submit" name="cbi.apply" value="<%:Save & Apply%>" />
<!--<input class="btn cbi-button cbi-button-save" type="submit" name="cbi.save" value="<%:Save%>" />-->
<!--<input class="btn cbi-button cbi-button-reset" type="button" value="<%:Reset%>" onclick="location.href='<%=REQUEST_URI%>'" />-->
</div>
</div>
<script type="text/template" id="nodes-table-template">
<fieldset class="cbi-section cbi-tblsection" id="cbi-<%=appname%>-nodes-{{group}}-fieldset">
@@ -838,7 +1303,7 @@ table td, .table .td {
{{node-tr}}
</table>
<div class="cbi-section-create cbi-tblsection-create">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="to_add_node()">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="node_action('add')">
<input class="cbi-button cbi-button-apply" style="display: none" type="button" id="save_order_btn_{{group}}" value="<%:Save Order%>" onclick="save_current_page_order('{{group}}')">
</div>
</fieldset>
@@ -858,11 +1323,11 @@ table td, .table .td {
<td class="td cbi-value-field pw-urltest">{{url_test}}</td>
<td class="td cbi-section-table-cell nowrap cbi-section-actions pw-actions">
<div class="node-wrapper">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="row_top(this, '{{group}}')" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Use%>" id="apply_{{id}}" onclick="open_set_node_div('{{id}}')"/>
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Copy%>" onclick="copy_node('{{id}}')"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="to_edit_node('{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="del_node('{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="node_action('row_top', null, {btn:this, group:'{{group}}'})" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-apply" type="button" value="<%:Use%>" id="apply_{{id}}" onclick="set_node_div.open('{{id}}')"/>
<input class="btn cbi-button cbi-button-add" type="button" value="<%:Copy%>" onclick="node_action('copy', '{{id}}')"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="node_action('edit', '{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="node_action('del', '{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<span class="drag-handle center" title="<%:Drag to reorder%>">⠿</span>
</div>
</td>
@@ -873,6 +1338,8 @@ table td, .table .td {
</fieldset>
<script type="text/javascript">
//init logic
function get_remarks_name(o) {
let str = "";
let remarks = o["remarks"] || "";
@@ -949,9 +1416,8 @@ table td, .table .td {
group_nodes[_node.group].push(_node)
}
var tab_ul_html = '<ul class="cbi-tabmenu">'
var tab_ul_li_html = ''
var tab_content_html = '<fieldset class="cbi-section-node cbi-section-node-tabbed" id="cbi-<%=appname%>-nodes">'
var tab_content_html = ''
var nodes_table_template = document.getElementById("nodes-table-template");
var node_template = document.getElementById("node-tr-template");
var default_group = null
@@ -1025,11 +1491,21 @@ table td, .table .td {
'</div>'
}
tab_ul_html += tab_ul_li_html + '</ul>'
tab_content_html += '</fieldset>'
var tab_html = tab_ul_html + tab_content_html
function addTabDOM(li, content) {
let tab_ul_html = '<ul class="cbi-tabmenu">';
let tab_content_html = '<fieldset class="cbi-section-shunt_rule cbi-section-shunt_rule-tabbed" id="cbi-<%=appname%>-nodes">';
if (li) {
tab_ul_html += li;
}
if (content) {
tab_content_html += content;
}
tab_ul_html += '</ul>';
tab_content_html += '</fieldset>';
return tab_ul_html + tab_content_html;
}
document.getElementById("node_list").innerHTML = tab_html
document.getElementById("node_list").innerHTML = addTabDOM(tab_ul_li_html, tab_content_html);
for (let group in group_nodes) {
cbi_t_add("<%=appname%>.nodes", group)
@@ -1038,7 +1514,7 @@ table td, .table .td {
if (default_group) {
cbi_t_switch("<%=appname%>.nodes", default_group)
}
origin_group_node_order = {};
for (let group in group_nodes) {
origin_group_node_order[group] = get_node_order(group);
@@ -1071,20 +1547,8 @@ table td, .table .td {
//Node list option saving logic
document.addEventListener("DOMContentLoaded", function () {
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function onChange(option, value) {
ajax.abortAll();
XHR.get('<%=api.url("save_node_list_opt")%>', {
option: option,
value: value
@@ -1118,31 +1582,96 @@ table td, .table .td {
links.forEach(link => {
link.addEventListener('click', (e) => {
if (link.getAttribute('target') !== '_blank') {
ajaxAbortAll();
ajax.abortAll();
}
});
});
window.addEventListener('beforeunload', (event) => {
ajax.abortAll();
});
});
//]]>
</script>
<div style="display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center;">
<div id="set_node_div" style="text-align: center;">
<div class="cbi-value" style="display: flex; justify-content: center; align-items: center; margin: 8px 0;">
<font color=#666><%:You choose node is:%></font><a style="color: red" id="set_node_name"></a>
<div id="modal-mask"></div>
<div id="add_link_div">
<div id="add_link_modal_container">
<h3><%:Add the node via the link%></h3>
<div class="value-custom">
<textarea id="nodes_link" rows="10"></textarea>
<p id="nodes_link_text"><%:Enter share links, one per line. Subscription links are not supported!%></p>
</div>
<div class="cbi-value" style="display: flex; justify-content: center; gap: 8px; margin: 8px 0;">
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="addlink_group_custom"><%:Group Name%></label>
<div id="addlink_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="addlink_group" value="">
</div>
</div>
</div>
<div id="add_link_button_container">
<input class="btn cbi-button cbi-button-add" type="button" onclick="add_link_div.add_node()" value="<%:Add%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="add_link_div.close()" value="<%:Close%>" />
</div>
</div>
</div>
<div id="reassign_group_div">
<div id="reassign_group_modal_container">
<h3><%:Reassign Node Group%></h3>
<div class="value-custom">
<div class="value-field-custom">
<label class="value-title-custom" for="reassign_group_custom"><%:Group Name%></label>
<div id="reassign_group_custom" class="custom-dropdown">
<div class="selected-display">
<span class="text"><%:default%></span>
<span class="arrow-down-small"></span>
</div>
<ul class="dropdown-list" style="display:none;">
<li class="dropdown-item selected" data-value=""><%:default%></li>
<li class="dropdown-item custom-input">
<input type="text" placeholder="-- <%:custom%> --" class="create-item-input" inputmode="text" enterkeyhint="done">
</li>
</ul>
<input type="hidden" name="to_group" value="">
</div>
</div>
</div>
<div id="reassign_group_button_container">
<input class="btn cbi-button cbi-button-edit" type="button" onclick="reassign_group_div.action()" value="<%:Save%>" />
<input class="btn cbi-button cbi-button-remove" type="button" onclick="reassign_group_div.close()" value="<%:Close%>" />
</div>
</div>
</div>
<div style="display: -webkit-flex; display: flex; -webkit-align-items: center; align-items: center; -webkit-justify-content: center; justify-content: center;">
<div id="set_node_div">
<div class="cbi-value" style="display: flex; justify-content: center; align-items: center; margin: 1em 0; height: 2em;">
<font color=#666><%:You choose node is:%></font><a style="color: red" id="set_node_name"></a>
<div style="position:absolute; right:0.5em; top: 0.5em;"><input class="btn cbi-button" type="button" onclick="set_node_div.close()" value="X" /></div>
</div>
<div class="cbi-value" style="justify-content: center; gap: 8px; margin: 1em 0;">
<%- if default_node_type == "_shunt" then
for i, v in ipairs(shunt_rule_list) do
-%>
<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node('<%=node%>', '<%=v[".name"]%>')" value="<%=v.remarks%>" />
<input class="btn cbi-button" type="button" onclick="set_node_div.set('<%=node%>', '<%=v[".name"]%>')" value="<%=v.remarks%>" />
<%-
end
-%>
<% else %>
<input class="btn cbi-button cbi-button-edit" type="button" onclick="set_node('@global[0]', 'node')" value="<%:Node%>" />
<input class="btn cbi-button" type="button" onclick="set_node_div.set('@global[0]', 'node')" value="<%:Node%>" />
<% end %>
<input class="btn cbi-button cbi-button-remove" type="button" onclick="close_set_node_div()" value="<%:Close%>" />
</div>
</div>
</div>
@@ -0,0 +1,465 @@
<%
local api = require "luci.passwall2.api"
local appname = api.appname
local uci = api.uci
-%>
<script src="<%=resource%>/view/<%=appname%>/Sortable.min.js"></script>
<style>
table th, .table .th {
text-align: center;
}
table td, .table .td {
text-align: center;
/* white-space: nowrap; */
word-break: keep-all;
}
.td.cbi-section-actions {
text-align: right !important;
}
.shunt_rule-wrapper {
align-items: center;
display: inline-flex !important;
gap: 4px;
}
.cbi-tabmenu > li {
margin-right: 2px !important;
}
.cbi-tabmenu > li:last-child {
margin-right: 0 !important;
}
.shunt_rule-wrapper .drag-handle {
cursor: grab !important;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
font-weight: 100;
padding: 0 !important;
line-height: inherit;
user-select: none;
align-self: stretch;
background-color: transparent;
}
.shunt_rule-wrapper .drag-handle:hover {
background: transparent;
}
/* enable flex for small screens*/
@media screen and (max-width: 1152px) {
.cbi-section-table-row {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: space-between;
}
.cbi-section-table-titles {
display: none !important;
}
.td-actions {
padding-top: 0 !important;
border-top-width: 0 !important;
flex: 1 1 350px;
}
}
/* shrink actionbar even further for mobile devices */
@media screen and (max-width: 500px) {
.shunt_rule-wrapper {
gap: 0;
}
.cbi-button {
margin-left: 0 !important;
margin-right: 1px !important;
}
.td-actions {
padding-left: 5px!important;
padding-right: 5px !important;
}
}
.sortable-chosen {
background-color: rgba(220, 235, 245, 0.4) !important;
opacity: 0.7;
}
.sortable-ghost {
background: #cce5ff !important;
height: 3px !important;
}
.dragging-row {
background-color: rgba(131, 191, 255, 0.7) !important;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
</style>
<% if api.is_js_luci() then -%>
<script type="text/javascript">
var cbi_t = [];
function cbi_t_add(section, tab) {
var t = document.getElementById('tab.' + section + '.' + tab);
var c = document.getElementById('container.' + section + '.' + tab);
if( t && c ) {
cbi_t[section] = (cbi_t[section] || [ ]);
cbi_t[section][tab] = { 'tab': t, 'container': c, 'cid': c.id };
}
}
function cbi_t_switch(section, tab) {
if( cbi_t[section] && cbi_t[section][tab] ) {
// Before switching tabs, first deselect all currently active tabs.
var o = cbi_t[section][tab];
var h = document.getElementById('tab.' + section);
for( var tid in cbi_t[section] ) {
var o2 = cbi_t[section][tid];
if( o.tab.id != o2.tab.id ) {
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab( |$)/, " cbi-tab-disabled ");
o2.container.style.display = 'none';
}
else {
if(h) h.value = tab;
o2.tab.className = o2.tab.className.replace(/(^| )cbi-tab-disabled( |$)/, " cbi-tab ");
o2.container.style.display = 'block';
}
}
}
return false
}
</script>
<%- end %>
<script type="text/javascript">
//<![CDATA[
function toPage(action, cbi_id) {
if (action == "add") {
const dom = document.getElementsByClassName("cbi-tab")[0];
const current_group = dom ? (dom.getAttribute("group_name") || "default") : "default";
const add_name_val = document.getElementById("add_name_" + current_group).value;
if (add_name_val.length > 1) {
XHR.get('<%=api.url("add_shunt_rule")%>', {
add_name: add_name_val,
group: current_group
},
function(x, result) {
if (x && x.status === 200) {
if (result.code == 1) {
window.location.href = result.data.redirect_url;
} else {
alert(result.data.message);
}
} else {
alert("<%:Save failed!%>");
}
}
);
}
}
if (action == "edit") {
window.location.href='<%=api.url("shunt_rules")%>/' + cbi_id;
}
if (action == "del") {
window.location.href = '<%=api.url("delete_select_shunt_rules")%>' + "?redirect=1&ids=" + cbi_id;
}
}
function row_top(btn, group) {
const row = btn.closest("tr");
if (!row) return;
const parent = row.parentNode;
let firstDataRow = parent.querySelector("tr:not(.cbi-section-table-titles)");
if (firstDataRow && firstDataRow !== row) {
parent.insertBefore(row, firstDataRow);
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_shunt_rule_order(group);
if (!arraysEqual(new_order, origin_group_shunt_rule_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
}
}
}
}
function get_shunt_rule_order(group) {
let table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
if (!table) {
return;
}
let rows = table.querySelectorAll("tr.cbi-section-table-row");
if (!rows || rows.length === 0) {
return;
}
var ids = [];
rows.forEach(function(row) {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
return ids;
}
function save_current_page_order(group) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
if (!table) {
alert("<%:No table!%>");
return;
}
var rows = table.querySelectorAll("tr.cbi-section-table-row");
if (!rows || rows.length === 0) {
alert("<%:No shunt_rules!%>");
return;
}
var btn = document.getElementById("save_order_btn_" + group);
if (btn) {
btn.style.display = "none";
btn.disabled = true;
}
var ids = [];
rows.forEach(function(row) {
var id = row.id.replace("cbi-<%=appname%>-", "");
ids.push(id);
});
XHR.get('<%=api.url("save_shunt_rule_order")%>', {
group: group,
ids: ids.join(",")
},
function(x, result) {
if (btn) {
btn.style.display = null;
btn.disabled = false;
}
if (x && x.status === 200) {
origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
alert("<%:Saved current page order successfully.%>");
if (btn) {
btn.style.display = "none";
}
} else {
alert("<%:Save failed!%>");
}
}
);
}
// List drag and rearrange
function initSortableForTable(table) {
if (!table) return null;
let group = table.id.replace("cbi-<%=appname%>-shunt_rules-", "").replace("-table", "")
var root = table.querySelector('tbody') || table;
if (root._sortable_initialized) return root._sortable_instance;
root._sortable_initialized = true;
var opts = {
handle: ".drag-handle",
draggable: "tr.cbi-section-table-row",
animation: 150,
ghostClass: "dragging-row",
fallbackOnBody: true,
forceFallback: false,
swapThreshold: 0.65,
onEnd: function (evt) {
//save_current_page_order(group); // Auto save
let save_order_btn = document.getElementById("save_order_btn_" + group);
if (save_order_btn) {
const new_order = get_shunt_rule_order(group);
if (!arraysEqual(new_order, origin_group_shunt_rule_order[group])) {
save_order_btn.style.display = null;
} else {
save_order_btn.style.display = "none";
}
}
}
};
try {
var instance = Sortable.create(root, opts);
root._sortable_instance = instance;
return instance;
} catch (err) {
root._sortable_initialized = false;
console.error("Sortable init failed:", err);
return null;
}
}
function initAllSortable(group_shunt_rules) {
if (typeof Sortable === 'undefined') {
var retries = 0;
var maxRetries = 25;
var t = setInterval(function () {
retries++;
if (typeof Sortable !== 'undefined') {
clearInterval(t);
for (var group in group_shunt_rules) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
initSortableForTable(table);
}
} else if (retries >= maxRetries) {
clearInterval(t);
}
}, 200);
} else {
for (var group in group_shunt_rules) {
var table = document.getElementById("cbi-<%=appname%>-shunt_rules-" + group + "-table");
initSortableForTable(table);
}
}
}
function escape_html(s) {
return s.replace(/[&<>"']/g, c => ({
"&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;"
}[c]));
}
</script>
<script type="text/template" id="shunt_rules-table-template">
<fieldset class="cbi-section cbi-tblsection" id="cbi-<%=appname%>-shunt_rules-{{group}}-fieldset">
<table class="table cbi-section-table" id="cbi-<%=appname%>-shunt_rules-{{group}}-table" style="">
<tr class="tr cbi-section-table-titles anonymous">
<th class="th cbi-section-table-cell" style="width:30%">ID</th>
<th class="th cbi-section-table-cell" style="width:30%"><%:Remarks%></th>
<th class="th cbi-section-table-cell cbi-section-actions"></th>
</tr>
{{shunt_rule-tr}}
</table>
<div class="cbi-section-create cbi-tblsection-create">
<input type="text" id="add_name_{{group}}">
<input class="cbi-button cbi-button-add" type="button" value="<%:Add%>" onclick="toPage('add')">
<input class="cbi-button cbi-button-apply" style="display: none" type="button" id="save_order_btn_{{group}}" value="<%:Save Order%>" onclick="save_current_page_order('{{group}}')">
</div>
</fieldset>
</script>
<script type="text/template" id="shunt_rule-tr-template">
<tr class="tr cbi-section-table-row" id="cbi-<%=appname%>-{{id}}">
<td class="td cbi-value-field"><b>{{id}}</b></td>
<td class="td cbi-value-field">{{remarks}}</td>
<td class="td cbi-section-table-cell nowrap cbi-section-actions td-actions">
<div class="shunt_rule-wrapper">
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:To Top%>" onclick="row_top(this, '{{group}}')" title="<%:To Top%>"/>
<input class="btn cbi-button cbi-button-edit" type="button" value="<%:Edit%>" onclick="toPage('edit', '{{id}}')" alt="<%:Edit%>" title="<%:Edit%>">
<input class="btn cbi-button cbi-button-remove" type="button" value="<%:Delete%>" onclick="toPage('del', '{{id}}')" alt="<%:Delete%>" title="<%:Delete%>">
<span class="drag-handle center" title="<%:Drag to reorder%>">⠿</span>
</div>
</td>
</tr>
</script>
<div class="cbi-section">
<h3>Sing-Box/Xray <%:Shunt Rule%></h3>
<div class="cbi-section-descr"><a style="color: red;text-decoration: none"><%:Please note attention to the priority, the higher the order, the higher the priority.%></a></div>
<div id="shunt_rule_list"></div>
</div>
<script type="text/javascript">
function loadNodeList() {
XHR.get('<%=api.url("get_shunt_rules")%>', null, function(x, result) {
var shunt_rule_list = result
var group_shunt_rules = {}
for (let i = 0; i < shunt_rule_list.length; i++) {
let _shunt_rule = shunt_rule_list[i]
if (!_shunt_rule.group || _shunt_rule.group === "") {
_shunt_rule.group = "default"
}
if (!group_shunt_rules[_shunt_rule.group]) {
group_shunt_rules[_shunt_rule.group] = []
}
group_shunt_rules[_shunt_rule.group].push(_shunt_rule)
}
var tab_ul_li_html = '';
var tab_content_html = '';
var shunt_rules_table_template = document.getElementById("shunt_rules-table-template");
var shunt_rule_template = document.getElementById("shunt_rule-tr-template");
var default_group = null
for (let group in group_shunt_rules) {
if (default_group == null)
default_group = group
var table_html = "";
if (true) {
var new_shunt_rules_table_dom = shunt_rules_table_template.cloneNode(true);
var _html = new_shunt_rules_table_dom.innerHTML;
_html = _html.split("{{group}}").join(group);
var shunt_rule_tr_html = "";
for (var i = 0; i < group_shunt_rules[group].length; i++) {
let o = group_shunt_rules[group][i]
var newDom = shunt_rule_template.cloneNode(true);
newDom.classList.add("cbi-rowstyle-" + (i % 2 + 1));
var innerHTML = newDom.innerHTML;
innerHTML = innerHTML.split("{{id}}").join(o[".name"]);
innerHTML = innerHTML.split("{{group}}").join(o["group"] || "");
innerHTML = innerHTML.split("{{remarks}}").join(o["remarks"] || "");
shunt_rule_tr_html += innerHTML
}
_html = _html.split("{{shunt_rule-tr}}").join(shunt_rule_tr_html);
table_html = _html;
}
var group_name = group
if (group === "default") {
group_name = "<%:default%>"
}
tab_ul_li_html +=
'<li group_name="' + group + '" id="tab.<%=appname%>.shunt_rules.' + group + '" class="cbi-tab">' +
'<a onclick="this.blur(); return cbi_t_switch(\'<%=appname%>.shunt_rules\', \'' + group + '\')" href="<%=REQUEST_URI%>?tab.<%=appname%>.shunt_rules=' + group + '">' + escape_html(group_name) + " | " + "<font style='color: red'>" + group_shunt_rules[group].length + '</font></a>' +
'</li>'
tab_content_html +=
'<div class="cbi-tabcontainer" id="container.<%=appname%>.shunt_rules.' + group + '" style="display: none;">' +
'' + table_html +
'</div>'
}
function addTabDOM(li, content) {
let tab_ul_html = '<ul class="cbi-tabmenu">';
let tab_content_html = '<fieldset class="cbi-section-shunt_rule cbi-section-shunt_rule-tabbed" id="cbi-<%=appname%>-shunt_rules">';
if (li) {
tab_ul_html += li;
}
if (content) {
tab_content_html += content;
}
tab_ul_html += '</ul>';
tab_content_html += '</fieldset>';
return tab_ul_html + tab_content_html;
}
document.getElementById("shunt_rule_list").innerHTML = addTabDOM(tab_ul_li_html, tab_content_html);
for (let group in group_shunt_rules) {
cbi_t_add("<%=appname%>.shunt_rules", group)
}
if (default_group) {
cbi_t_switch("<%=appname%>.shunt_rules", default_group)
}
origin_group_shunt_rule_order = {};
for (let group in group_shunt_rules) {
origin_group_shunt_rule_order[group] = get_shunt_rule_order(group);
}
initAllSortable(group_shunt_rules);
});
}
loadNodeList();
//]]>
</script>
@@ -3,6 +3,9 @@ local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("server_user")%>/' + node_id;
document.addEventListener("DOMContentLoaded", function () {
waitForElement('select[name*="<%=self.config%>"][name*="type"]', function(el) {
let o_val = el.value;
@@ -12,12 +15,12 @@ local api = self.api
let save = true;
if (save) {
//document.getElementsByClassName('cbi-button-save')[0].click();
update_node({
enable: getOption("enable").value,
remarks: getOption("remarks").value,
type: getOption("type").value,
server_update_config(node_id, {
enable: getOption("<%=self.config%>", node_id, "enable").value,
remarks: getOption("<%=self.config%>", node_id, "remarks").value,
type: getOption("<%=self.config%>", node_id, "type").value,
protocol: ''
});
}, node_config_url);
}
}
});
@@ -1,51 +1,15 @@
<%
local api = self.api
-%>
<script type="text/javascript">
//<![CDATA[
let node_id = "<%=self.section%>";
let node_config_url = '<%=api.url("server_user")%>/' + node_id;
function waitForElement(selector, callback) {
const el = document.querySelector(selector);
if (el) return callback(el);
const observer = new MutationObserver(() => {
const el = document.querySelector(selector);
if (el) {
observer.disconnect();
callback(el);
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function getOption(opt) {
var obj;
var id = 'cbid.<%=self.config%>.' + node_id + '.' + opt;
obj = document.getElementsByName(id)[0] || document.getElementById(id);
if (obj) {
var combobox = document.getElementById('cbi.combobox.' + id);
if (combobox) {
obj.combobox = combobox;
}
var div = document.getElementById(id);
if (div && div.getElementsByTagName("li").length > 0) {
obj = div;
}
return obj;
} else {
return null;
}
}
function update_node(data) {
XHR.get('<%=api.url("server_user_update")%>', {
id: node_id,
function server_update_config(cfgid, data, redirect_url) {
XHR.get('<%=self.api.url("server_update_config")%>', {
id: cfgid,
data: JSON.stringify(data)
},
function(x, data) {
if (x && x.status == 200 && data.code == 1) {
window.location.href = node_config_url;
if (redirect_url) {
window.location.href = redirect_url;
}
}
else {
alert("<%:Error%>");
+23 -2
View File
@@ -337,8 +337,8 @@ msgstr "آیا از حذف گره‌های انتخاب شده اطمینان د
msgid "You no select nodes !"
msgstr "شما هیچ گره‌ای انتخاب نکرده‌اید!"
msgid "Are you sure set to"
msgstr "آیا مطمئن هستید که تنظیم شود به"
msgid "Are you sure set this node?"
msgstr "مطمئنی این گره رو تنظیم کردی؟"
msgid "the server?"
msgstr "سرور؟"
@@ -970,6 +970,9 @@ msgstr "توجه: مصرف منابع را افزایش می‌دهد."
msgid "Shunt Rule"
msgstr "قانون شنت"
msgid "Shunt Rule Group"
msgstr "گروه قوانین شنت"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "لطفاً به اولویت توجه کنید، هرچه ترتیب بالاتر باشد، اولویت بیشتر است."
@@ -1750,6 +1753,12 @@ msgstr "حل مشکل DNS دامنه"
msgid "If the node address is a domain name, this DNS will be used for resolution."
msgstr "اگر آدرس گره یک نام دامنه باشد، از این DNS برای حل مسئله استفاده خواهد شد."
msgid "Enable Happy Eyeballs"
msgstr "فعال کردن چشم‌های خوشحال"
msgid "Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection."
msgstr "IPv4 و IPv6 را همزمان امتحان می‌کند؛ به‌طور خودکار از اتصال سریع‌تر استفاده می‌کند."
msgid "Supports only Xray or Sing-box node types."
msgstr "فقط گره‌های نوع Xray یا Sing-box پشتیبانی می‌شوند."
@@ -2413,3 +2422,15 @@ msgstr "اندازه بسته Gecko (حداکثر)"
msgid "valid time (hh:mm)"
msgstr "زمان معتبر (ساعت:میلی‌متر)"
msgid "Disable TLS Reuse"
msgstr "غیرفعال کردن استفاده مجدد از TLS"
msgid "Not Set"
msgstr "تنظیم نشده"
msgid "Cipher Suites"
msgstr "سوئیت‌های رمزدار"
msgid "Configures the list of supported cipher suites, separated by :"
msgstr "فهرست مجموعه‌های رمز پشتیبانی‌شده را که با موارد زیر از هم جدا شده‌اند، پیکربندی می‌کند:"
+23 -2
View File
@@ -334,8 +334,8 @@ msgstr "Вы действительно хотите удалить выбран
msgid "You no select nodes !"
msgstr "Вы не выбрали ни одного узла!"
msgid "Are you sure set to"
msgstr "Вы действительно хотите назначить"
msgid "Are you sure set this node?"
msgstr "Вы уверены, что задали этот узел?"
msgid "the server?"
msgstr "активным сервером?"
@@ -971,6 +971,9 @@ msgstr "Примечание: увеличивает нагрузку на си
msgid "Shunt Rule"
msgstr "Правила разделения трафика"
msgid "Shunt Rule Group"
msgstr "Группа правил шунтирования"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "Обратите внимание: правила выше в списке имеют более высокий приоритет."
@@ -1751,6 +1754,12 @@ msgstr "Разрешение DNS-запросов для домена"
msgid "If the node address is a domain name, this DNS will be used for resolution."
msgstr "Если адрес узла является доменным именем, для разрешения будет использоваться этот DNS-сервер."
msgid "Enable Happy Eyeballs"
msgstr "Включить счастливые глаза"
msgid "Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection."
msgstr "Одновременно пытается использовать IPv4 и IPv6; автоматически выбирает более быстрое соединение."
msgid "Supports only Xray or Sing-box node types."
msgstr "Поддерживаются только узлы типа Xray или Sing-box."
@@ -2411,3 +2420,15 @@ msgstr "Размер упаковки Gecko (макс)"
msgid "valid time (hh:mm)"
msgstr "Время действия (чч:мм)"
msgid "Disable TLS Reuse"
msgstr "Отключить повторное использование TLS"
msgid "Not Set"
msgstr "Не задано"
msgid "Cipher Suites"
msgstr "Наборы шифров"
msgid "Configures the list of supported cipher suites, separated by :"
msgstr "Задает список поддерживаемых наборов шифров, разделенных двоеточием (:)."
+23 -2
View File
@@ -334,8 +334,8 @@ msgstr "你确定要删除选择的节点吗?"
msgid "You no select nodes !"
msgstr "你没有选择任何节点!"
msgid "Are you sure set to"
msgstr "你确定要设为"
msgid "Are you sure set this node?"
msgstr "你确定要设置这个节点吗?"
msgid "the server?"
msgstr "服务器吗?"
@@ -962,6 +962,9 @@ msgstr "注:会增加一些系统资源的开销。"
msgid "Shunt Rule"
msgstr "分流规则"
msgid "Shunt Rule Group"
msgstr "分流规则组"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "请注意优先级问题,排序越上面优先级越高。"
@@ -1736,6 +1739,12 @@ msgstr "域名 DNS 解析"
msgid "If the node address is a domain name, this DNS will be used for resolution."
msgstr "如果节点地址是域名,则将使用此 DNS 进行解析。"
msgid "Enable Happy Eyeballs"
msgstr "启用 Happy Eyeballs"
msgid "Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection."
msgstr "同时尝试 IPv4 和 IPv6 连接,自动使用更快的连接。"
msgid "Supports only Xray or Sing-box node types."
msgstr "仅支持 Xray 或 Sing-box 类型节点。"
@@ -2399,3 +2408,15 @@ msgstr "Gecko 包大小(最大)"
msgid "valid time (hh:mm)"
msgstr "有效时间(hh:mm)"
msgid "Disable TLS Reuse"
msgstr "禁用 TLS 连接复用"
msgid "Not Set"
msgstr "未设置"
msgid "Cipher Suites"
msgstr "密码套件"
msgid "Configures the list of supported cipher suites, separated by :"
msgstr "配置支持的密码套件列表,以冒号 (:) 分隔。"
+23 -2
View File
@@ -334,8 +334,8 @@ msgstr "你確定要刪除選择的節點吗?"
msgid "You no select nodes !"
msgstr "你沒有選择任何節點!"
msgid "Are you sure set to"
msgstr "你確定要設為"
msgid "Are you sure set this node?"
msgstr "你確定要設定這個節點嗎?"
msgid "the server?"
msgstr "服務器吗?"
@@ -962,6 +962,9 @@ msgstr "注:会增加一些系統资源的開销。"
msgid "Shunt Rule"
msgstr "分流規則"
msgid "Shunt Rule Group"
msgstr "分流規則組"
msgid "Please note attention to the priority, the higher the order, the higher the priority."
msgstr "請注意優先級問題,排序越上面優先級越高。"
@@ -1742,6 +1745,12 @@ msgstr "域名 DNS 解析"
msgid "If the node address is a domain name, this DNS will be used for resolution."
msgstr "如果節點地址是域名,則將使用此 DNS 進行解析。"
msgid "Enable Happy Eyeballs"
msgstr "啓用 Happy Eyeballs"
msgid "Attempts IPv4 and IPv6 simultaneously; automatically uses the faster connection."
msgstr "同時嘗試 IPv4 和 IPv6 鏈接,自動使用更快的鏈接。"
msgid "Supports only Xray or Sing-box node types."
msgstr "僅支援 Xray 或 Sing-box 類型節點。"
@@ -2405,3 +2414,15 @@ msgstr "Gecko 包大小(最大)"
msgid "valid time (hh:mm)"
msgstr "有效時間(hh:mm)"
msgid "Disable TLS Reuse"
msgstr "禁用 TLS 連接復用"
msgid "Not Set"
msgstr "未設置"
msgid "Cipher Suites"
msgstr "密碼套件"
msgid "Configures the list of supported cipher suites, separated by :"
msgstr "配置支援的密碼套件列表,以冒號 (:) 分隔。"
@@ -66,6 +66,25 @@ frag_len=$(uci -q get passwall2.@global_xray[0].fragment_length)
[ -n "$frag_len" ] && uci -q set passwall2.@global_xray[0].fragment_lengths="$frag_len" && uci -q delete passwall2.@global_xray[0].fragment_length
frag_dly=$(uci -q get passwall2.@global_xray[0].fragment_delay)
[ -n "$frag_dly" ] && uci -q set passwall2.@global_xray[0].fragment_delays="$frag_dly" && uci -q delete passwall2.@global_xray[0].fragment_delay
for sid in $(uci show passwall2 | grep "=socks" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}'); do
if [ -z "$(echo ${sid} | grep '^socks_')" ]; then
new_id="socks_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 5)"
uci -q rename passwall2.${sid}="${new_id}"
unset sid new_id
fi
done
for sid in $(uci show passwall2 | grep "@acl_rule" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
new_id="acl_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 5)"
uci -q rename passwall2.${sid}="${new_id}"
unset sid new_id
done
for sid in $(uci show passwall2 | grep "@subscribe_list" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
new_id="sub_$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 5)"
uci -q rename passwall2.${sid}="${new_id}"
unset sid new_id
done
uci -q commit passwall2
sed -i "s#add_from#group#g" /etc/config/passwall2 2>/dev/null
@@ -59,51 +59,6 @@ config global_subscribe
config global_singbox
config shunt_rules 'China'
option remarks 'China'
option network 'tcp,udp'
option domain_list 'geosite:cn'
option ip_list 'geoip:cn'
config shunt_rules 'Iran'
option remarks 'Iran'
option network 'tcp,udp'
option domain_list 'geosite:ir
#rule-set:remote:https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs'
option ip_list 'geoip:ir
#rule-set:remote:https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs'
config shunt_rules 'Russia_Block'
option remarks 'Russia_Block'
option network 'tcp,udp'
option domain_list 'geosite:ru-blocked
#rule-set:remote:https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/refs/heads/release/sing-box/rule-set-geosite/geosite-ru-blocked.srs'
option ip_list 'geoip:ru-blocked
#rule-set:remote:https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/refs/heads/release/sing-box/rule-set-geoip/geoip-ru-blocked.srs'
config shunt_rules 'Russia'
option remarks 'Russia'
option network 'tcp,udp'
option ip_list 'geoip:ru
#rule-set:remote:https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/refs/heads/release/sing-box/rule-set-geoip/geoip-ru.srs'
config shunt_rules 'PrivateIP'
option remarks 'PrivateIP'
option network 'tcp,udp'
option ip_list '10.0.0.0/8
127.0.0.0/8
198.51.100.0/24
192.0.0.0/24
169.254.0.0/16
224.0.0.0/3
192.168.0.0/16
192.88.99.0/24
100.64.0.0/10
172.16.0.0/12
0.0.0.0/8
192.0.2.0/24
203.0.113.0/24'
config nodes 'examplenode'
option remarks 'Example'
option type 'Xray'
@@ -126,3 +81,151 @@ config nodes 'rulenode'
option domainMatcher 'hybrid'
option write_ipset_direct '1'
option enable_geoview_ip '1'
option shunt_group 'CN'
config shunt_rules 'DirectFront'
option remarks 'DirectFront'
option network 'tcp,udp'
option domain_list 'domain:ip233.cn'
option group 'CN'
config shunt_rules 'ProxyFront'
option remarks 'ProxyFront'
option network 'tcp,udp'
option domain_list 'domain:www.gstatic.com
domain:ssl.gstatic.com
domain:fonts.gstatic.com'
option group 'CN'
config shunt_rules 'DirectGame'
option remarks 'DirectGame'
option domain_list '# steam直连域名获取国内CDN走国内线路下载
cm.steampowered.com
steamserver.net
# steam国内CDN华为云
steampipe.steamcontent.tnkjmec.com
# steam国内CDN白山云
st.dl.eccdnx.com
st.dl.bscstorage.net
st.dl.pinyuncloud.com
# steam国内CDN新流云(原金山云)(支持ipv6)
dl.steam.clngaa.com
# steam国内CDN网宿
cdn.mileweb.cs.steampowered.com.8686c.com
cdn-ws.content.steamchina.com
# steam国内CDN腾讯云 (蒸汽中国独占)
cdn-qc.content.steamchina.com
# steam国内CDN阿里云(支持ipv6)
cdn-ali.content.steamchina.com
xz.pphimalayanrt.com
lv.queniujq.cn
alibaba.cdn.steampipe.steamcontent.com
# 国内游戏geosite域名
geosite:category-games@cn'
option ip_list '# steam直连IP
45.121.184.0/24
103.10.124.0/23
103.28.54.0/24
146.66.152.0/24
146.66.155.0/24
153.254.86.0/24
155.133.224.0/22
155.133.230.0/24
155.133.232.0/23
155.133.234.0/24
155.133.236.0/22
155.133.240.0/23
155.133.244.0/23
155.133.246.0/24
155.133.248.0/21
162.254.192.0/21
185.25.182.0/23
190.217.32.0/22
192.69.96.0/22
205.196.6.0/24
208.64.200.0/22
208.78.164.0/22
205.185.194.0/24'
option network 'tcp,udp'
option group 'CN'
config shunt_rules 'ProxyGame'
option remarks 'ProxyGame'
option domain_list '# steam 商店/客服/聊天/网页布局/API/二维码/Google云同步 代理URL
steamcommunity.com
www.steamcommunity.com
store.steampowered.com
checkout.steampowered.com
api.steampowered.com
help.steampowered.com
login.steampowered.com
store.akamai.steamstatic.com
steambroadcast.akamaized.net
steamvideo-a.akamaihd.net
steamusercontent-a.akamaihd.net
steamstore-a.akamaihd.net
steamcommunity-a.akamaihd.net
steamcdn-a.akamaihd.net
steamuserimages-a.akamaihd.net
community.akamai.steamstatic.com
avatars.akamai.steamstatic.com
community.steamstatic.com
cdn.akamai.steamstatic.com
avatars.steamstatic.com
shared.akamai.steamstatic.com
clan.akamai.steamstatic.com
cdn.cloudflare.steamstatic.com
community.cloudflare.steamstatic.com
store.cloudflare.steamstatic.com
avatars.cloudflare.steamstatic.com
clan.cloudflare.steamstatic.com
shared.cloudflare.steamstatic.com
steam-chat.com
steamcloud-ugc.storage.googleapis.com
steamcloud-eu-ams.storage.googleapis.com
steamcloud-eu-fra.storage.googleapis.com
steamcloud-finland.storage.googleapis.com
steamcloud-saopaulo.storage.googleapis.com
steamcloud-singapore.storage.googleapis.com
steamcloud-sydney.storage.googleapis.com
steamcloud-taiwan.storage.googleapis.com
steamcloud-eu.storage.googleapis.com
domain:blizzard.com
geosite:category-games'
option network 'tcp,udp'
option group 'CN'
config shunt_rules 'CN'
option remarks 'CN'
option domain_list 'geosite:cn'
option ip_list 'geoip:cn'
option network 'tcp,udp'
option group 'CN'
config shunt_rules 'Iran'
option remarks 'Iran'
option network 'tcp,udp'
option domain_list 'geosite:ir
#rule-set:remote:https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs'
option ip_list 'geoip:ir
#rule-set:remote:https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs'
option group 'IR'
config shunt_rules 'Russia_Block'
option remarks 'Russia_Block'
option network 'tcp,udp'
option domain_list 'geosite:ru-blocked
#rule-set:remote:https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/refs/heads/release/sing-box/rule-set-geosite/geosite-ru-blocked.srs'
option ip_list 'geoip:ru-blocked
#rule-set:remote:https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/refs/heads/release/sing-box/rule-set-geoip/geoip-ru-blocked.srs'
option group 'RU'
config shunt_rules 'Russia'
option remarks 'Russia'
option network 'tcp,udp'
option ip_list 'geoip:ru
#rule-set:remote:https://raw.githubusercontent.com/runetfreedom/russia-v2ray-rules-dat/refs/heads/release/sing-box/rule-set-geoip/geoip-ru.srs'
option group 'RU'
@@ -419,7 +419,7 @@ run_socks() {
json_add_string "logfile" "${log_file}"
}
[ -n "$no_run" ] && json_add_string "no_run" "1"
json_add_string "flag" "SOCKS_${flag}"
json_add_string "flag" "${flag}"
json_add_string "local_socks_address" "${bind}"
json_add_string "local_socks_port" "${socks_port}"
json_add_string "direct_dns_udp_port" "${DIRECT_DNS_UDP_PORT}"
@@ -441,7 +441,7 @@ run_socks() {
json_add_null "server_port"
}
[ -n "$no_run" ] && json_add_string "no_run" "1"
json_add_string "flag" "SOCKS_${flag}"
json_add_string "flag" "${flag}"
json_add_string "local_socks_address" "${bind}"
json_add_string "local_socks_port" "${socks_port}"
json_add_string "direct_dns_udp_port" "${DIRECT_DNS_UDP_PORT}"
@@ -535,13 +535,13 @@ socks_node_switch() {
local prefix pf filename
# Kill the SS plugin process
for prefix in "" "HTTP_"; do
pf="$TMP_PATH/${prefix}SOCKS_${flag}_plugin.pid"
pf="$TMP_PATH/${prefix}${flag}_plugin.pid"
[ -s "$pf" ] && kill -9 "$(head -n1 "$pf")" >/dev/null 2>&1
done
busybox pgrep -af "$TMP_BIN_PATH" | awk -v P1="${flag}" 'BEGIN{IGNORECASE=1}$0~P1 && !/acl\/|acl_/{print $1}' | xargs kill -9 >/dev/null 2>&1
for prefix in "" "HTTP_" "HTTP2"; do
rm -rf "$TMP_PATH/${prefix}SOCKS_${flag}"*
rm -rf "$TMP_PATH/${prefix}${flag}"*
done
for filename in $(ls ${TMP_SCRIPT_FUNC_PATH}); do
@@ -552,15 +552,15 @@ socks_node_switch() {
local bind="0.0.0.0"
[ "$bind_local" = "1" ] && bind="127.0.0.1"
local port=$(config_n_get $flag port)
local config_file="SOCKS_${flag}.json"
local log_file="SOCKS_${flag}.log"
local config_file="${flag}.json"
local log_file="${flag}.log"
local log=$(config_n_get $flag log 1)
[ "$log" == "0" ] && log_file=""
local http_port=$(config_n_get $flag http_port 0)
local http_config_file="HTTP2SOCKS_${flag}.json"
LOG_FILE="/dev/null"
run_socks flag=$flag node=$new_node bind=$bind socks_port=$port config_file=$config_file http_port=$http_port http_config_file=$http_config_file log_file=$log_file
set_cache_var "socks_${flag}" "$new_node"
set_cache_var "${flag}" "$new_node"
local USE_TABLES=$(get_cache_var "USE_TABLES")
[ -n "$USE_TABLES" ] && source $APP_PATH/${USE_TABLES}.sh filter_direct_node_list
}
@@ -764,14 +764,14 @@ start_socks() {
local bind="0.0.0.0"
[ "$bind_local" = "1" ] && bind="127.0.0.1"
local port=$(config_n_get $id port)
local config_file="SOCKS_${id}.json"
local log_file="SOCKS_${id}.log"
local config_file="${id}.json"
local log_file="${id}.log"
local log=$(config_n_get $id log 1)
[ "$log" == "0" ] && log_file=""
local http_port=$(config_n_get $id http_port 0)
local http_config_file="HTTP2SOCKS_${id}.json"
run_socks flag=$id node=$node bind=$bind socks_port=$port config_file=$config_file http_port=$http_port http_config_file=$http_config_file log_file=$log_file
set_cache_var "socks_${id}" "$node"
set_cache_var "${id}" "$node"
# Auto switch logic
local enable_autoswitch=$(config_n_get $id enable_autoswitch 0)
@@ -869,10 +869,9 @@ start_crontab() {
for item in $(uci show ${CONFIG} | grep "=subscribe_list" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
sub_update_week_mode=$(config_n_get $item update_week_mode)
if [ -n "$sub_update_week_mode" ]; then
cfgid=$(uci show ${CONFIG}.$item | head -n 1 | cut -d '.' -sf 2 | cut -d '=' -sf 1)
remark=$(config_n_get $item remark)
sub_update_time_mode=$(config_n_get $item update_time_mode)
echo "$cfgid" >> $TMP_SUB_PATH/${sub_update_week_mode}_${sub_update_time_mode}
echo "$item" >> $TMP_SUB_PATH/${sub_update_week_mode}_${sub_update_time_mode}
log_i18n 0 "Scheduled tasks: Auto update [%s] subscription." "${remark}"
fi
done
@@ -1010,22 +1009,26 @@ run_ipset_dnsmasq() {
acl_app() {
local items=$(uci show ${CONFIG} | grep "=acl_rule" | cut -d '.' -sf 2 | cut -d '=' -sf 1)
[ -n "$items" ] && {
if [ -z "$items" ]; then
ENABLED_ACLS=0
return
else
local has_enabled
local index=0
local item
local sid
local redir_port dns_port dnsmasq_port socks_port
local ipt_tmp msg msg2
redir_port=11200
socks_port=11600
dns_port=11300
dnsmasq_port=${GLOBAL_DNSMASQ_PORT:-11400}
for item in $items; do
for sid in $items; do
index=$(expr $index + 1)
local enabled sid remarks sources interface tcp_no_redir_ports udp_no_redir_ports node direct_dns_query_strategy remote_dns_protocol remote_dns remote_dns_doh remote_dns_client_ip remote_dns_detour remote_fakedns remote_dns_query_strategy log loglevel log_file
local _ip _mac _iprange _ipset _ip_or_mac source_list config_file
local sid=$(uci -q show "${CONFIG}.${item}" | grep "=acl_rule" | awk -F '=' '{print $1}' | awk -F '.' '{print $2}')
[ "$(config_n_get $sid enabled)" = "1" ] || continue
eval $(uci -q show "${CONFIG}.${item}" | cut -d'.' -sf 3-)
has_enabled=1
eval $(uci -q show "${CONFIG}.${sid}" | cut -d'.' -sf 3-)
log=${log:-0}
loglevel=${loglevel:-warn}
log_file="/dev/null"
@@ -1150,7 +1153,8 @@ acl_app() {
unset _ip _mac _iprange _ipset _ip_or_mac source_list config_file
done
unset redir_port dns_port dnsmasq_port
}
[ -n "${has_enabled}" ] || ENABLED_ACLS=0
fi
}
start() {
@@ -1209,8 +1213,7 @@ start() {
local cfgids item
for item in $(uci show ${CONFIG} | grep "=subscribe_list" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
if [ "$(config_n_get "$item" boot_update 0)" = "1" ]; then
local cfgid=$(uci show ${CONFIG}.$item | head -n 1 | cut -d '.' -sf 2 | cut -d '=' -sf 1)
cfgids="${cfgids:+$cfgids,}$cfgid"
cfgids="${cfgids:+$cfgids,}$item"
fi
done
[ -n "$cfgids" ] && {
@@ -1298,9 +1301,6 @@ get_config() {
[ -n "$NODE" ] && [ "$(config_get_type $NODE)" == "nodes" ] && ENABLED_DEFAULT_ACL=1
}
ENABLED_ACLS=$(config_t_get global acl_enable 0)
[ "$ENABLED_ACLS" == 1 ] && {
[ "$(uci show ${CONFIG} | grep "@acl_rule" | grep "enabled='1'" | wc -l)" == 0 ] && ENABLED_ACLS=0
}
SOCKS_ENABLED=$(config_t_get global socks_enabled 0)
REDIR_PORT=$(echo $(get_new_port 1041 tcp,udp))
TCP_PROXY_WAY=$(config_t_get global_forwarding tcp_proxy_way redirect)
@@ -189,7 +189,9 @@ gen_shunt_list() {
local default_outbound="redirect"
[ "$default_node" = "_direct" ] && default_outbound="direct"
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
local shunt_group=$(config_n_get $node shunt_group)
for shunt_id in $shunt_ids; do
[ "${shunt_group}" != "$(config_n_get ${shunt_id} group)" ] && continue
local shunt_node=$(config_n_get ${node} "${shunt_id}")
[ -n "$shunt_node" ] && {
local ipset_v4="psw2_${node}_${shunt_id}"
@@ -702,15 +704,13 @@ add_firewall_rule() {
fi
$ipt_n -N PSW2
$ipt_n -A PSW2 $(dst $IPSET_DIRECT) -j RETURN
$ipt_n -A PSW2 $(dst $IPSET_VPS) -j RETURN
$ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
[ "$accept_icmp" = "1" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2"
[ -z "${is_tproxy}" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2"
[ "$accept_icmp" = "1" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "$(dst $IPSET_DIRECT !) -p icmp -j PSW2"
[ -z "${is_tproxy}" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "$(dst $IPSET_DIRECT !) -p tcp -j PSW2"
$ipt_n -N PSW2_OUTPUT
$ipt_n -A PSW2_OUTPUT $(dst $IPSET_DIRECT) -j RETURN
$ipt_n -A PSW2_OUTPUT $(dst $IPSET_VPS) -j RETURN
$ipt_n -A PSW2_OUTPUT -m mark --mark 255 -j RETURN
@@ -722,10 +722,6 @@ add_firewall_rule() {
$ipt_n -I PREROUTING -m set --match-set $IPSET_DIRECT src -j PSW2_DNS
fi
$ipt_m -N PSW2_DIVERT
$ipt_m -A PSW2_DIVERT -j MARK --set-mark ${FWMARK}
$ipt_m -A PSW2_DIVERT -j ACCEPT
$ipt_m -N PSW2_RULE
$ipt_m -A PSW2_RULE -j CONNMARK --restore-mark
$ipt_m -A PSW2_RULE -m mark --mark ${FWMARK} -j RETURN
@@ -734,17 +730,16 @@ add_firewall_rule() {
$ipt_m -A PSW2_RULE -j CONNMARK --save-mark
$ipt_m -N PSW2
$ipt_m -A PSW2 $(dst $IPSET_DIRECT) -j RETURN
# 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 $(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
insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2"
# Only TCP, UDP Invalid.
insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT"
insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "$(dst $IPSET_DIRECT !) -j PSW2"
$ipt_m -N PSW2_OUTPUT
$ipt_m -A PSW2_OUTPUT $(dst $IPSET_DIRECT) -j RETURN
$ipt_m -A PSW2_OUTPUT $(dst $IPSET_VPS) -j RETURN
$ipt_m -A PSW2_OUTPUT -m conntrack --ctdir REPLY -j RETURN
$ipt_m -A PSW2_OUTPUT -m mark --mark 255 -j RETURN
@@ -754,12 +749,10 @@ add_firewall_rule() {
[ "$accept_icmpv6" = "1" ] && {
$ip6t_n -N PSW2
$ip6t_n -A PSW2 $(dst $IPSET_DIRECT6) -j RETURN
$ip6t_n -A PSW2 $(dst $IPSET_VPS6) -j RETURN
$ip6t_n -A PREROUTING -p ipv6-icmp -j PSW2
$ip6t_n -A PREROUTING $(dst $IPSET_DIRECT6 !) -p ipv6-icmp -j PSW2
$ip6t_n -N PSW2_OUTPUT
$ip6t_n -A PSW2_OUTPUT $(dst $IPSET_DIRECT6) -j RETURN
$ip6t_n -A PSW2_OUTPUT $(dst $IPSET_VPS6) -j RETURN
$ip6t_n -A PSW2_OUTPUT -m mark --mark 255 -j RETURN
}
@@ -772,10 +765,6 @@ add_firewall_rule() {
$ip6t_n -I PREROUTING -m set --match-set $IPSET_DIRECT6 src -j PSW2_DNS
fi
$ip6t_m -N PSW2_DIVERT
$ip6t_m -A PSW2_DIVERT -j MARK --set-mark ${FWMARK}
$ip6t_m -A PSW2_DIVERT -j ACCEPT
$ip6t_m -N PSW2_RULE
$ip6t_m -A PSW2_RULE -j CONNMARK --restore-mark
$ip6t_m -A PSW2_RULE -m mark --mark ${FWMARK} -j RETURN
@@ -784,18 +773,17 @@ add_firewall_rule() {
$ip6t_m -A PSW2_RULE -j CONNMARK --save-mark
$ip6t_m -N PSW2
$ip6t_m -A PSW2 $(dst $IPSET_DIRECT6) -j RETURN
# 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 $(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
insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2"
# Only TCP, UDP Invalid.
insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT"
insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "$(dst $IPSET_DIRECT6 !) -j PSW2"
$ip6t_m -N PSW2_OUTPUT
$ip6t_m -A PSW2_OUTPUT -m mark --mark 255 -j RETURN
$ip6t_m -A PSW2_OUTPUT $(dst $IPSET_DIRECT6) -j RETURN
$ip6t_m -A PSW2_OUTPUT $(dst $IPSET_VPS6) -j RETURN
$ip6t_m -A PSW2_OUTPUT -m conntrack --ctdir REPLY -j RETURN
@@ -860,14 +848,14 @@ add_firewall_rule() {
# Loading local router proxy TCP
if [ -n "$NODE" ] && [ "$TCP_LOCALHOST_PROXY" = "1" ]; then
[ "$accept_icmp" = "1" ] && {
$ipt_n -A OUTPUT -p icmp -j PSW2_OUTPUT
$ipt_n -A OUTPUT $(dst $IPSET_DIRECT !) -p icmp -j PSW2_OUTPUT
$ipt_n -A PSW2_OUTPUT -p icmp -d $FAKE_IP $(REDIRECT)
add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_n -A PSW2_OUTPUT -p icmp" "$(REDIRECT)"
$ipt_n -A PSW2_OUTPUT -p icmp $(REDIRECT)
}
[ "$accept_icmpv6" = "1" ] && {
$ip6t_n -A OUTPUT -p ipv6-icmp -j PSW2_OUTPUT
$ip6t_n -A OUTPUT $(dst $IPSET_DIRECT6 !) -p ipv6-icmp -j PSW2_OUTPUT
$ip6t_n -A PSW2_OUTPUT -p ipv6-icmp -d $FAKE_IP_6 $(REDIRECT)
add_shunt_t_rule "${SHUNT_LIST6}" "$ip6t_n -A PSW2_OUTPUT -p ipv6-icmp" "$(REDIRECT)"
$ip6t_n -A PSW2_OUTPUT -p ipv6-icmp $(REDIRECT)
@@ -882,11 +870,11 @@ add_firewall_rule() {
$ipt_tmp -A PSW2_OUTPUT -p tcp -d $FAKE_IP ${ipt_j}
add_shunt_t_rule "${SHUNT_LIST4}" "$ipt_tmp -A PSW2_OUTPUT -p tcp" "${ipt_j}" $TCP_REDIR_PORTS
add_port_rules "$ipt_tmp -A PSW2_OUTPUT -p tcp" $TCP_REDIR_PORTS "${ipt_j}"
[ -z "${is_tproxy}" ] && $ipt_n -A OUTPUT -p tcp -j PSW2_OUTPUT
[ -z "${is_tproxy}" ] && $ipt_n -A OUTPUT $(dst $IPSET_DIRECT !) -p tcp -j PSW2_OUTPUT
[ -n "${is_tproxy}" ] && {
$ipt_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo $(REDIRECT $REDIR_PORT TPROXY)
$ipt_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo -j RETURN
insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p tcp -j PSW2_OUTPUT"
insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) $(dst $IPSET_DIRECT !) -p tcp -j PSW2_OUTPUT"
}
[ "$PROXY_IPV6" == "1" ] && {
@@ -895,7 +883,7 @@ add_firewall_rule() {
add_port_rules "$ip6t_m -A PSW2_OUTPUT -p tcp" $TCP_REDIR_PORTS "-j PSW2_RULE"
$ip6t_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo $(REDIRECT $REDIR_PORT TPROXY)
$ip6t_m -A PSW2 $(comment "${comment_l}") -p tcp -i lo -j RETURN
insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p tcp -j PSW2_OUTPUT"
insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) $(dst $IPSET_DIRECT6 !) -p tcp -j PSW2_OUTPUT"
}
[ -d "${TMP_IFACE_PATH}" ] && {
@@ -913,7 +901,7 @@ add_firewall_rule() {
add_port_rules "$ipt_m -A PSW2_OUTPUT -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE"
$ipt_m -A PSW2 $(comment "${comment_l}") -p udp -i lo $(REDIRECT $REDIR_PORT TPROXY)
$ipt_m -A PSW2 $(comment "${comment_l}") -p udp -i lo -j RETURN
insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p udp -j PSW2_OUTPUT"
insert_rule_before "$ipt_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) $(dst $IPSET_DIRECT !) -p udp -j PSW2_OUTPUT"
[ "$PROXY_IPV6" == "1" ] && {
$ip6t_m -A PSW2_OUTPUT -p udp -d $FAKE_IP_6 -j PSW2_RULE
@@ -921,7 +909,7 @@ add_firewall_rule() {
add_port_rules "$ip6t_m -A PSW2_OUTPUT -p udp" $UDP_REDIR_PORTS "-j PSW2_RULE"
$ip6t_m -A PSW2 $(comment "${comment_l}") -p udp -i lo $(REDIRECT $REDIR_PORT TPROXY)
$ip6t_m -A PSW2 $(comment "${comment_l}") -p udp -i lo -j RETURN
insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -p udp -j PSW2_OUTPUT"
insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) $(dst $IPSET_DIRECT6 !) -p udp -j PSW2_OUTPUT"
}
[ -d "${TMP_IFACE_PATH}" ] && {
@@ -938,8 +926,10 @@ add_firewall_rule() {
$ip6t_m -I OUTPUT $(comment "mangle-OUTPUT-PSW2") -o lo -j RETURN
insert_rule_before "$ip6t_m" "OUTPUT" "mwan3" "$(comment mangle-OUTPUT-PSW2) -m mark --mark ${FWMARK} -j RETURN"
$ipt_m -A PSW2 -p udp --dport 53 -j RETURN
$ip6t_m -A PSW2 -p udp --dport 53 -j RETURN
$ipt_m -A PSW2 -p tcp --dport 53 -j ACCEPT
$ipt_m -A PSW2 -p udp --dport 53 -j ACCEPT
$ip6t_m -A PSW2 -p tcp --dport 53 -j ACCEPT
$ip6t_m -A PSW2 -p udp --dport 53 -j ACCEPT
}
load_acl
@@ -970,7 +960,7 @@ del_firewall_rule() {
$ipt -D $chain $index 2>/dev/null
done
done
for chain in "PSW2" "PSW2_OUTPUT" "PSW2_DIVERT" "PSW2_DNS" "PSW2_RULE"; do
for chain in "PSW2" "PSW2_OUTPUT" "PSW2_DNS" "PSW2_RULE"; do
$ipt -F $chain 2>/dev/null
$ipt -X $chain 2>/dev/null
done
@@ -1004,7 +994,7 @@ gen_include() {
[ -z "${_ipt}" ] && return
echo "*$2"
${_ipt}-save -t $2 | grep "PSW2" | grep -v "\-j PSW2$" | grep -v "socket \-j PSW2_DIVERT$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/"
${_ipt}-save -t $2 | grep "PSW2" | grep -v "\-j PSW2$" | sed -e "s/^-A \(OUTPUT\|PREROUTING\)/-I \1 1/"
echo 'COMMIT'
}
local __ipt=""
@@ -1017,11 +1007,10 @@ gen_include() {
$(extract_rules 4 mangle)
EOT
[ "$accept_icmp" = "1" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2")
[ -z "${is_tproxy}" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2")
[ "$accept_icmp" = "1" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "$(dst $IPSET_DIRECT !) -p icmp -j PSW2")
[ -z "${is_tproxy}" ] && \$(${MY_PATH} insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "$(dst $IPSET_DIRECT !) -p tcp -j PSW2")
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2")
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT")
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "$(dst $IPSET_DIRECT !) -j PSW2")
EOF
)
}
@@ -1035,10 +1024,9 @@ gen_include() {
$(extract_rules 6 mangle)
EOT
[ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING -p ipv6-icmp -j PSW2
[ "$accept_icmpv6" = "1" ] && $ip6t_n -A PREROUTING $(dst $IPSET_DIRECT6 !) -p ipv6-icmp -j PSW2
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2")
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT")
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "$(dst $IPSET_DIRECT6 !) -j PSW2")
EOF
)
}
@@ -231,7 +231,9 @@ gen_shunt_list() {
local default_outbound="redirect"
[ "$default_node" = "_direct" ] && default_outbound="direct"
local shunt_ids=$(uci show $CONFIG | grep "=shunt_rules" | awk -F '.' '{print $2}' | awk -F '=' '{print $1}')
local shunt_group=$(config_n_get $node shunt_group)
for shunt_id in $shunt_ids; do
[ "${shunt_group}" != "$(config_n_get ${shunt_id} group)" ] && continue
local shunt_node=$(config_n_get ${node} "${shunt_id}")
[ -n "$shunt_node" ] && {
local nftset_v4="psw2_${node}_${shunt_id}"
@@ -700,13 +702,13 @@ add_firewall_rule() {
for ip in $(cat /usr/share/passwall2/direct_ip | tr -s "\r\n" "\n" | grep -v "^#" | sed -e "/^$/d"); do
if [[ "$ip" == *::* ]]; then
echo "$ip" | insert_nftset $NFTSET_DIRECT6 0
echo "$ip" | insert_nftset $NFTSET_DIRECT6 "-1"
elif [[ "$ip" == "geoip:"* ]]; then
local _geoip_code=$(echo $ip | awk -F ':' '{print $2}')
get_geoip $_geoip_code ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_DIRECT "0"
get_geoip $_geoip_code ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_DIRECT6 "0"
get_geoip $_geoip_code ipv4 | grep -E "(\.((2(5[0-5]|[0-4][0-9]))|[0-1]?[0-9]{1,2})){3}" | insert_nftset $NFTSET_DIRECT "-1"
get_geoip $_geoip_code ipv6 | grep -E "([A-Fa-f0-9]{1,4}::?){1,7}[A-Fa-f0-9]{1,4}" | insert_nftset $NFTSET_DIRECT6 "-1"
else
echo "$ip" | insert_nftset $NFTSET_DIRECT 0
echo "$ip" | insert_nftset $NFTSET_DIRECT "-1"
fi
done
@@ -726,14 +728,14 @@ add_firewall_rule() {
update_wan_sets
[ -n "$ISP_DNS" ] && {
echo "$ISP_DNS" | insert_nftset $NFTSET_DIRECT 0
echo "$ISP_DNS" | insert_nftset $NFTSET_DIRECT "-1"
for ispip in $ISP_DNS; do
log_i18n 1 "$(i18n "Add ISP %s DNS to the whitelist: %s" "IPv4" "${ispip}")"
done
}
[ -n "$ISP_DNS6" ] && {
echo $ISP_DNS6 | insert_nftset $NFTSET_DIRECT6 0
echo $ISP_DNS6 | insert_nftset $NFTSET_DIRECT6 "-1"
for ispip6 in $ISP_DNS6; do
log_i18n 1 "$(i18n "Add ISP %s DNS to the whitelist: %s" "IPv6" "${ispip6}")"
done
@@ -762,11 +764,6 @@ add_firewall_rule() {
nft_output_chain="PSW2_OUTPUT_MANGLE"
fi
nft "add chain $NFTABLE_NAME PSW2_DIVERT"
nft "flush chain $NFTABLE_NAME PSW2_DIVERT"
# Only TCP, UDP Invalid.
nft "add rule $NFTABLE_NAME PSW2_DIVERT meta l4proto tcp socket transparent 1 mark set ${FWMARK} counter accept"
nft "add chain $NFTABLE_NAME PSW2_DNS"
nft "flush chain $NFTABLE_NAME PSW2_DNS"
if [ $(config_t_get global dns_redirect "1") = "0" ]; then
@@ -790,33 +787,29 @@ add_firewall_rule() {
#ipv4 tproxy mode and udp
nft "add chain $NFTABLE_NAME PSW2_MANGLE"
nft "flush chain $NFTABLE_NAME PSW2_MANGLE"
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_DIRECT counter return"
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_VPS counter return"
nft "add rule $NFTABLE_NAME PSW2_MANGLE ct direction reply counter return"
nft "add chain $NFTABLE_NAME PSW2_OUTPUT_MANGLE"
nft "flush chain $NFTABLE_NAME PSW2_OUTPUT_MANGLE"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip daddr @$NFTSET_DIRECT counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip daddr @$NFTSET_VPS counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ct direction reply counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE meta mark 255 counter return"
# jump chains
nft "add rule $NFTABLE_NAME mangle_prerouting counter jump PSW2_DIVERT"
nft "add rule $NFTABLE_NAME mangle_prerouting ip protocol udp counter jump PSW2_MANGLE"
[ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME mangle_prerouting ip protocol tcp counter jump PSW2_MANGLE"
# Only TCP, UDP Invalid.
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto ipv4 meta l4proto tcp socket transparent 1 mark set ${FWMARK} counter accept"
nft "add rule $NFTABLE_NAME mangle_prerouting ip daddr != @$NFTSET_DIRECT ip protocol udp counter jump PSW2_MANGLE"
[ -n "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME mangle_prerouting ip daddr != @$NFTSET_DIRECT ip protocol tcp counter jump PSW2_MANGLE"
#ipv4 tcp redirect mode
[ -z "${is_tproxy}" ] && {
nft "add chain $NFTABLE_NAME PSW2_NAT"
nft "flush chain $NFTABLE_NAME PSW2_NAT"
nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_DIRECT counter return"
nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_VPS counter return"
nft "add rule $NFTABLE_NAME dstnat ip protocol tcp counter jump PSW2_NAT"
nft "add rule $NFTABLE_NAME dstnat ip daddr != @$NFTSET_DIRECT ip protocol tcp counter jump PSW2_NAT"
nft "add chain $NFTABLE_NAME PSW2_OUTPUT_NAT"
nft "flush chain $NFTABLE_NAME PSW2_OUTPUT_NAT"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_NAT ip daddr @$NFTSET_DIRECT counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_NAT ip daddr @$NFTSET_VPS counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_NAT meta mark 255 counter return"
}
@@ -825,16 +818,16 @@ add_firewall_rule() {
if [ "$accept_icmp" = "1" ]; then
nft "add chain $NFTABLE_NAME PSW2_ICMP_REDIRECT"
nft "flush chain $NFTABLE_NAME PSW2_ICMP_REDIRECT"
nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip daddr @$NFTSET_DIRECT counter return"
nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip daddr @$NFTSET_VPS counter return"
[ "$accept_icmpv6" = "1" ] && {
nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip6 daddr @$NFTSET_DIRECT6 counter return"
nft "add rule $NFTABLE_NAME PSW2_ICMP_REDIRECT ip6 daddr @$NFTSET_VPS6 counter return"
}
nft "add rule $NFTABLE_NAME dstnat meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT"
nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT"
nft "add rule $NFTABLE_NAME dstnat ip daddr != @$NFTSET_DIRECT meta l4proto icmp counter jump PSW2_ICMP_REDIRECT"
nft "add rule $NFTABLE_NAME dstnat ip6 daddr != @$NFTSET_DIRECT6 meta l4proto icmpv6 counter jump PSW2_ICMP_REDIRECT"
nft "add rule $NFTABLE_NAME nat_output ip daddr != @$NFTSET_DIRECT meta l4proto icmp counter jump PSW2_ICMP_REDIRECT"
nft "add rule $NFTABLE_NAME nat_output ip6 daddr != @$NFTSET_DIRECT6 meta l4proto icmpv6 counter jump PSW2_ICMP_REDIRECT"
fi
#ipv4 wan_ip
@@ -847,13 +840,11 @@ add_firewall_rule() {
#ipv6 tproxy mode and udp
nft "add chain $NFTABLE_NAME PSW2_MANGLE_V6"
nft "flush chain $NFTABLE_NAME PSW2_MANGLE_V6"
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_DIRECT6 counter return"
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_VPS6 counter return"
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ct direction reply counter return"
nft "add chain $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6"
nft "flush chain $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 ip6 daddr @$NFTSET_DIRECT6 counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 ip6 daddr @$NFTSET_VPS6 counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 ct direction reply counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta mark 255 counter return"
@@ -874,8 +865,10 @@ add_firewall_rule() {
# jump chains
[ "$PROXY_IPV6" == "1" ] && {
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto {ipv6} counter jump PSW2_MANGLE_V6"
nft "add rule $NFTABLE_NAME mangle_output meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\""
# Only TCP, UDP Invalid.
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto ipv6 meta l4proto tcp socket transparent 1 mark set ${FWMARK} counter accept"
nft "add rule $NFTABLE_NAME mangle_prerouting ip6 daddr != @$NFTSET_DIRECT6 meta nfproto {ipv6} counter jump PSW2_MANGLE_V6"
nft "add rule $NFTABLE_NAME mangle_output ip6 daddr != @$NFTSET_DIRECT6 meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
@@ -948,11 +941,11 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp ip daddr $FAKE_IP ${nft_j}"
add_shunt_t_rule "${SHUNT_LIST4}" "nft add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ip daddr" "${nft_j}"
nft "add rule $NFTABLE_NAME $nft_chain ip protocol tcp $(factor $TCP_REDIR_PORTS "tcp dport") ${nft_j}"
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME nat_output ip protocol tcp counter jump PSW2_OUTPUT_NAT"
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME nat_output ip daddr != @$NFTSET_DIRECT ip protocol tcp counter jump PSW2_OUTPUT_NAT"
[ -n "${is_tproxy}" ] && {
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp iif lo $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp iif lo counter return comment \"${comment_l}\""
nft "add rule $NFTABLE_NAME mangle_output ip protocol tcp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\""
nft "add rule $NFTABLE_NAME mangle_output ip daddr != @$NFTSET_DIRECT ip protocol tcp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\""
}
[ "$PROXY_IPV6" == "1" ] && {
@@ -978,7 +971,7 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_REDIR_PORTS "udp dport") counter jump PSW2_RULE"
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp iif lo $(REDIRECT $REDIR_PORT TPROXY4) comment \"${comment_l}\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp iif lo counter return comment \"${comment_l}\""
nft "add rule $NFTABLE_NAME mangle_output ip protocol udp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\""
nft "add rule $NFTABLE_NAME mangle_output ip daddr != @$NFTSET_DIRECT ip protocol udp counter jump PSW2_OUTPUT_MANGLE comment \"PSW2_OUTPUT_MANGLE\""
[ "$PROXY_IPV6" == "1" ] && {
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr $FAKE_IP_6 jump PSW2_RULE"
@@ -996,11 +989,13 @@ add_firewall_rule() {
}
fi
nft "add rule $NFTABLE_NAME mangle_output oif lo counter return comment \"PSW2_OUTPUT_MANGLE\""
nft "add rule $NFTABLE_NAME mangle_output meta mark ${FWMARK} counter return comment \"PSW2_OUTPUT_MANGLE\""
nft "add rule $NFTABLE_NAME mangle_output ip daddr != @$NFTSET_DIRECT oif lo counter return comment \"PSW2_OUTPUT_MANGLE\""
nft "add rule $NFTABLE_NAME mangle_output ip daddr != @$NFTSET_DIRECT meta mark ${FWMARK} counter return comment \"PSW2_OUTPUT_MANGLE\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp udp dport 53 counter return"
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp udp dport 53 counter return"
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol tcp tcp dport 53 counter accept"
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip protocol udp udp dport 53 counter accept"
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto tcp tcp dport 53 counter accept"
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 meta l4proto udp udp dport 53 counter accept"
}
load_acl
@@ -85,7 +85,7 @@ local function fetch_geofile(geo_name, geo_type, url)
local tmp_path = "/tmp/" .. geo_name
local asset_path = asset_location .. geo_name
local down_filename = url:match("^.*/([^/?#]+)")
local sha_url = url:gsub(down_filename, down_filename .. ".sha256sum")
local sha_url = url:gsub((down_filename:gsub("(%W)", "%%%1")), down_filename .. ".sha256sum")
local sha_path = tmp_path .. ".sha256sum"
local function verify_sha256(sha_file)
@@ -77,8 +77,8 @@ test_auto_switch() {
local b_nodes=$1
local now_node=$2
[ -z "$now_node" ] && {
if [ -n "$(get_cache_var "socks_${id}")" ]; then
now_node=$(get_cache_var "socks_${id}")
if [ -n "$(get_cache_var "${id}")" ]; then
now_node=$(get_cache_var "${id}")
else
#log_i18n 0 "Socks switch detection: Unknown error."
return 1
@@ -126,7 +126,11 @@ test_auto_switch() {
# If the current node is not found, or if the current node is the last node, then take the first node.
[ -z "$new_node" ] && new_node="$first_node"
local msg2="$(i18n "next backup node")"
[ "$now_node" = "$main_node" ] && msg2="$(i18n "backup node")"
if [ "$new_node" = "$main_node" ]; then
msg2="$(i18n "main node")"
else
[ "$now_node" = "$main_node" ] && msg2="$(i18n "backup node")"
fi
msg="$(i18n "switch to %s test detect!" "${msg2}")"
else
# When there is only one backup node, poll with the primary node.
@@ -138,11 +142,6 @@ test_auto_switch() {
log_i18n 0 "Socks switch detection: %s 【%s:[%s]】 abnormal, %s" "${id}" "$(config_n_get $now_node type)" "$(config_n_get $now_node remarks)" "${msg}"
test_node ${new_node}
if [ $? -eq 0 ]; then
# [ "$restore_switch" = "0" ] && {
# uci set $CONFIG.${id}.node=$new_node
# [ -z "$(echo $b_nodes | grep $main_node)" ] && uci add_list $CONFIG.${id}.autoswitch_backup_node=$main_node
# uci commit $CONFIG
# }
check_process
log_i18n 0 "Socks switch detection: %s 【%s:[%s]】 normal, switch to this node!" "${id}" "$(config_n_get $new_node type)" "$(config_n_get $new_node remarks)"
$APP_FILE socks_node_switch flag=${id} new_node=${new_node}
@@ -171,6 +170,10 @@ start() {
backup_node_num=$(printf "%s\n" "$backup_node" | wc -w)
if [ "$backup_node_num" -eq 1 ]; then
[ "$main_node" = "$backup_node" ] && return
elif [ "$backup_node_num" -gt 1 ]; then
[ "$restore_switch" != "1" ] && {
[ -z "$(echo $backup_node | grep -F "$main_node")" ] && backup_node="${backup_node} ${main_node}"
}
fi
else
return
@@ -168,10 +168,10 @@ do
local flag = i18n.translatef("Socks node list [%s]", i) .. " " .. i18n.translatef("Backup node list")
local currentNodes = {}
local newNodes = {}
for k, node_id in ipairs(t.autoswitch_backup_node) do
if node_id then
local currentNode = uci:get_all(appname, node_id) or nil
if currentNode then
for k, asb_node in ipairs(t.autoswitch_backup_node) do
if asb_node then
local currentNode = uci:get_all(appname, asb_node) or {}
if currentNode[".type"] == "nodes" then
currentNodes[#currentNodes + 1] = {
log = true,
remarks = flag .. "[" .. k .. "]",
@@ -276,36 +276,39 @@ do
for k, e in pairs(rules) do
local _node_id = node[e[".name"]] or nil
if _node_id and not _node_id:find("Socks_") then
CONFIG[#CONFIG + 1] = {
log = false,
currentNode = _node_id and uci:get_all(appname, _node_id) or nil,
remarks = i18n.translatef("Shunt [%s] node", e.remarks),
set = function(o, server)
if not server then server = "" end
uci:set(appname, node_id, e[".name"], server)
o.newNodeId = server
end
}
if _node_id then
local section = uci:get_all(appname, node_id) or {}
if section[".type"] == "nodes" then
CONFIG[#CONFIG + 1] = {
log = false,
currentNode = section,
remarks = i18n.translatef("Shunt [%s] node", e.remarks),
set = function(o, server)
if not server then server = "" end
uci:set(appname, node_id, e[".name"], server)
o.newNodeId = server
end
}
end
end
end
elseif node.protocol and node.protocol == '_balancing' then
local flag = i18n.translatef("Xray Load Balancing node [%s] list", node_id)
local currentNodes = {}
local newNodes = {}
if node.balancing_node then
for k, node in pairs(node.balancing_node) do
for k, b_node_id in pairs(node.balancing_node) do
currentNodes[#currentNodes + 1] = {
log = true,
node = node,
node = b_node_id,
currentNode = (function()
if node and node:find("Socks_") then
return { Socks = node }
local section = uci:get_all(appname, b_node_id) or {}
if section[".type"] == "socks" then
return { Socks = b_node_id }
end
return node and uci:get_all(appname, node) or nil
return section
end)(),
remarks = node,
remarks = b_node_id,
set = function(o, server)
if o and server and server ~= "nil" then
table.insert(o.newNodes, server)
@@ -328,37 +331,41 @@ do
-- Backup Node
local currentNode = uci:get_all(appname, node_id) or nil
if currentNode and currentNode.fallback_node and not currentNode.fallback_node:find("Socks_") then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Xray Load Balancing node [%s] backup node", node_id),
currentNode = uci:get_all(appname, currentNode.fallback_node) or nil,
set = function(o, server)
uci:set(appname, node_id, "fallback_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "fallback_node")
end
}
if currentNode and currentNode.fallback_node then
local section = uci:get_all(appname, currentNode.fallback_node) or {}
if section[".type"] == "nodes" then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Xray Load Balancing node [%s] backup node", node_id),
currentNode = section,
set = function(o, server)
uci:set(appname, node_id, "fallback_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "fallback_node")
end
}
end
end
elseif node.protocol and node.protocol == '_urltest' then
local flag = i18n.translatef("Sing-Box URLTest node [%s] list", node_id)
local currentNodes = {}
local newNodes = {}
if node.urltest_node then
for k, node in pairs(node.urltest_node) do
for k, u_node_id in pairs(node.urltest_node) do
currentNodes[#currentNodes + 1] = {
log = true,
node = node,
node = u_node_id,
currentNode = (function()
if node and node:find("Socks_") then
return { Socks = node }
local section = uci:get_all(appname, u_node_id) or {}
if section[".type"] == "socks" then
return { Socks = u_node_id }
end
return node and uci:get_all(appname, node) or nil
return section
end)(),
remarks = node,
remarks = u_node_id,
set = function(o, server)
if o and server and server ~= "nil" then
table.insert(o.newNodes, server)
@@ -381,37 +388,43 @@ do
else
-- Preproxy Node
local currentNode = uci:get_all(appname, node_id) or nil
if currentNode and currentNode.preproxy_node and not currentNode.preproxy_node:find("Socks_") then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Node [%s] preproxy node", node_id),
currentNode = uci:get_all(appname, currentNode.preproxy_node) or nil,
set = function(o, server)
uci:set(appname, node_id, "preproxy_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "preproxy_node")
end
}
if currentNode and currentNode.preproxy_node then
local section = uci:get_all(appname, currentNode.preproxy_node) or {}
if section[".type"] == "nodes" then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Node [%s] preproxy node", node_id),
currentNode = uci:get_all(appname, currentNode.preproxy_node) or nil,
set = function(o, server)
uci:set(appname, node_id, "preproxy_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "preproxy_node")
end
}
end
end
-- Landing node
local currentNode = uci:get_all(appname, node_id) or nil
if currentNode and currentNode.to_node and not currentNode.to_node:find("Socks_") then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Node [%s] landing node", node_id),
currentNode = uci:get_all(appname, currentNode.to_node) or nil,
set = function(o, server)
uci:set(appname, node_id, "to_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "to_node")
end
}
if currentNode and currentNode.to_node then
local section = uci:get_all(appname, currentNode.to_node) or {}
if section[".type"] == "nodes" then
CONFIG[#CONFIG + 1] = {
log = true,
id = node_id,
remarks = i18n.translatef("Node [%s] landing node", node_id),
currentNode = uci:get_all(appname, currentNode.to_node) or nil,
set = function(o, server)
uci:set(appname, node_id, "to_node", server)
o.newNodeId = server
end,
delete = function(o)
uci:delete(appname, node_id, "to_node")
end
}
end
end
end
end)
@@ -768,6 +781,9 @@ local function parseClashNode(node, add_mode, group, sub_cfg)
result.reality_publicKey = node["reality-opts"]["public-key"]
result.reality_shortId = node["reality-opts"]["short-id"]
end
if node["disable-reuse"] then
result.anytls_disable_reuse = "1"
end
end
if not result.remarks or result.remarks == "" then
if result.address and result.port then
@@ -1865,7 +1881,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
end
if params.security == "tls" or params.security == "reality" then
result.tls = "1"
result.tls_serverName = params.sni
result.tls_serverName = params.sni or params.peer
result.alpn = params.alpn
if params.fp and params.fp ~= "" then
result.utls = "1"
@@ -40,10 +40,9 @@ do
for item in $(uci show ${CONFIG} | grep "=subscribe_list" | cut -d '.' -sf 2 | cut -d '=' -sf 1); do
sub_update_week_mode=$(config_n_get $item update_week_mode)
if [ -n "$sub_update_week_mode" ]; then
cfgid=$(uci show ${CONFIG}.$item | head -n 1 | cut -d '.' -sf 2 | cut -d '=' -sf 1)
remark=$(config_n_get $item remark)
sub_update_interval_mode=$(config_n_get $item update_interval_mode)
echo "$cfgid" >> $TMP_SUB_PATH/${sub_update_week_mode}_${sub_update_interval_mode}
echo "$item" >> $TMP_SUB_PATH/${sub_update_week_mode}_${sub_update_interval_mode}
fi
done
@@ -0,0 +1,47 @@
/**
* Admin custom-background mode (scheme D) — active only when header.ut saw
* struct_main_bg and stamped <body data-bg>. Without the attribute none of
* these selectors match and the page paints exactly as it does today.
*
* Frost only the two small fixed chrome surfaces (the budget Apple's own
* iOS chrome runs on); cards stay blur-free alpha blends. Every color
* derives from --bg/--surface via color-mix, so custom palettes and dark
* mode follow automatically. The three tunables default in var() fallbacks,
* never on body — the config app's UCI overrides land on :root and a
* body-level declaration would mask them.
*/
body[data-bg] {
& .page-bg {
--page-bg: var(--main-bg);
--page-bg-lqip: var(--main-bg-lqip);
}
/* Two jobs on one element. relative: the fixed .page-bg paints above
non-positioned flow content (positioned elements come after in-flow
blocks in the painting order) — the login page has always dodged this
via .login-screen's relative; a negative z-index on the layer is NOT an
option, body's own opaque background would cover the image. canvas: a
--bg-derived translucent wash under ALL content is what makes ARBITRARY
plugin DOM readable — bare headings, descriptions and hand-rolled
widgets sit on this floor instead of the raw photo, with zero
per-plugin knowledge. 55% is a deliberate internal constant, not a
fourth tunable. */
& #maincontent {
@apply relative [background:color-mix(in_srgb,var(--bg)_55%,transparent)];
}
& header,
& .sidebar-panel-inner {
@apply [background:color-mix(in_srgb,var(--bg)_var(--main-bg-alpha,67%),transparent)] [backdrop-filter:blur(var(--main-bg-blur,20px))_saturate(150%)];
}
/* Scrim washes toward the theme bg to keep contrast; z-1 lifts it above
the ::after full-image layer (siblings otherwise paint after ::before). */
& .page-bg::before {
@apply pointer-events-none absolute inset-0 z-1 content-[''] [background:color-mix(in_srgb,var(--bg)_var(--main-bg-scrim,20%),transparent)];
}
& #maincontent .cbi-section {
@apply [background:color-mix(in_srgb,var(--surface)_var(--main-bg-alpha,67%),transparent)];
}
}
Loaded 100 of 117 files, more files were not shown because too many files have changed in this diff. Show more