mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 22:15:13 +08:00
🎨 Sync 2026-08-09 00:31:16
This commit is contained in:
1 parent
5d62494863
commit
ee62266d6b
117 files changed
+5152
-2651
No files matched your search
+1
-1
@@ -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
@@ -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
@@ -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))
|
||||
|
||||
@@ -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
@@ -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)?
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 路径。"
|
||||
@@ -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" ]
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 + '§ion=' + 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 + '§ion=' + 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 => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[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%>");
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 + '§ion=' + 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 + '§ion=' + 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 => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[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%>");
|
||||
|
||||
@@ -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 "فهرست مجموعههای رمز پشتیبانیشده را که با موارد زیر از هم جدا شدهاند، پیکربندی میکند:"
|
||||
@@ -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 "Задает список поддерживаемых наборов шифров, разделенных двоеточием (:)."
|
||||
@@ -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 "配置支持的密码套件列表,以冒号 (:) 分隔。"
|
||||
@@ -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
Reference in new issue
Block a user