mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-26 18:52:51 +08:00
🌈 Sync 2026-09-24 15:19:50
This commit is contained in:
1 parent
765ac2b267
commit
87101cc7f2
126 files changed
+4403
-2879
No files matched your search
+10
-6
@@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64
|
||||
AGENTFLOW_ARCH_aarch64:=arm64
|
||||
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
|
||||
|
||||
AGENTFLOW_HASH_x86_64:=9174fec77dc835640f94ea8bef84b580b84f947823f4dd63fc495c4c58a68297
|
||||
AGENTFLOW_HASH_aarch64:=256adf7bcb836259dacbf583c52af04f0134587328689a49811c135260fa5aed
|
||||
AGENTFLOW_HASH_x86_64:=3bdd9b3af7a95c78d6fb6589117931e2be3f2800056fc7836aa95abe48d0a33d
|
||||
AGENTFLOW_HASH_aarch64:=8962a1ec6f57ee394ddfdf61abd72191f5378644c5ab1ce85040b132b757cc43
|
||||
|
||||
PKG_NAME:=agentflow
|
||||
PKG_VERSION:=0.3.3
|
||||
PKG_RELEASE:=13
|
||||
PKG_VERSION:=0.3.4
|
||||
PKG_RELEASE:=14
|
||||
|
||||
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
|
||||
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
|
||||
@@ -40,7 +40,7 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=AgentFlow
|
||||
DEPENDS:=@(x86_64||aarch64) +git +git-http +mise
|
||||
DEPENDS:=@(x86_64||aarch64) +git +git-http +mise +linkease-app-entry
|
||||
URL:=https://agentflow.geili.ai/
|
||||
endef
|
||||
|
||||
@@ -58,6 +58,9 @@ if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/30-agentflow.json 2>/dev/null)" = "/usr/share/agentflow/agentflow-plugin.json" ]; then
|
||||
rm -f /usr/share/linkeasefull/desktop-apps.d/30-agentflow.json
|
||||
fi
|
||||
if [ "$$(readlink /usr/share/linkease/apps.d/30-agentflow.json 2>/dev/null)" = "/usr/share/agentflow/agentflow-plugin.json" ]; then
|
||||
rm -f /usr/share/linkease/apps.d/30-agentflow.json
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
@@ -74,7 +77,7 @@ define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config $(1)/usr/share/agentflow/www $(1)/usr/share/linkeasefull/desktop-apps.d
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config $(1)/usr/share/agentflow/www $(1)/usr/share/linkease/apps.d $(1)/usr/share/linkeasefull/desktop-apps.d
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/agentflow $(1)/usr/sbin/agentflow
|
||||
$(INSTALL_BIN) ./files/agentflow.init $(1)/etc/init.d/agentflow
|
||||
$(INSTALL_BIN) ./files/agentflow.uci-default $(1)/etc/uci-defaults/09-agentflow
|
||||
@@ -82,6 +85,7 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DATA) ./files/agentflow-plugin.json $(1)/usr/share/agentflow/agentflow-plugin.json
|
||||
$(INSTALL_DATA) ./files/www/logo.svg $(1)/usr/share/agentflow/www/logo.svg
|
||||
ln -sf /usr/share/agentflow/agentflow-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/30-agentflow.json
|
||||
ln -sf /usr/share/agentflow/agentflow-plugin.json $(1)/usr/share/linkease/apps.d/30-agentflow.json
|
||||
endef
|
||||
|
||||
$(eval $(call Download,agentflow))
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"standalone": {
|
||||
"entry": "index.html",
|
||||
"basePath": "/apps/agentflow/",
|
||||
"url": "/cgi-bin/luci/admin/services/agentflow/open",
|
||||
"url": "/apps/agentflow/",
|
||||
"externalOpen": {
|
||||
"enabled": true,
|
||||
"label": "Open AgentFlow"
|
||||
|
||||
@@ -19,7 +19,7 @@ class DesktopManifestContractTest(unittest.TestCase):
|
||||
self.assertEqual(self.manifest["standalone"]["basePath"], "/apps/agentflow/")
|
||||
self.assertEqual(
|
||||
self.manifest["standalone"]["url"],
|
||||
"/cgi-bin/luci/admin/services/agentflow/open",
|
||||
"/apps/agentflow/",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.manifest["standalone"]["externalOpen"],
|
||||
|
||||
+7
-3
@@ -11,7 +11,7 @@ PKG_ARCH_BAIDUDRIVE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=baidudrive
|
||||
PKG_VERSION:=1.0.6
|
||||
PKG_RELEASE:=21
|
||||
PKG_RELEASE:=22
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/baidudrive-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
@@ -29,7 +29,7 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=BaiduDrive
|
||||
DEPENDS:=@(x86_64||aarch64) +curl
|
||||
DEPENDS:=@(x86_64||aarch64) +curl +linkease-app-entry
|
||||
URL:=https://github.com/linkease/istore-packages
|
||||
endef
|
||||
|
||||
@@ -47,6 +47,9 @@ if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/10-baidudrive-plugin.json 2>/dev/null)" = "/usr/share/baidudrive/baidudrive-plugin.json" ]; then
|
||||
rm -f /usr/share/linkeasefull/desktop-apps.d/10-baidudrive-plugin.json
|
||||
fi
|
||||
if [ "$$(readlink /usr/share/linkease/apps.d/10-baidudrive-plugin.json 2>/dev/null)" = "/usr/share/baidudrive/baidudrive-plugin.json" ]; then
|
||||
rm -f /usr/share/linkease/apps.d/10-baidudrive-plugin.json
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
@@ -58,11 +61,12 @@ define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/libexec/baidudrive $(1)/usr/share/baidudrive/www $(1)/usr/share/linkeasefull/desktop-apps.d $(1)/opt/baidunas-sdk $(1)/opt/baidunas-glibc $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/libexec/baidudrive $(1)/usr/share/baidudrive/www $(1)/usr/share/linkease/apps.d $(1)/usr/share/linkeasefull/desktop-apps.d $(1)/opt/baidunas-sdk $(1)/opt/baidunas-glibc $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/etc/config
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/baidudrive.$(PKG_ARCH_BAIDUDRIVE) $(1)/usr/sbin/baidudrive
|
||||
$(CP) $(PKG_BUILD_DIR)/web/dist/. $(1)/usr/share/baidudrive/www/
|
||||
$(INSTALL_DATA) ./files/baidudrive-plugin.json $(1)/usr/share/baidudrive/baidudrive-plugin.json
|
||||
ln -sf /usr/share/baidudrive/baidudrive-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/10-baidudrive-plugin.json
|
||||
ln -sf /usr/share/baidudrive/baidudrive-plugin.json $(1)/usr/share/linkease/apps.d/10-baidudrive-plugin.json
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/nas_sdk/$(PKG_ARCH_BAIDUDRIVE)/baiduNas $(1)/opt/baidunas-sdk/baiduNas
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/nas_sdk/$(PKG_ARCH_BAIDUDRIVE)/P2PClient.bin $(1)/opt/baidunas-sdk/P2PClient.bin
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/nas_sdk/$(PKG_ARCH_BAIDUDRIVE)/libkernel.so $(1)/opt/baidunas-sdk/libkernel.so
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
"defaultPort": 10780,
|
||||
"upstreamBasePath": "/apps/baidudrive/",
|
||||
"apiPath": "api/",
|
||||
"pathMode": "preserve"
|
||||
"pathMode": "preserve",
|
||||
"externalBasePath": "/"
|
||||
},
|
||||
"window": {
|
||||
"width": 1180,
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.09.20
|
||||
PKG_RELEASE:=46
|
||||
PKG_VERSION:=2026.09.23
|
||||
PKG_RELEASE:=47
|
||||
|
||||
PKG_SOURCE:=dae-src-2026.09.20-2a846ee5a7e4.tar.gz
|
||||
PKG_SOURCE:=dae-src-2026.09.23-c9f580bcaa8a.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
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION:=2026.09.20
|
||||
PKG_RELEASE:=57
|
||||
PKG_VERSION:=2026.09.23
|
||||
PKG_RELEASE:=58
|
||||
|
||||
PKG_SOURCE:=daed-src-2026.09.20-d00211e69e94.tar.gz
|
||||
PKG_SOURCE:=daed-src-2026.09.23-fcc80bac4542.tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-daede/releases/download/daed-src
|
||||
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-$(PKG_VERSION)
|
||||
PKG_HASH:=skip
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-06-23
|
||||
Subject: [PATCH] daed: detach BPF hooks when stopping from dashboard
|
||||
|
||||
Detach BPF hooks when the dashboard stops dae through EmptyConfig.
|
||||
Reported: https://github.com/kenzok8/openwrt-daede/issues/13
|
||||
---
|
||||
--- a/dae/run.go
|
||||
+++ b/dae/run.go
|
||||
@@ -138,6 +138,13 @@
|
||||
/* dae-wing start */
|
||||
newConf := newReloadMsg.Config
|
||||
/* dae-wing end */
|
||||
+
|
||||
+ // openwrt-daede: dashboard stop reloads to EmptyConfig; detach hooks.
|
||||
+ if newConf == EmptyConfig {
|
||||
+ if e := c.DetachBpfHooks(); e != nil {
|
||||
+ log.Errorln("[Stop] DetachBpfHooks:", e)
|
||||
+ }
|
||||
+ }
|
||||
// New logger.
|
||||
oldLogOutput := log.Out
|
||||
log = logrus.New()
|
||||
@@ -1,18 +0,0 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-06-29
|
||||
Subject: [PATCH] daed: use bounded probes for manual latency tests
|
||||
|
||||
Use the existing bounded probe for manual dashboard latency tests.
|
||||
Reported: https://github.com/kenzok8/openwrt-daede/issues/20
|
||||
---
|
||||
--- a/graphql/service/node/latency_mutation_utils.go
|
||||
+++ b/graphql/service/node/latency_mutation_utils.go
|
||||
@@ -108,7 +108,7 @@ func testSingleNodeLatency(option *diale
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
- result, err := d.ProbeLatency()
|
||||
+ result, err := d.ProbeLatencyFast()
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
resolver.MessageV = &msg
|
||||
@@ -1,76 +1,80 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-09-23
|
||||
Subject: [PATCH] daed: bound startup network wait on new wing
|
||||
|
||||
Return an error after 60 seconds and retry on the next generation.
|
||||
---
|
||||
diff --git a/dae/run.go b/dae/run.go
|
||||
index d8f86c2..a0a665d 100644
|
||||
index 7c9a5cc..7f00748 100644
|
||||
--- a/dae/run.go
|
||||
+++ b/dae/run.go
|
||||
@@ -32,7 +32,8 @@ var ChReloadConfigs = make(chan *ReloadMessage)
|
||||
var GracefullyExit = make(chan struct{})
|
||||
var EmptyConfig *daeConfig.Config
|
||||
var c *control.ControlPlane
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/daeuniverse/dae/control"
|
||||
"github.com/daeuniverse/dae/pkg/config_parser"
|
||||
"github.com/daeuniverse/dae/pkg/logger"
|
||||
+ "github.com/daeuniverse/outbound/netproxy"
|
||||
"github.com/daeuniverse/outbound/protocol/direct"
|
||||
"github.com/mohae/deepcopy"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -43,7 +44,8 @@ var EmptyConfig *daeConfig.Config
|
||||
// active is the generation currently serving traffic. It is nil before the
|
||||
// first generation is ready and while a reload has retired the old one.
|
||||
var active atomic.Pointer[control.ControlPlane]
|
||||
-var onceWaitingNetwork sync.Once
|
||||
+var waitingNetworkMu sync.Mutex
|
||||
+var networkReady bool
|
||||
|
||||
func init() {
|
||||
sections, err := config_parser.Parse(`global{} routing{}`)
|
||||
@@ -52,6 +53,19 @@ func ControlPlane() (*control.ControlPlane, error) {
|
||||
@@ -64,6 +66,19 @@ func ControlPlane() (*control.ControlPlane, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
+func waitForNetwork(log *logrus.Logger) error {
|
||||
+func waitForNetwork(log *logrus.Logger, directDialer netproxy.Dialer) error {
|
||||
+ waitingNetworkMu.Lock()
|
||||
+ defer waitingNetworkMu.Unlock()
|
||||
+ if networkReady {
|
||||
+ return nil
|
||||
+ }
|
||||
+ if err := WaitForNetwork(log); err != nil {
|
||||
+ if err := WaitForNetwork(log, directDialer); err != nil {
|
||||
+ return err
|
||||
+ }
|
||||
+ networkReady = true
|
||||
+ return nil
|
||||
+}
|
||||
+
|
||||
func Run(log *logrus.Logger, conf *daeConfig.Config, externGeoDataDirs []string, disableTimestamp bool, dry bool) (err error) {
|
||||
defer close(GracefullyExit)
|
||||
// Not really run dae.
|
||||
@@ -221,6 +235,9 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
|
||||
|
||||
// Deep copy to prevent modification.
|
||||
conf = deepcopy.Copy(conf).(*daeConfig.Config)
|
||||
+ if bpf == nil {
|
||||
+ control.PurgeStaleTCFilters(log)
|
||||
+ }
|
||||
|
||||
// Init Direct Dialers.
|
||||
direct.InitDirectDialers(conf.Global.FallbackResolver)
|
||||
@@ -228,9 +245,9 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
|
||||
// generation is one control plane with its listener. retired is set before
|
||||
// the plane is torn down so a Serve error from a plane that is being replaced
|
||||
// is not mistaken for the active datapath dying.
|
||||
@@ -345,9 +360,9 @@ func newControlPlane(log *logrus.Logger, dnsCache map[string]*control.DnsCache,
|
||||
|
||||
if !conf.Global.DisableWaitingNetwork && len(conf.Global.WanInterface) > 0 {
|
||||
// Wait for network for WAN ready.
|
||||
- onceWaitingNetwork.Do(func() {
|
||||
- WaitForNetwork(log)
|
||||
- WaitForNetwork(log, directDialers.Symmetric)
|
||||
- })
|
||||
+ if err = waitForNetwork(log); err != nil {
|
||||
+ if err = waitForNetwork(log, directDialers.Symmetric); err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
}
|
||||
|
||||
/// Get subscription -> nodeList mapping.
|
||||
diff --git a/dae/utils.go b/dae/utils.go
|
||||
index 65965a2..dc43cbc 100644
|
||||
index 2b4f6bf..93efb7e 100644
|
||||
--- a/dae/utils.go
|
||||
+++ b/dae/utils.go
|
||||
@@ -94,8 +94,9 @@ func preprocessWanInterfaceAuto(params *daeConfig.Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
-func WaitForNetwork(log *logrus.Logger) {
|
||||
+func WaitForNetwork(log *logrus.Logger) error {
|
||||
-func WaitForNetwork(log *logrus.Logger, directDialer netproxy.Dialer) {
|
||||
+func WaitForNetwork(log *logrus.Logger, directDialer netproxy.Dialer) error {
|
||||
epo := 5 * time.Second
|
||||
+ deadline := time.Now().Add(60 * time.Second)
|
||||
client := http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: func(ctx context.Context, network, addr string) (c net.Conn, err error) {
|
||||
@@ -115,6 +116,9 @@ func WaitForNetwork(log *logrus.Logger) {
|
||||
@@ -114,6 +115,9 @@ func WaitForNetwork(log *logrus.Logger, directDialer netproxy.Dialer) {
|
||||
}
|
||||
log.Infoln("Waiting for network...")
|
||||
for i := 0; ; i++ {
|
||||
@@ -80,7 +84,7 @@ index 65965a2..dc43cbc 100644
|
||||
resp, err := client.Get(cmd.CheckNetworkLinks[i%len(cmd.CheckNetworkLinks)])
|
||||
if err != nil {
|
||||
log.Debugln("CheckNetwork:", err)
|
||||
@@ -134,4 +138,5 @@ func WaitForNetwork(log *logrus.Logger) {
|
||||
@@ -133,4 +137,5 @@ func WaitForNetwork(log *logrus.Logger, directDialer netproxy.Dialer) {
|
||||
time.Sleep(epo)
|
||||
}
|
||||
log.Infoln("Network online.")
|
||||
|
||||
@@ -1,22 +1,9 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-09-23
|
||||
Subject: [PATCH] daed: strictly reconcile subscription updates
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
订阅更新按节点链接精确对账,普通组里的失效节点删除,fixed 组固定节点转成独立节点保留。
|
||||
|
||||
数据库提交后按运行状态应用配置,补齐并发地址校验、导入失败回滚和 reload 超时恢复。
|
||||
Reconcile nodes by link, preserve fixed-group nodes, and apply committed changes.
|
||||
---
|
||||
cmd/run.go | 6 +-
|
||||
graphql/mutation.go | 9 +-
|
||||
graphql/root_schema.go | 2 +-
|
||||
graphql/service/config/mutation_utils.go | 59 ++++-
|
||||
.../service/subscription/mutation_utils.go | 181 +++++++++++----
|
||||
.../subscription/mutation_utils_test.go | 212 ++++++++++++++++++
|
||||
6 files changed, 408 insertions(+), 61 deletions(-)
|
||||
create mode 100644 graphql/service/subscription/mutation_utils_test.go
|
||||
|
||||
diff --git a/cmd/run.go b/cmd/run.go
|
||||
index 7dce577..978f46c 100644
|
||||
--- a/cmd/run.go
|
||||
@@ -43,10 +30,10 @@ index 7dce577..978f46c 100644
|
||||
}
|
||||
|
||||
diff --git a/graphql/mutation.go b/graphql/mutation.go
|
||||
index fcbf440..adb747c 100644
|
||||
index af065fd..d462ecc 100644
|
||||
--- a/graphql/mutation.go
|
||||
+++ b/graphql/mutation.go
|
||||
@@ -256,14 +256,7 @@ func (r *MutationResolver) SelectConfig(args *struct {
|
||||
@@ -256,16 +256,7 @@ func (r *MutationResolver) SelectConfig(args *struct {
|
||||
func (r *MutationResolver) Run(args *struct {
|
||||
Dry bool
|
||||
}) (int32, error) {
|
||||
@@ -56,7 +43,9 @@ index fcbf440..adb747c 100644
|
||||
- tx.Rollback()
|
||||
- return 0, err
|
||||
- }
|
||||
- tx.Commit()
|
||||
- if err = tx.Commit().Error; err != nil {
|
||||
- return 0, fmt.Errorf("commit run: %w", err)
|
||||
- }
|
||||
- return ret, nil
|
||||
+ return config.Run(context.TODO(), args.Dry)
|
||||
}
|
||||
@@ -76,7 +65,7 @@ index 8b82db6..a96cace 100644
|
||||
|
||||
# updateSubscriptionLink is to update the subscription link without re-fetching nodes.
|
||||
diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go
|
||||
index fd61554..feb94d3 100644
|
||||
index e6bf9da..8ab756b 100644
|
||||
--- a/graphql/service/config/mutation_utils.go
|
||||
+++ b/graphql/service/config/mutation_utils.go
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
@@ -163,7 +152,7 @@ index fd61554..feb94d3 100644
|
||||
|
||||
// Running -> false
|
||||
diff --git a/graphql/service/subscription/mutation_utils.go b/graphql/service/subscription/mutation_utils.go
|
||||
index e55e65b..e01a342 100644
|
||||
index 7e6dc08..e87bf78 100644
|
||||
--- a/graphql/service/subscription/mutation_utils.go
|
||||
+++ b/graphql/service/subscription/mutation_utils.go
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
@@ -174,7 +163,7 @@ index e55e65b..e01a342 100644
|
||||
"github.com/daeuniverse/dae-wing/graphql/service/node"
|
||||
"github.com/daeuniverse/dae/common/subscription"
|
||||
"github.com/go-co-op/gocron"
|
||||
@@ -171,8 +172,9 @@ func AutoUpdateVersionByIds(d *gorm.DB, ids []uint) (err error) {
|
||||
@@ -175,8 +176,9 @@ func AutoUpdateVersionByIds(d *gorm.DB, ids []uint) (err error) {
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -186,7 +175,7 @@ index e55e65b..e01a342 100644
|
||||
)
|
||||
|
||||
func UpdateAll(ctx context.Context) {
|
||||
@@ -258,68 +260,161 @@ func Update(ctx context.Context, _id graphql.ID) (r *Resolver, err error) {
|
||||
@@ -262,68 +264,161 @@ func Update(ctx context.Context, _id graphql.ID) (r *Resolver, err error) {
|
||||
return &Resolver{Subscription: m}, nil
|
||||
}
|
||||
|
||||
@@ -212,7 +201,7 @@ index e55e65b..e01a342 100644
|
||||
- tx := db.BeginTx(ctx)
|
||||
- defer func() {
|
||||
- if err == nil {
|
||||
- tx.Commit()
|
||||
- err = tx.Commit().Error
|
||||
+ current := make(map[string]struct{}, len(existing))
|
||||
+ var staleIds []uint
|
||||
+ for _, n := range existing {
|
||||
@@ -303,7 +292,7 @@ index e55e65b..e01a342 100644
|
||||
}
|
||||
- hasAnyCandidate := false
|
||||
for _, r := range result {
|
||||
- if r.Error == nil {
|
||||
- if r.Error == nil || *r.Error == node.DuplicatedError.Error() {
|
||||
- hasAnyCandidate = true
|
||||
- break
|
||||
+ if r.Error != nil {
|
||||
@@ -388,7 +377,7 @@ index e55e65b..e01a342 100644
|
||||
return &m, nil
|
||||
}
|
||||
|
||||
@@ -399,6 +494,8 @@ func UpdateLink(ctx context.Context, _id graphql.ID, link string) (r *Resolver,
|
||||
@@ -406,6 +501,8 @@ func UpdateLink(ctx context.Context, _id graphql.ID, link string) (r *Resolver,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -397,11 +386,11 @@ index e55e65b..e01a342 100644
|
||||
|
||||
tx := db.BeginTx(ctx)
|
||||
defer func() {
|
||||
diff --git a/graphql/service/subscription/mutation_utils_test.go b/graphql/service/subscription/mutation_utils_test.go
|
||||
diff --git a/graphql/service/subscription/reconcile_patch_test.go b/graphql/service/subscription/reconcile_patch_test.go
|
||||
new file mode 100644
|
||||
index 0000000..18ff9b9
|
||||
--- /dev/null
|
||||
+++ b/graphql/service/subscription/mutation_utils_test.go
|
||||
+++ b/graphql/service/subscription/reconcile_patch_test.go
|
||||
@@ -0,0 +1,212 @@
|
||||
+/*
|
||||
+ * SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
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)
|
||||
@@ -0,0 +1,28 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-09-23
|
||||
Subject: [PATCH] daed: keep reload callbacks nonblocking
|
||||
|
||||
A timed-out request must not block the daemon when reload later finishes.
|
||||
---
|
||||
diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go
|
||||
index 5996eaa..64229e8 100644
|
||||
--- a/graphql/service/config/mutation_utils.go
|
||||
+++ b/graphql/service/config/mutation_utils.go
|
||||
@@ -298,7 +298,7 @@ func ApplyIfRunning(ctx context.Context) error {
|
||||
func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
//// Dry run.
|
||||
if noLoad {
|
||||
- ch := make(chan error)
|
||||
+ ch := make(chan error, 1)
|
||||
reloadMsg := &dae.ReloadMessage{
|
||||
Config: dae.EmptyConfig,
|
||||
Callback: ch,
|
||||
@@ -523,7 +523,7 @@ func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
readTx = nil
|
||||
|
||||
/// Reload with current config.
|
||||
- chReloadCallback := make(chan error)
|
||||
+ chReloadCallback := make(chan error, 1)
|
||||
reloadMsg := &dae.ReloadMessage{
|
||||
Config: c,
|
||||
Callback: chReloadCallback,
|
||||
@@ -1,837 +0,0 @@
|
||||
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
|
||||
From: kenzok8 <kenzok8@gmail.com>
|
||||
Date: Sun, 20 Sep 2026 16:59:23 +0800
|
||||
Subject: [PATCH] Adapt wing to new dae core
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
改动:适配新核心生命周期,并修复空配置停止和重载回滚。
|
||||
|
||||
原因:旧接口删除且新控制面交接流程已改变。
|
||||
---
|
||||
dae-core/config/marshal.go | 5 +
|
||||
dae/run.go | 402 +++++++++++++-----
|
||||
dae/run_handoff_test.go | 33 ++
|
||||
dae/runtime.go | 6 +-
|
||||
dae/utils.go | 9 +-
|
||||
db/node_utils.go | 3 +-
|
||||
graphql/service/config/mutation_utils.go | 9 +-
|
||||
graphql/service/node/latency_cache.go | 2 +-
|
||||
.../service/node/latency_mutation_utils.go | 59 ++-
|
||||
graphql/service/routing/resolver.go | 2 +-
|
||||
10 files changed, 415 insertions(+), 115 deletions(-)
|
||||
create mode 100644 dae/run_handoff_test.go
|
||||
|
||||
diff --git a/dae-core/config/marshal.go b/dae-core/config/marshal.go
|
||||
index f68006a..69a5325 100644
|
||||
--- a/dae-core/config/marshal.go
|
||||
+++ b/dae-core/config/marshal.go
|
||||
@@ -42,6 +42,11 @@ type Marshaller struct {
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
+// Bytes returns the serialized section buffer without exposing mutable state.
|
||||
+func (m *Marshaller) Bytes() []byte {
|
||||
+ return m.buf.Bytes()
|
||||
+}
|
||||
+
|
||||
func (m *Marshaller) writeLine(depth int, line string) {
|
||||
if depth < 0 {
|
||||
depth = 0
|
||||
diff --git a/dae/run.go b/dae/run.go
|
||||
index a0a665d..96b579c 100644
|
||||
--- a/dae/run.go
|
||||
+++ b/dae/run.go
|
||||
@@ -6,6 +6,8 @@
|
||||
package dae
|
||||
|
||||
import (
|
||||
+ "context"
|
||||
+ "errors"
|
||||
"fmt"
|
||||
"net/netip"
|
||||
"runtime"
|
||||
@@ -28,6 +30,22 @@ type ReloadMessage struct {
|
||||
Callback chan<- error
|
||||
}
|
||||
|
||||
+func closeReloadResources(closers ...func() error) error {
|
||||
+ var errs error
|
||||
+ for _, closeResource := range closers {
|
||||
+ if closeResource != nil {
|
||||
+ errs = errors.Join(errs, closeResource())
|
||||
+ }
|
||||
+ }
|
||||
+ return errs
|
||||
+}
|
||||
+
|
||||
+func notifyReloadCallback(callback chan<- error, err error) {
|
||||
+ if callback != nil {
|
||||
+ callback <- err
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
var ChReloadConfigs = make(chan *ReloadMessage)
|
||||
var GracefullyExit = make(chan struct{})
|
||||
var EmptyConfig *daeConfig.Config
|
||||
@@ -77,88 +95,115 @@ func Run(log *logrus.Logger, conf *daeConfig.Config, externGeoDataDirs []string,
|
||||
case nil:
|
||||
break dryLoop
|
||||
default:
|
||||
- newConf.Callback <- nil
|
||||
+ notifyReloadCallback(newConf.Callback, nil)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// New c.
|
||||
- c, err = newControlPlane(log, nil, nil, conf, externGeoDataDirs)
|
||||
+ processSessions := control.NewSessionManager(context.Background())
|
||||
+ defer func() { _ = processSessions.Close() }()
|
||||
+ c, err = newControlPlane(context.Background(), log, nil, nil, conf, externGeoDataDirs, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
+ if err = c.AttachSessionManager(processSessions); err != nil {
|
||||
+ _ = c.Close()
|
||||
+ return err
|
||||
+ }
|
||||
+ datapathDetached := false
|
||||
+ if conf == EmptyConfig {
|
||||
+ if err = c.DetachBpfHooks(); err != nil {
|
||||
+ _ = c.Close()
|
||||
+ return fmt.Errorf("detach initial empty datapath: %w", err)
|
||||
+ }
|
||||
+ datapathDetached = true
|
||||
+ }
|
||||
|
||||
- // Serve tproxy TCP/UDP server util signals.
|
||||
var listener *control.Listener
|
||||
+ if err = control.GetDaeNetns().With(func() error {
|
||||
+ var listenErr error
|
||||
+ listener, listenErr = c.Listen(conf.Global.TproxyPort)
|
||||
+ if listenErr != nil {
|
||||
+ return listenErr
|
||||
+ }
|
||||
+ return listener.ValidateCurrentNetns()
|
||||
+ }); err != nil {
|
||||
+ if listener != nil {
|
||||
+ _ = listener.Close()
|
||||
+ }
|
||||
+ _ = c.Close()
|
||||
+ return fmt.Errorf("listen in dae netns: %w", err)
|
||||
+ }
|
||||
+ readyChan := make(chan bool, 1)
|
||||
+ serveDone := make(chan error, 1)
|
||||
+ go func() { serveDone <- c.Serve(readyChan, listener) }()
|
||||
+ if !<-readyChan {
|
||||
+ serveErr := <-serveDone
|
||||
+ _ = closeReloadResources(listener.Close, c.Close)
|
||||
+ if serveErr == nil {
|
||||
+ serveErr = fmt.Errorf("initial control plane was not ready")
|
||||
+ }
|
||||
+ return serveErr
|
||||
+ }
|
||||
+ log.Infoln("Ready")
|
||||
go func() {
|
||||
- readyChan := make(chan bool, 1)
|
||||
- go func() {
|
||||
- <-readyChan
|
||||
- log.Infoln("Ready")
|
||||
- }()
|
||||
- control.GetDaeNetns().With(func() error {
|
||||
- if listener, err = c.ListenAndServe(readyChan, conf.Global.TproxyPort); err != nil {
|
||||
- log.Errorln("ListenAndServe:", err)
|
||||
- }
|
||||
- return err
|
||||
- })
|
||||
- // Exit
|
||||
+ if serveErr := <-serveDone; serveErr != nil {
|
||||
+ log.WithError(serveErr).Errorln("control plane serve stopped")
|
||||
+ }
|
||||
ChReloadConfigs <- nil
|
||||
}()
|
||||
reloading := false
|
||||
- /* dae-wing start */
|
||||
- var errReload error
|
||||
- var chCallback chan<- error
|
||||
- /* dae-wing end */
|
||||
loop:
|
||||
for newReloadMsg := range ChReloadConfigs {
|
||||
switch newReloadMsg {
|
||||
case nil:
|
||||
- /* dae-wing start */
|
||||
- // We will receive nil after control plane being Closed.
|
||||
- // We'll judge if we are in a reloading.
|
||||
- /* dae-wing end */
|
||||
-
|
||||
if reloading {
|
||||
- if listener == nil {
|
||||
- // Failed to listen. Exit.
|
||||
- break loop
|
||||
- }
|
||||
- // Serve.
|
||||
+ // The old generation's Serve loop reports its shutdown after the
|
||||
+ // candidate has already taken over the cloned listener.
|
||||
reloading = false
|
||||
- log.Warnln("[Reload] Serve")
|
||||
- readyChan := make(chan bool, 1)
|
||||
- go func() {
|
||||
- if err := c.Serve(readyChan, listener); err != nil {
|
||||
- log.Errorln("ListenAndServe:", err)
|
||||
- }
|
||||
- // Exit
|
||||
- ChReloadConfigs <- nil
|
||||
- }()
|
||||
- <-readyChan
|
||||
- log.Warnln("[Reload] Finished")
|
||||
- /* dae-wing start */
|
||||
- chCallback <- errReload
|
||||
- /* dae-wing end */
|
||||
+ continue
|
||||
} else {
|
||||
- // Listening error.
|
||||
break loop
|
||||
}
|
||||
default:
|
||||
// Reload signal.
|
||||
log.Warnln("[Reload] Received reload signal; prepare to reload")
|
||||
|
||||
- /* dae-wing start */
|
||||
newConf := newReloadMsg.Config
|
||||
- /* dae-wing end */
|
||||
-
|
||||
- // openwrt-daede: dashboard stop reloads to EmptyConfig; detach hooks.
|
||||
- if newConf == EmptyConfig {
|
||||
- if e := c.DetachBpfHooks(); e != nil {
|
||||
- log.Errorln("[Stop] DetachBpfHooks:", e)
|
||||
+ if datapathDetached && newConf == EmptyConfig {
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, nil)
|
||||
+ continue
|
||||
+ }
|
||||
+ oldC := c
|
||||
+ wasStopped := datapathDetached
|
||||
+ failReload := func(cause, cleanupErr error) error {
|
||||
+ cause = errors.Join(cause, cleanupErr)
|
||||
+ if cleanupErr == nil && wasStopped {
|
||||
+ if detachErr := oldC.DetachBpfHooks(); detachErr != nil {
|
||||
+ cause = errors.Join(cause, fmt.Errorf("restore stopped datapath: %w", detachErr))
|
||||
+ cleanupErr = detachErr
|
||||
+ } else {
|
||||
+ datapathDetached = true
|
||||
+ }
|
||||
}
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, cause)
|
||||
+ if cleanupErr != nil {
|
||||
+ return cause
|
||||
+ }
|
||||
+ return nil
|
||||
}
|
||||
+ if wasStopped {
|
||||
+ if restoreErr := oldC.RestoreDatapathForReloadRollback(); restoreErr != nil {
|
||||
+ if fatalErr := failReload(fmt.Errorf("prepare stopped datapath: %w", restoreErr), nil); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ datapathDetached = false
|
||||
+ }
|
||||
+
|
||||
// New logger.
|
||||
oldLogOutput := log.Out
|
||||
log = logrus.New()
|
||||
@@ -167,65 +212,223 @@ loop:
|
||||
log.SetOutput(oldLogOutput) // FIXME: THIS IS A HACK.
|
||||
logrus.SetOutput(oldLogOutput)
|
||||
|
||||
- // New control plane.
|
||||
- obj := c.EjectBpf()
|
||||
var dnsCache map[string]*control.DnsCache
|
||||
if conf.Dns.IpVersionPrefer == newConf.Dns.IpVersionPrefer {
|
||||
// Only keep dns cache when ip version preference not change.
|
||||
dnsCache = c.CloneDnsCache()
|
||||
}
|
||||
- log.Warnln("[Reload] Load new control plane")
|
||||
- newC, err := newControlPlane(log, obj, dnsCache, newConf, externGeoDataDirs)
|
||||
+ log.Warnln("[Reload] Prepare new control plane")
|
||||
+ freshState, err := c.SnapshotFreshDatapathState()
|
||||
if err != nil {
|
||||
- /* dae-wing start */
|
||||
- errReload = err
|
||||
- /* dae-wing end */
|
||||
-
|
||||
- log.WithFields(logrus.Fields{
|
||||
- "err": err,
|
||||
- }).Errorln("[Reload] Failed to reload; try to roll back configuration")
|
||||
- // Load last config back.
|
||||
- newC, err = newControlPlane(log, obj, dnsCache, conf, externGeoDataDirs)
|
||||
- if err != nil {
|
||||
- obj.Close()
|
||||
- c.Close()
|
||||
- log.WithFields(logrus.Fields{
|
||||
- "err": err,
|
||||
- }).Fatalln("[Reload] Failed to roll back configuration")
|
||||
+ if fatalErr := failReload(err, nil); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
}
|
||||
- newConf = conf
|
||||
- log.Errorln("[Reload] Last reload failed; rolled back configuration")
|
||||
- } else {
|
||||
- log.Warnln("[Reload] Stopped old control plane")
|
||||
-
|
||||
- /* dae-wing start */
|
||||
- errReload = nil
|
||||
- /* dae-wing end */
|
||||
+ continue
|
||||
}
|
||||
+ newC, err := newControlPlane(context.Background(), log, freshState, dnsCache, newConf, externGeoDataDirs, true)
|
||||
+ if err == nil {
|
||||
+ err = newC.AttachSessionManager(processSessions)
|
||||
+ }
|
||||
+ if err != nil {
|
||||
+ var cleanupErr error
|
||||
+ if newC != nil {
|
||||
+ cleanupErr = newC.Close()
|
||||
+ }
|
||||
+ if fatalErr := failReload(err, cleanupErr); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ stagedListener, err := listener.Clone()
|
||||
+ if err != nil {
|
||||
+ if fatalErr := failReload(fmt.Errorf("clone listener: %w", err), newC.Close()); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ if err = oldC.StopDNSListener(); err != nil {
|
||||
+ cleanupErr := closeReloadResources(stagedListener.Close, newC.Close, oldC.RestartDNSListener)
|
||||
+ if fatalErr := failReload(fmt.Errorf("stop old DNS listener: %w", err), cleanupErr); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ dnsStopped := true
|
||||
+ readyChan := make(chan bool, 1)
|
||||
+ serveDone := make(chan error, 1)
|
||||
+ go func() { serveDone <- newC.Serve(readyChan, stagedListener) }()
|
||||
+ ready := <-readyChan
|
||||
+ if !ready {
|
||||
+ serveErr := <-serveDone
|
||||
+ if serveErr == nil {
|
||||
+ serveErr = fmt.Errorf("prepared control plane was not ready")
|
||||
+ }
|
||||
+ cleanupErr := closeReloadResources(
|
||||
+ stagedListener.Close,
|
||||
+ newC.Close,
|
||||
+ oldC.RestartDNSListener,
|
||||
+ )
|
||||
+ dnsStopped = false
|
||||
+ if fatalErr := failReload(serveErr, cleanupErr); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ hookPrepared := false
|
||||
+ provisionalOwner := false
|
||||
+ hookFlipCommitted := false
|
||||
+ hookSetAdopted := false
|
||||
+ flowDatapathAdopted := false
|
||||
+ rollbackCleanupFailed := false
|
||||
+ rollbackCandidate := func(cause error) error {
|
||||
+ var rollbackErr error
|
||||
+ addRollbackErr := func(err error) {
|
||||
+ if err != nil {
|
||||
+ rollbackCleanupFailed = true
|
||||
+ rollbackErr = errors.Join(rollbackErr, err)
|
||||
+ }
|
||||
+ }
|
||||
+ if flowDatapathAdopted {
|
||||
+ addRollbackErr(oldC.AdoptProcessFlowDatapath(newC))
|
||||
+ }
|
||||
+ if hookSetAdopted {
|
||||
+ if ownerErr := newC.RestorePreparedTCHookSet(oldC); ownerErr != nil {
|
||||
+ rollbackCleanupFailed = true
|
||||
+ return errors.Join(cause, rollbackErr, fmt.Errorf("restore TC HookSet owner: %w", ownerErr))
|
||||
+ }
|
||||
+ }
|
||||
+ if hookFlipCommitted {
|
||||
+ addRollbackErr(newC.RollbackPreparedBpfHookFlip())
|
||||
+ }
|
||||
+ if hookPrepared && !hookSetAdopted && !hookFlipCommitted {
|
||||
+ addRollbackErr(newC.ClearPreparedTCHookHandoff())
|
||||
+ }
|
||||
+ if provisionalOwner {
|
||||
+ newC.UnregisterProvisionalRoutingEpochExecutionOwner()
|
||||
+ }
|
||||
+ addRollbackErr(newC.DetachBpfHooks())
|
||||
+ addRollbackErr(stagedListener.Close())
|
||||
+ addRollbackErr(newC.Close())
|
||||
+ if dnsStopped {
|
||||
+ addRollbackErr(oldC.RestartDNSListener())
|
||||
+ dnsStopped = false
|
||||
+ }
|
||||
+ return errors.Join(cause, rollbackErr)
|
||||
+ }
|
||||
+ if err = newC.PrepareTCHookHandoff(oldC); err != nil {
|
||||
+ rollbackErr := rollbackCandidate(err)
|
||||
+ if rollbackCleanupFailed {
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
|
||||
+ return rollbackErr
|
||||
+ }
|
||||
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ hookPrepared = true
|
||||
+ if err = newC.RegisterProvisionalRoutingEpochExecutionOwner(); err != nil {
|
||||
+ rollbackErr := rollbackCandidate(err)
|
||||
+ if rollbackCleanupFailed {
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
|
||||
+ return rollbackErr
|
||||
+ }
|
||||
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ provisionalOwner = true
|
||||
+ if err = newC.CommitPreparedBpfHookFlip(); err != nil {
|
||||
+ rollbackErr := rollbackCandidate(err)
|
||||
+ if rollbackCleanupFailed {
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
|
||||
+ return rollbackErr
|
||||
+ }
|
||||
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ hookFlipCommitted = true
|
||||
+ if err = newC.AdoptPreparedTCHookSet(oldC); err != nil {
|
||||
+ rollbackErr := rollbackCandidate(err)
|
||||
+ if rollbackCleanupFailed {
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
|
||||
+ return rollbackErr
|
||||
+ }
|
||||
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ hookSetAdopted = true
|
||||
+ if err = newC.AdoptProcessFlowDatapath(oldC); err != nil {
|
||||
+ rollbackErr := rollbackCandidate(err)
|
||||
+ if rollbackCleanupFailed {
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, rollbackErr)
|
||||
+ return rollbackErr
|
||||
+ }
|
||||
+ if fatalErr := failReload(rollbackErr, nil); fatalErr != nil {
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ continue
|
||||
+ }
|
||||
+ flowDatapathAdopted = true
|
||||
+ newC.UnregisterProvisionalRoutingEpochExecutionOwner()
|
||||
+ provisionalOwner = false
|
||||
+ if err = newC.FinalizePreparedTCHooks(); err != nil {
|
||||
+ fatalErr := errors.Join(fmt.Errorf("finalize prepared TC hooks: %w", err), closeReloadResources(
|
||||
+ listener.Close,
|
||||
+ stagedListener.Close,
|
||||
+ newC.Close,
|
||||
+ oldC.Close,
|
||||
+ ))
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, fatalErr)
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ hookPrepared = false
|
||||
+ hookFlipCommitted = false
|
||||
+ hookSetAdopted = false
|
||||
|
||||
- // Inject bpf objects into the new control plane life-cycle.
|
||||
- newC.InjectBpf(obj)
|
||||
-
|
||||
- // Prepare new context.
|
||||
- oldC := c
|
||||
+ oldListener := listener
|
||||
c = newC
|
||||
conf = newConf
|
||||
+ listener = stagedListener
|
||||
+ datapathDetached = false
|
||||
+ retireErr := closeReloadResources(oldListener.Close, oldC.Close)
|
||||
+ if newConf == EmptyConfig {
|
||||
+ if retireErr != nil {
|
||||
+ fatalErr := errors.Join(fmt.Errorf("retire previous control plane: %w", retireErr), closeReloadResources(stagedListener.Close, newC.Close))
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, fatalErr)
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ if detachErr := newC.DetachBpfHooks(); detachErr != nil {
|
||||
+ fatalErr := errors.Join(fmt.Errorf("detach stopped datapath: %w", detachErr), closeReloadResources(stagedListener.Close, newC.Close))
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, fatalErr)
|
||||
+ return fatalErr
|
||||
+ }
|
||||
+ datapathDetached = true
|
||||
+ } else if retireErr != nil {
|
||||
+ log.WithError(retireErr).Warnln("[Reload] Failed to close old control plane")
|
||||
+ }
|
||||
reloading = true
|
||||
- /* dae-wing start */
|
||||
- chCallback = newReloadMsg.Callback
|
||||
- /* dae-wing end */
|
||||
-
|
||||
- // Ready to close.
|
||||
- oldC.Close()
|
||||
+ notifyReloadCallback(newReloadMsg.Callback, nil)
|
||||
+ go func(done <-chan error) {
|
||||
+ if serveErr := <-done; serveErr != nil {
|
||||
+ log.WithError(serveErr).Errorln("control plane serve stopped")
|
||||
+ }
|
||||
+ ChReloadConfigs <- nil
|
||||
+ }(serveDone)
|
||||
}
|
||||
}
|
||||
- if e := c.Close(); e != nil {
|
||||
+ if e := closeReloadResources(listener.Close, c.Close); e != nil {
|
||||
return fmt.Errorf("close control plane: %w", e)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
-func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*control.DnsCache, conf *daeConfig.Config, externGeoDataDirs []string) (c *control.ControlPlane, err error) {
|
||||
+func newControlPlane(ctx context.Context, log *logrus.Logger, bpf interface{}, dnsCache map[string]*control.DnsCache, conf *daeConfig.Config, externGeoDataDirs []string, prepared bool) (c *control.ControlPlane, err error) {
|
||||
|
||||
// Print configuration.
|
||||
if log.IsLevelEnabled(logrus.DebugLevel) {
|
||||
@@ -239,10 +442,6 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
|
||||
control.PurgeStaleTCFilters(log)
|
||||
}
|
||||
|
||||
- // Init Direct Dialers.
|
||||
- direct.InitDirectDialers(conf.Global.FallbackResolver)
|
||||
- netutils.FallbackDns = netip.MustParseAddrPort(conf.Global.FallbackResolver)
|
||||
-
|
||||
if !conf.Global.DisableWaitingNetwork && len(conf.Global.WanInterface) > 0 {
|
||||
// Wait for network for WAN ready.
|
||||
if err = waitForNetwork(log); err != nil {
|
||||
@@ -266,7 +465,10 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
|
||||
}
|
||||
|
||||
// New dae control plane.
|
||||
- c, err = control.NewControlPlane(
|
||||
+ directDialers := direct.NewDirectDialers(conf.Global.FallbackResolver)
|
||||
+ systemDNSResolver := netutils.NewSystemDNSResolver(netip.MustParseAddrPort(conf.Global.FallbackResolver))
|
||||
+ c, err = control.NewControlPlaneWithContextOptions(
|
||||
+ ctx,
|
||||
log,
|
||||
bpf,
|
||||
dnsCache,
|
||||
@@ -276,6 +478,14 @@ func newControlPlane(log *logrus.Logger, bpf interface{}, dnsCache map[string]*c
|
||||
&conf.Global,
|
||||
&conf.Dns,
|
||||
externGeoDataDirs,
|
||||
+ control.ControlPlaneBuildOptions{
|
||||
+ DelayDatapathCommit: prepared,
|
||||
+ DelayDNSListenerStart: prepared,
|
||||
+ IsReload: prepared || bpf != nil,
|
||||
+ DirectDialer: directDialers.Symmetric,
|
||||
+ FullconeDirectDialer: directDialers.Fullcone,
|
||||
+ SystemDNSResolver: systemDNSResolver,
|
||||
+ },
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
diff --git a/dae/run_handoff_test.go b/dae/run_handoff_test.go
|
||||
new file mode 100644
|
||||
index 0000000..0ffb2ec
|
||||
--- /dev/null
|
||||
+++ b/dae/run_handoff_test.go
|
||||
@@ -0,0 +1,33 @@
|
||||
+package dae
|
||||
+
|
||||
+import (
|
||||
+ "errors"
|
||||
+ "reflect"
|
||||
+ "testing"
|
||||
+)
|
||||
+
|
||||
+func TestCloseReloadResourcesOrder(t *testing.T) {
|
||||
+ var got []string
|
||||
+ err := closeReloadResources(
|
||||
+ func() error { got = append(got, "old-listener"); return nil },
|
||||
+ func() error { got = append(got, "staged-listener"); return nil },
|
||||
+ func() error { got = append(got, "new-control-plane"); return nil },
|
||||
+ func() error { got = append(got, "old-control-plane"); return nil },
|
||||
+ )
|
||||
+ if err != nil {
|
||||
+ t.Fatalf("closeReloadResources() error = %v", err)
|
||||
+ }
|
||||
+ want := []string{"old-listener", "staged-listener", "new-control-plane", "old-control-plane"}
|
||||
+ if !reflect.DeepEqual(got, want) {
|
||||
+ t.Fatalf("close order = %v, want %v", got, want)
|
||||
+ }
|
||||
+}
|
||||
+
|
||||
+func TestCloseReloadResourcesJoinsErrors(t *testing.T) {
|
||||
+ one := errors.New("old listener")
|
||||
+ two := errors.New("old control plane")
|
||||
+ err := closeReloadResources(func() error { return one }, func() error { return two })
|
||||
+ if !errors.Is(err, one) || !errors.Is(err, two) {
|
||||
+ t.Fatalf("joined error = %v, want both close errors", err)
|
||||
+ }
|
||||
+}
|
||||
diff --git a/dae/runtime.go b/dae/runtime.go
|
||||
index 2ab0d68..af46089 100644
|
||||
--- a/dae/runtime.go
|
||||
+++ b/dae/runtime.go
|
||||
@@ -30,18 +30,16 @@ type RuntimeOverview struct {
|
||||
}
|
||||
|
||||
func GetRuntimeOverview(windowSec int, maxPoints int) (*RuntimeOverview, error) {
|
||||
- activeTCPConnections := 0
|
||||
ctl, err := ControlPlane()
|
||||
+ var snapshot control.RuntimeStatsSnapshot
|
||||
if err != nil {
|
||||
if !errors.Is(err, ErrControlPlaneNotInit) {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
- activeTCPConnections = ctl.ActiveTCPConnections()
|
||||
+ snapshot = ctl.SnapshotRuntimeStats(windowSec, maxPoints)
|
||||
}
|
||||
|
||||
- snapshot := control.SnapshotRuntimeStats(activeTCPConnections, control.DefaultUdpEndpointPool.Count(), windowSec, maxPoints)
|
||||
-
|
||||
samples := make([]RuntimeTrafficSample, 0, len(snapshot.Samples))
|
||||
for _, sample := range snapshot.Samples {
|
||||
samples = append(samples, RuntimeTrafficSample{
|
||||
diff --git a/dae/utils.go b/dae/utils.go
|
||||
index dc43cbc..ca61c85 100644
|
||||
--- a/dae/utils.go
|
||||
+++ b/dae/utils.go
|
||||
@@ -32,8 +32,11 @@ var (
|
||||
EmptyGlobalSection = `global {}`
|
||||
)
|
||||
|
||||
-func NecessaryOutbounds(routing *daeConfig.Routing) (outbounds []string) {
|
||||
- f := daeConfig.FunctionOrStringToFunction(routing.Fallback)
|
||||
+func NecessaryOutbounds(routing *daeConfig.Routing) (outbounds []string, err error) {
|
||||
+ f, err := daeConfig.ParseFunctionOrString(routing.Fallback)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
outbounds = append(outbounds, f.Name)
|
||||
for _, r := range routing.Rules {
|
||||
outbound := r.Outbound.Name
|
||||
@@ -42,7 +45,7 @@ func NecessaryOutbounds(routing *daeConfig.Routing) (outbounds []string) {
|
||||
}
|
||||
outbounds = append(outbounds, outbound)
|
||||
}
|
||||
- return daeCommon.Deduplicate(outbounds)
|
||||
+ return daeCommon.Deduplicate(outbounds), nil
|
||||
}
|
||||
|
||||
func ParseConfig(globalSection *string, dnsSection *string, routingSection *string) (*daeConfig.Config, error) {
|
||||
diff --git a/db/node_utils.go b/db/node_utils.go
|
||||
index b7b43b8..7ca8fc3 100644
|
||||
--- a/db/node_utils.go
|
||||
+++ b/db/node_utils.go
|
||||
@@ -6,6 +6,7 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
+ "context"
|
||||
"strings"
|
||||
|
||||
"github.com/daeuniverse/dae-wing/common"
|
||||
@@ -25,7 +26,7 @@ func NewNodeModel(link string, tag *string, subscriptionId *uint) (*Node, error)
|
||||
}
|
||||
_tag = *tag
|
||||
}
|
||||
- d, err := dialer.NewFromLink(&dialer.GlobalOption{
|
||||
+ d, err := dialer.NewFromLinkContext(context.Background(), &dialer.GlobalOption{
|
||||
Log: logrus.StandardLogger(),
|
||||
}, dialer.InstanceOption{DisableCheck: false}, link, _tag)
|
||||
if err != nil {
|
||||
diff --git a/graphql/service/config/mutation_utils.go b/graphql/service/config/mutation_utils.go
|
||||
index 59f0ebd..c7d6b78 100644
|
||||
--- a/graphql/service/config/mutation_utils.go
|
||||
+++ b/graphql/service/config/mutation_utils.go
|
||||
@@ -298,7 +298,7 @@ func ApplyIfRunning(ctx context.Context) error {
|
||||
func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
//// Dry run.
|
||||
if noLoad {
|
||||
- ch := make(chan error)
|
||||
+ ch := make(chan error, 1)
|
||||
reloadMsg := &dae.ReloadMessage{
|
||||
Config: dae.EmptyConfig,
|
||||
Callback: ch,
|
||||
@@ -384,7 +384,10 @@ func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
}
|
||||
/// Fill in necessary groups and nodes.
|
||||
// Find groups needed by routing.
|
||||
- outbounds := dae.NecessaryOutbounds(&c.Routing)
|
||||
+ outbounds, err := dae.NecessaryOutbounds(&c.Routing)
|
||||
+ if err != nil {
|
||||
+ return 0, err
|
||||
+ }
|
||||
var groups []db.Group
|
||||
q = d.Model(&db.Group{}).
|
||||
Where("name in ?", outbounds).
|
||||
@@ -523,7 +526,7 @@ func runLocked(ctx context.Context, noLoad bool) (n int32, err error) {
|
||||
readTx = nil
|
||||
|
||||
/// Reload with current config.
|
||||
- chReloadCallback := make(chan error)
|
||||
+ chReloadCallback := make(chan error, 1)
|
||||
reloadMsg := &dae.ReloadMessage{
|
||||
Config: c,
|
||||
Callback: chReloadCallback,
|
||||
diff --git a/graphql/service/node/latency_cache.go b/graphql/service/node/latency_cache.go
|
||||
index ba66794..e0ec91e 100644
|
||||
--- a/graphql/service/node/latency_cache.go
|
||||
+++ b/graphql/service/node/latency_cache.go
|
||||
@@ -162,7 +162,7 @@ func refreshLatencyCache(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
|
||||
- results := testLatencyResultsForNodes(option, nodes)
|
||||
+ results := testLatencyResultsForNodes(ctx, option, nodes)
|
||||
storeLatencyResults(results)
|
||||
|
||||
if ctl, err := dae.ControlPlane(); err == nil {
|
||||
diff --git a/graphql/service/node/latency_mutation_utils.go b/graphql/service/node/latency_mutation_utils.go
|
||||
index 056f9fa..5026b33 100644
|
||||
--- a/graphql/service/node/latency_mutation_utils.go
|
||||
+++ b/graphql/service/node/latency_mutation_utils.go
|
||||
@@ -8,6 +8,8 @@ package node
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
+ "net/netip"
|
||||
+ "strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -15,6 +17,7 @@ import (
|
||||
"github.com/daeuniverse/dae-wing/dae"
|
||||
"github.com/daeuniverse/dae-wing/db"
|
||||
dialer "github.com/daeuniverse/dae/component/outbound/dialer"
|
||||
+ "github.com/daeuniverse/outbound/netproxy"
|
||||
"github.com/graph-gophers/graphql-go"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -32,12 +35,12 @@ func TestLatencies(ctx context.Context, ids *[]graphql.ID) ([]*LatencyResolver,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
- results := testLatencyResultsForNodes(option, nodes)
|
||||
+ results := testLatencyResultsForNodes(ctx, option, nodes)
|
||||
storeLatencyResults(results)
|
||||
return results, nil
|
||||
}
|
||||
|
||||
-func testLatencyResultsForNodes(option *dialer.GlobalOption, nodes []db.Node) []*LatencyResolver {
|
||||
+func testLatencyResultsForNodes(ctx context.Context, option *dialer.GlobalOption, nodes []db.Node) []*LatencyResolver {
|
||||
results := make([]*LatencyResolver, len(nodes))
|
||||
sem := make(chan struct{}, latencyProbeConcurrency)
|
||||
var wg sync.WaitGroup
|
||||
@@ -51,7 +54,7 @@ func testLatencyResultsForNodes(option *dialer.GlobalOption, nodes []db.Node) []
|
||||
defer func() { <-sem }()
|
||||
|
||||
node := nodes[index]
|
||||
- results[index] = testSingleNodeLatency(option, &node)
|
||||
+ results[index] = testSingleNodeLatency(ctx, option, &node)
|
||||
}()
|
||||
}
|
||||
|
||||
@@ -93,14 +96,14 @@ func latencyProbeNodes(ctx context.Context, ids *[]graphql.ID) ([]db.Node, error
|
||||
return nodes, nil
|
||||
}
|
||||
|
||||
-func testSingleNodeLatency(option *dialer.GlobalOption, node *db.Node) *LatencyResolver {
|
||||
+func testSingleNodeLatency(ctx context.Context, option *dialer.GlobalOption, node *db.Node) *LatencyResolver {
|
||||
resolver := &LatencyResolver{
|
||||
NodeID: node.ID,
|
||||
AliveVal: false,
|
||||
TestedAtV: time.Now(),
|
||||
}
|
||||
|
||||
- d, err := dialer.NewFromLink(option, dialer.InstanceOption{DisableCheck: false}, node.Link, "")
|
||||
+ d, err := dialer.NewFromLinkContext(ctx, option, dialer.InstanceOption{DisableCheck: false}, node.Link, "")
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
resolver.MessageV = &msg
|
||||
@@ -108,7 +111,7 @@ func testSingleNodeLatency(option *dialer.GlobalOption, node *db.Node) *LatencyR
|
||||
}
|
||||
defer d.Close()
|
||||
|
||||
- result, err := d.ProbeLatencyFast()
|
||||
+ result, err := probeLatencyFast(ctx, d)
|
||||
if err != nil {
|
||||
msg := err.Error()
|
||||
resolver.MessageV = &msg
|
||||
@@ -128,3 +131,47 @@ func testSingleNodeLatency(option *dialer.GlobalOption, node *db.Node) *LatencyR
|
||||
}
|
||||
return resolver
|
||||
}
|
||||
+
|
||||
+func probeLatencyFast(ctx context.Context, d *dialer.Dialer) (*dialer.LatencyProbeResult, error) {
|
||||
+ opt, err := d.TcpCheckOptionRaw.Option()
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ network, err := netproxy.ParseMagicNetwork(d.TcpCheckOptionRaw.ResolverNetwork)
|
||||
+ if err != nil {
|
||||
+ return nil, err
|
||||
+ }
|
||||
+ result := &dialer.LatencyProbeResult{CheckedAt: time.Now()}
|
||||
+ var messages []string
|
||||
+ for _, probe := range []struct {
|
||||
+ index int
|
||||
+ ip netip.Addr
|
||||
+ }{
|
||||
+ {index: dialer.IdxTcp4, ip: opt.Ip4},
|
||||
+ {index: dialer.IdxTcp6, ip: opt.Ip6},
|
||||
+ } {
|
||||
+ if !probe.ip.IsValid() {
|
||||
+ continue
|
||||
+ }
|
||||
+ probeCtx, cancel := context.WithTimeout(ctx, 1500*time.Millisecond)
|
||||
+ started := time.Now()
|
||||
+ ok, probeErr := d.HttpCheck(probeCtx, probe.index, opt.Url, probe.ip, opt.Method, network.Mark, network.Mptcp)
|
||||
+ cancel()
|
||||
+ if ok {
|
||||
+ latency := time.Since(started)
|
||||
+ if !result.Alive || latency < result.Latency {
|
||||
+ result.Latency = latency
|
||||
+ }
|
||||
+ result.Alive = true
|
||||
+ } else if probeErr != nil {
|
||||
+ messages = append(messages, probeErr.Error())
|
||||
+ }
|
||||
+ }
|
||||
+ if !result.Alive {
|
||||
+ result.Message = strings.Join(messages, "; ")
|
||||
+ if result.Message == "" {
|
||||
+ result.Message = "no latency result"
|
||||
+ }
|
||||
+ }
|
||||
+ return result, nil
|
||||
+}
|
||||
diff --git a/graphql/service/routing/resolver.go b/graphql/service/routing/resolver.go
|
||||
index 7822d64..3ed61aa 100644
|
||||
--- a/graphql/service/routing/resolver.go
|
||||
+++ b/graphql/service/routing/resolver.go
|
||||
@@ -41,7 +41,7 @@ func (r *Resolver) Selected() bool {
|
||||
return r.Model.Selected
|
||||
}
|
||||
|
||||
-func (r *Resolver) ReferenceGroups() (outbounds []string) {
|
||||
+func (r *Resolver) ReferenceGroups() (outbounds []string, err error) {
|
||||
return dae.NecessaryOutbounds(r.DaeRouting)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
From: kenzok8 <kenzok8@noreply>
|
||||
Date: 2026-09-23
|
||||
Subject: [PATCH] daed: expose config marshaller bytes
|
||||
|
||||
The current wing generator calls Marshaller.Bytes when updating global config.
|
||||
---
|
||||
diff --git a/dae-core/config/marshal.go b/dae-core/config/marshal.go
|
||||
index f68006a..dfd6777 100644
|
||||
--- a/dae-core/config/marshal.go
|
||||
+++ b/dae-core/config/marshal.go
|
||||
@@ -42,6 +42,10 @@ type Marshaller struct {
|
||||
buf bytes.Buffer
|
||||
}
|
||||
|
||||
+func (m *Marshaller) Bytes() []byte {
|
||||
+ return m.buf.Bytes()
|
||||
+}
|
||||
+
|
||||
func (m *Marshaller) writeLine(depth int, line string) {
|
||||
if depth < 0 {
|
||||
depth = 0
|
||||
@@ -10,7 +10,7 @@ PKG_ARCH_DOCKERMANAGER:=$(ARCH)
|
||||
|
||||
PKG_NAME:=dockermanager
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_RELEASE:=7
|
||||
PKG_RELEASE:=8
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/dockermanager-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
@@ -26,7 +26,7 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=System
|
||||
TITLE:=Docker Manager
|
||||
URL:=https://github.com/linkease/linkease-docker-manager
|
||||
DEPENDS:=+docker +dockerd +ca-bundle
|
||||
DEPENDS:=+docker +dockerd +ca-bundle +linkease-app-entry
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
@@ -43,6 +43,9 @@ if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/20-dockermanager-plugin.json 2>/dev/null)" = "/usr/share/dockermanager/dockermanager-plugin.json" ]; then
|
||||
rm -f /usr/share/linkeasefull/desktop-apps.d/20-dockermanager-plugin.json
|
||||
fi
|
||||
if [ "$$(readlink /usr/share/linkease/apps.d/20-dockermanager-plugin.json 2>/dev/null)" = "/usr/share/dockermanager/dockermanager-plugin.json" ]; then
|
||||
rm -f /usr/share/linkease/apps.d/20-dockermanager-plugin.json
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
@@ -54,12 +57,13 @@ define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/share/dockermanager/www $(1)/usr/share/linkeasefull/desktop-apps.d $(1)/etc/init.d $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/share/dockermanager/www $(1)/usr/share/linkease/apps.d $(1)/usr/share/linkeasefull/desktop-apps.d $(1)/etc/init.d $(1)/etc/config
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/docker-manager.$(PKG_ARCH_DOCKERMANAGER) $(1)/usr/sbin/docker-manager
|
||||
$(CP) $(PKG_BUILD_DIR)/www/* $(1)/usr/share/dockermanager/www/
|
||||
$(INSTALL_DATA) ./files/logo.svg $(1)/usr/share/dockermanager/www/logo.svg
|
||||
$(INSTALL_DATA) ./files/dockermanager-plugin.json $(1)/usr/share/dockermanager/dockermanager-plugin.json
|
||||
ln -sf /usr/share/dockermanager/dockermanager-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/20-dockermanager-plugin.json
|
||||
ln -sf /usr/share/dockermanager/dockermanager-plugin.json $(1)/usr/share/linkease/apps.d/20-dockermanager-plugin.json
|
||||
$(INSTALL_BIN) ./files/dockermanager.init $(1)/etc/init.d/dockermanager
|
||||
$(INSTALL_CONF) ./files/dockermanager.config $(1)/etc/config/dockermanager
|
||||
endef
|
||||
|
||||
+19
-3
@@ -10,7 +10,7 @@ PKG_ARCH_FASTNET:=$(ARCH)
|
||||
|
||||
PKG_NAME:=fastnet
|
||||
PKG_VERSION:=0.7.7
|
||||
PKG_RELEASE:=4
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/fastnet-runtime-v$(PKG_VERSION)/
|
||||
@@ -28,7 +28,20 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=FastNet - network test Web UI
|
||||
DEPENDS:=@(arm||x86_64||aarch64)
|
||||
DEPENDS:=@(arm||x86_64||aarch64) +linkease-app-entry
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||
if [ "$$(readlink /usr/share/linkease/apps.d/17-fastnet.json 2>/dev/null)" = "/usr/share/fastnet/fastnet-plugin.json" ]; then
|
||||
rm -f /usr/share/linkease/apps.d/17-fastnet.json
|
||||
fi
|
||||
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/17-fastnet.json 2>/dev/null)" = "/usr/share/fastnet/fastnet-plugin.json" ]; then
|
||||
rm -f /usr/share/linkeasefull/desktop-apps.d/17-fastnet.json
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
@@ -54,11 +67,14 @@ define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/usr/share/fastnet $(1)/usr/share/linkease/apps.d $(1)/usr/share/linkeasefull/desktop-apps.d
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/FastNet.$(PKG_ARCH_FASTNET) $(1)/usr/sbin/FastNet
|
||||
$(INSTALL_CONF) ./files/fastnet.config $(1)/etc/config/fastnet
|
||||
$(INSTALL_BIN) ./files/fastnet.init $(1)/etc/init.d/fastnet
|
||||
$(INSTALL_BIN) ./files/fastnet.uci-default $(1)/etc/uci-defaults/fastnet
|
||||
$(INSTALL_DATA) ./files/fastnet-plugin.json $(1)/usr/share/fastnet/fastnet-plugin.json
|
||||
ln -sf /usr/share/fastnet/fastnet-plugin.json $(1)/usr/share/linkease/apps.d/17-fastnet.json
|
||||
ln -sf /usr/share/fastnet/fastnet-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/17-fastnet.json
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "fastnet",
|
||||
"name": "FastNet",
|
||||
"desktop": {
|
||||
"mode": "iframe",
|
||||
"target": {
|
||||
"scheme": "http",
|
||||
"hostMode": "request-host",
|
||||
"port": {
|
||||
"default": 3200,
|
||||
"keys": {
|
||||
"uci": "fastnet.@fastnet[0].port"
|
||||
}
|
||||
},
|
||||
"path": "/"
|
||||
},
|
||||
"access": {
|
||||
"scope": "lan",
|
||||
"blockedTitle": "请在局域网内打开 FastNet",
|
||||
"blockedMessage": "浏览器需要直接连接设备才能准确测量局域网速率。请连接设备所在局域网后,使用设备的局域网地址重新打开。"
|
||||
}
|
||||
},
|
||||
"standalone": {
|
||||
"basePath": "/apps/fastnet/",
|
||||
"externalOpen": {
|
||||
"enabled": true,
|
||||
"label": "在浏览器中打开 FastNet"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"providerOrder": ["uci"],
|
||||
"providers": {
|
||||
"uci": {"type": "uci"}
|
||||
}
|
||||
},
|
||||
"window": {
|
||||
"width": 1180,
|
||||
"height": 760,
|
||||
"singleton": true
|
||||
},
|
||||
"capabilities": ["speed-test", "diagnostics", "network"],
|
||||
"categories": ["network", "service"]
|
||||
}
|
||||
+2
-2
@@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=headscale
|
||||
PKG_VERSION:=0.29.3
|
||||
PKG_RELEASE:=8
|
||||
PKG_VERSION:=0.29.4
|
||||
PKG_RELEASE:=9
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/juanfont/headscale/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -12,7 +12,7 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
|
||||
PKG_NAME:=istoreenhance
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=0.8.0
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=9
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
ARCH_HEXCODE=8664
|
||||
@@ -41,7 +41,7 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=iStoreEnhance - Fix some net issues for istore
|
||||
DEPENDS:=@(arm||x86_64||aarch64)
|
||||
DEPENDS:=@(arm||x86_64||aarch64) +linkease-app-entry
|
||||
PKGARCH:=all
|
||||
URL:=https://www.kspeeder.com/
|
||||
endef
|
||||
@@ -68,6 +68,9 @@ if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/20-kspeeder.json 2>/dev/null)" = "/usr/share/kspeeder/kspeeder-plugin.json" ]; then
|
||||
rm -f /usr/share/linkeasefull/desktop-apps.d/20-kspeeder.json
|
||||
fi
|
||||
if [ "$$(readlink /usr/share/linkease/apps.d/20-kspeeder.json 2>/dev/null)" = "/usr/share/kspeeder/kspeeder-plugin.json" ]; then
|
||||
rm -f /usr/share/linkease/apps.d/20-kspeeder.json
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
@@ -79,7 +82,7 @@ define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/usr/share/kspeeder/www $(1)/usr/share/linkeasefull/desktop-apps.d
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults $(1)/usr/share/kspeeder/www $(1)/usr/share/linkease/apps.d $(1)/usr/share/linkeasefull/desktop-apps.d
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/iStoreEnhance.$(PKG_ARCH_ISTOREENHANCE) $(1)/usr/sbin/iStoreEnhance
|
||||
$(INSTALL_CONF) ./files/istoreenhance.config $(1)/etc/config/istoreenhance
|
||||
$(INSTALL_BIN) ./files/istoreenhance.init $(1)/etc/init.d/istoreenhance
|
||||
@@ -93,6 +96,7 @@ define Package/$(PKG_NAME)/install
|
||||
fi
|
||||
$(INSTALL_DATA) ./files/logo.png $(1)/usr/share/kspeeder/www/logo.png
|
||||
ln -sf /usr/share/kspeeder/kspeeder-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/20-kspeeder.json
|
||||
ln -sf /usr/share/kspeeder/kspeeder-plugin.json $(1)/usr/share/linkease/apps.d/20-kspeeder.json
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,43 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=kai-agent
|
||||
PKG_VERSION:=linkease-app-entry-runtime-v3.0.22
|
||||
PKG_RELEASE:=1
|
||||
PKG_SOURCE:=kai-agent-bundle-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kai-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/kai-agent-bundle-$(PKG_VERSION)
|
||||
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_USE_MIPS16:=0
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=System
|
||||
TITLE:=KAI OpenCode Agent bundle
|
||||
URL:=https://github.com/linkease/linkease-skills
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
Versioned local OpenCode instructions and skills for KAI on iStoreOS.
|
||||
endef
|
||||
|
||||
define Build/Configure
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/kai/agent-bundles/$(PKG_VERSION)
|
||||
$(CP) $(PKG_BUILD_DIR)/agents $(1)/usr/share/kai/agent-bundles/$(PKG_VERSION)/
|
||||
$(CP) $(PKG_BUILD_DIR)/skills $(1)/usr/share/kai/agent-bundles/$(PKG_VERSION)/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/home-prompts.json $(1)/usr/share/kai/agent-bundles/$(PKG_VERSION)/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/manifest.json $(1)/usr/share/kai/agent-bundles/$(PKG_VERSION)/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/opencode.json $(1)/usr/share/kai/agent-bundles/$(PKG_VERSION)/
|
||||
$(LN) agent-bundles/$(PKG_VERSION) $(1)/usr/share/kai/agent
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
+23
-4
@@ -10,10 +10,10 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_kai:=$(ARCH)
|
||||
|
||||
PKG_NAME:=kai
|
||||
PKG_VERSION:=0.0.6
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=linkease-app-entry-runtime-v3.0.22
|
||||
PKG_RELEASE:=6
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/linkease/istore-packages/releases/download/prebuilt/
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kai-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_VERSION)
|
||||
|
||||
@@ -27,6 +27,7 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=System
|
||||
SUBMENU:=
|
||||
TITLE:=KAI
|
||||
DEPENDS:=+kai_session +kai-agent +luci-lib-linkeaseauth +linkease-app-entry
|
||||
URL:=https://github.com/Carseason/openwrt-packages
|
||||
endef
|
||||
|
||||
@@ -38,6 +39,19 @@ define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/kai
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||
if [ "$$(readlink /usr/share/linkease/apps.d/05-kai-plugin.json 2>/dev/null)" = "/usr/share/kai/kai-plugin.json" ]; then
|
||||
rm -f /usr/share/linkease/apps.d/05-kai-plugin.json
|
||||
fi
|
||||
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/05-kai-plugin.json 2>/dev/null)" = "/usr/share/kai/kai-plugin.json" ]; then
|
||||
rm -f /usr/share/linkeasefull/desktop-apps.d/05-kai-plugin.json
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Configure
|
||||
endef
|
||||
|
||||
@@ -45,10 +59,15 @@ define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/init.d $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/libexec $(1)/etc/init.d $(1)/etc/config $(1)/usr/share/kai/www $(1)/usr/share/linkease/apps.d $(1)/usr/share/linkeasefull/desktop-apps.d
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/kai.$(PKG_ARCH_kai) $(1)/usr/sbin/kai_bin
|
||||
$(INSTALL_BIN) ./files/kai-session-launch $(1)/usr/libexec/kai-session-launch
|
||||
$(INSTALL_BIN) ./files/kai.init $(1)/etc/init.d/kai
|
||||
$(INSTALL_CONF) ./files/kai.config $(1)/etc/config/kai
|
||||
$(INSTALL_DATA) ./files/kai-plugin.json $(1)/usr/share/kai/kai-plugin.json
|
||||
$(INSTALL_DATA) ./files/logo.png $(1)/usr/share/kai/www/logo.png
|
||||
ln -sf /usr/share/kai/kai-plugin.json $(1)/usr/share/linkease/apps.d/05-kai-plugin.json
|
||||
ln -sf /usr/share/kai/kai-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/05-kai-plugin.json
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "kai",
|
||||
"name": "KAI",
|
||||
"icon": "logo.png",
|
||||
"staticRoot": "/usr/share/kai/www",
|
||||
"desktop": {
|
||||
"mode": "iframe",
|
||||
"target": {
|
||||
"scheme": "http",
|
||||
"hostMode": "request-host",
|
||||
"port": {
|
||||
"default": 8197,
|
||||
"keys": {
|
||||
"uci": "kai.@kai[0].port"
|
||||
}
|
||||
},
|
||||
"path": "/apps/kai/"
|
||||
},
|
||||
"access": {
|
||||
"scope": "lan",
|
||||
"blockedTitle": "请在局域网内打开 KAI",
|
||||
"blockedMessage": "KAI 运行在设备的独立服务端口,请连接设备所在局域网后再打开。"
|
||||
}
|
||||
},
|
||||
"standalone": {
|
||||
"basePath": "/apps/kai/",
|
||||
"externalOpen": {
|
||||
"enabled": true,
|
||||
"label": "在浏览器中打开 KAI"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
"providerOrder": ["uci"],
|
||||
"providers": {
|
||||
"uci": {
|
||||
"type": "uci"
|
||||
}
|
||||
}
|
||||
},
|
||||
"backend": {
|
||||
"type": "http",
|
||||
"transport": "tcp",
|
||||
"scheme": "http",
|
||||
"host": "127.0.0.1",
|
||||
"portFromUci": "kai.@kai[0].port",
|
||||
"defaultPort": 8197,
|
||||
"upstreamBasePath": "/apps/kai/",
|
||||
"pathMode": "preserve",
|
||||
"proxyMode": "app-base"
|
||||
},
|
||||
"window": {
|
||||
"width": 1180,
|
||||
"height": 760,
|
||||
"singleton": true
|
||||
},
|
||||
"capabilities": ["assistant", "chat"],
|
||||
"categories": ["other"]
|
||||
}
|
||||
Executable
+10
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
if [ "$#" -ne 1 ] || [ -z "$1" ]; then
|
||||
echo "usage: kai-session-launch WORKING_DIRECTORY" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cd "$1"
|
||||
exec /usr/sbin/kai_session serve --port 8196 --hostname 127.0.0.1
|
||||
@@ -2,3 +2,4 @@ config kai
|
||||
option 'enabled' '0'
|
||||
option 'data_dir' ''
|
||||
option 'port' '8197'
|
||||
option 'auth_mode' 'openwrt_luci'
|
||||
+14
-71
@@ -7,74 +7,12 @@ get_config() {
|
||||
config_get_bool enabled $1 enabled 1
|
||||
config_get data_dir $1 data_dir ""
|
||||
config_get port $1 port "8197"
|
||||
}
|
||||
|
||||
http_get() {
|
||||
local url="$1"
|
||||
if command -v uclient-fetch >/dev/null 2>&1; then
|
||||
uclient-fetch -O /dev/stdout "$url" 2>/dev/null
|
||||
return $?
|
||||
fi
|
||||
if command -v wget >/dev/null 2>&1; then
|
||||
wget -qO- "$url" 2>/dev/null
|
||||
return $?
|
||||
fi
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -fsL "$url" 2>/dev/null
|
||||
return $?
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
maybe_refresh_opencode_skills_cache() {
|
||||
[ -n "$data_dir" ] || return 0
|
||||
[ -n "$port" ] || return 0
|
||||
command -v jsonfilter >/dev/null 2>&1 || return 0
|
||||
|
||||
local cache_root="${data_dir}/cache/opencode/skills"
|
||||
local state_root="${data_dir}/state/opencode"
|
||||
local version_file="${state_root}/skills.manifest.version"
|
||||
local managed_file="${state_root}/skills.managed.list"
|
||||
local manifest_url="http://127.0.0.1:${port}/agentconf/skills/manifest.json"
|
||||
|
||||
[ -d "$cache_root" ] || mkdir -p "$cache_root"
|
||||
[ -d "$state_root" ] || mkdir -p "$state_root"
|
||||
|
||||
local manifest version prev_version skills
|
||||
manifest="$(http_get "$manifest_url")" || return 0
|
||||
version="$(printf '%s' "$manifest" | jsonfilter -e '@.version' 2>/dev/null)"
|
||||
[ -n "$version" ] || return 0
|
||||
|
||||
prev_version=""
|
||||
[ -f "$version_file" ] && prev_version="$(cat "$version_file" 2>/dev/null)"
|
||||
[ "$version" = "$prev_version" ] && return 0
|
||||
|
||||
logger -t kai "opencode skills manifest changed; refreshing managed skill cache"
|
||||
|
||||
if [ -f "$managed_file" ]; then
|
||||
while IFS= read -r s; do
|
||||
[ -n "$s" ] || continue
|
||||
rm -rf "${cache_root}/${s}"
|
||||
done <"$managed_file"
|
||||
fi
|
||||
|
||||
skills="$(printf '%s' "$manifest" | jsonfilter -e '@.skills[*]' 2>/dev/null)"
|
||||
if [ -n "$skills" ]; then
|
||||
printf '%s\n' "$skills" >"$managed_file"
|
||||
printf '%s\n' "$skills" | while IFS= read -r s; do
|
||||
[ -n "$s" ] || continue
|
||||
rm -rf "${cache_root}/${s}"
|
||||
done
|
||||
else
|
||||
: >"$managed_file"
|
||||
fi
|
||||
|
||||
printf '%s\n' "$version" >"$version_file"
|
||||
return 0
|
||||
config_get auth_mode $1 auth_mode "openwrt_luci"
|
||||
}
|
||||
|
||||
start_kai_bin(){
|
||||
procd_open_instance kai_bin
|
||||
procd_set_param env KAI_AUTH_MODE="$auth_mode"
|
||||
procd_set_param command /usr/sbin/kai_bin -a 0.0.0.0 -p "$port"
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
@@ -87,15 +25,21 @@ start_kai_session(){
|
||||
procd_open_instance kai_session
|
||||
local root="${data_dir}"
|
||||
local cwd="${data_dir}/cwd"
|
||||
local agent_root="/usr/share/kai/agent"
|
||||
if [ ! -f "${agent_root}/opencode.json" ] || [ ! -d "${agent_root}/skills" ]; then
|
||||
logger -t kai "local OpenCode Agent bundle is missing under ${agent_root}"
|
||||
return 1
|
||||
fi
|
||||
procd_set_param env \
|
||||
OPENCODE_CWD="${cwd}" \
|
||||
OPENCODE_CONFIG="http://127.0.0.1:${port}/agentconf/opencode.json" \
|
||||
OPENCODE_CONFIG="${agent_root}/opencode.json" \
|
||||
KAIPLUS_SKILLS_DIR="${agent_root}/skills" \
|
||||
ISTORE_AI_HELPER_BASE="http://127.0.0.1:${port}" \
|
||||
XDG_CACHE_HOME="${root}/cache" \
|
||||
XDG_DATA_HOME="${root}/data" \
|
||||
XDG_CONFIG_HOME="${root}/config" \
|
||||
XDG_STATE_HOME="${root}/state"
|
||||
procd_set_param command /usr/sbin/kai_session
|
||||
procd_append_param command serve --port "8196" --hostname "127.0.0.1"
|
||||
procd_set_param command /usr/libexec/kai-session-launch "${cwd}"
|
||||
procd_set_param file "${agent_root}/opencode.json" "${agent_root}/manifest.json"
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
procd_close_instance
|
||||
@@ -103,7 +47,7 @@ start_kai_session(){
|
||||
start_service() {
|
||||
config_load kai
|
||||
config_foreach get_config kai
|
||||
if [ $enabled != 1 ]; then
|
||||
if [ "$enabled" != 1 ]; then
|
||||
return 1
|
||||
fi
|
||||
if [ -z "${data_dir}" ]; then
|
||||
@@ -119,8 +63,7 @@ start_service() {
|
||||
mkdir_cwd "${data_dir}/data"
|
||||
mkdir_cwd "${data_dir}/config"
|
||||
mkdir_cwd "${data_dir}/state"
|
||||
maybe_refresh_opencode_skills_cache
|
||||
start_kai_session
|
||||
start_kai_session || return 1
|
||||
logger -t kai "Starting KAI Service Completed"
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -10,10 +10,10 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_kai_session:=$(ARCH)
|
||||
|
||||
PKG_NAME:=kai_session
|
||||
PKG_VERSION:=0.0.6
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=linkease-app-entry-runtime-v3.0.22
|
||||
PKG_RELEASE:=6
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/linkease/istore-packages/releases/download/prebuilt/
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kai-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_VERSION)
|
||||
|
||||
@@ -54,6 +54,7 @@ endef
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/kai_session.$(PKG_ARCH_kai_session) $(1)/usr/sbin/kai_session
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/rg.$(PKG_ARCH_kai_session) $(1)/usr/sbin/rg
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
+7
-2
@@ -10,7 +10,7 @@ PKG_ARCH_kaiplus:=$(ARCH)
|
||||
|
||||
PKG_NAME:=kaiplus
|
||||
PKG_VERSION:=1.0.9
|
||||
PKG_RELEASE:=13
|
||||
PKG_RELEASE:=14
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/kaiplus-runtime-v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
@@ -27,6 +27,7 @@ define Package/$(PKG_NAME)
|
||||
SUBMENU:=
|
||||
TITLE:=KaiPlus
|
||||
URL:=https://github.com/linkease/istore-packages
|
||||
DEPENDS:=+linkease-app-entry
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
@@ -43,6 +44,9 @@ if [ "$${IPKG_INSTROOT}" = "" ]; then
|
||||
if [ "$$(readlink /usr/share/linkeasefull/desktop-apps.d/00-kaiplus-plugin.json 2>/dev/null)" = "/usr/share/kaiplus/kaiplus-plugin.json" ]; then
|
||||
rm -f /usr/share/linkeasefull/desktop-apps.d/00-kaiplus-plugin.json
|
||||
fi
|
||||
if [ "$$(readlink /usr/share/linkease/apps.d/00-kaiplus-plugin.json 2>/dev/null)" = "/usr/share/kaiplus/kaiplus-plugin.json" ]; then
|
||||
rm -f /usr/share/linkease/apps.d/00-kaiplus-plugin.json
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
@@ -54,7 +58,7 @@ define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/share/kaiplus/www $(1)/usr/share/kaiplus/defaults $(1)/usr/share/kaiplus/helpers $(1)/usr/share/linkeasefull/desktop-apps.d $(1)/etc/init.d $(1)/etc/config
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/usr/share/kaiplus/www $(1)/usr/share/kaiplus/defaults $(1)/usr/share/kaiplus/helpers $(1)/usr/share/linkease/apps.d $(1)/usr/share/linkeasefull/desktop-apps.d $(1)/etc/init.d $(1)/etc/config
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/kaiplus_bin.$(PKG_ARCH_kaiplus) $(1)/usr/sbin/kaiplus_bin
|
||||
$(CP) $(PKG_BUILD_DIR)/www/* $(1)/usr/share/kaiplus/www/
|
||||
$(INSTALL_DATA) ./files/logo.png $(1)/usr/share/kaiplus/www/logo.png
|
||||
@@ -62,6 +66,7 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/helpers/kaiplus_workspace_tool.* $(1)/usr/share/kaiplus/helpers/
|
||||
$(INSTALL_DATA) ./files/kaiplus-plugin.json $(1)/usr/share/kaiplus/kaiplus-plugin.json
|
||||
ln -sf /usr/share/kaiplus/kaiplus-plugin.json $(1)/usr/share/linkeasefull/desktop-apps.d/00-kaiplus-plugin.json
|
||||
ln -sf /usr/share/kaiplus/kaiplus-plugin.json $(1)/usr/share/linkease/apps.d/00-kaiplus-plugin.json
|
||||
$(INSTALL_BIN) ./files/kaiplus.init $(1)/etc/init.d/kaiplus
|
||||
$(INSTALL_CONF) ./files/kaiplus.config $(1)/etc/config/kaiplus
|
||||
endef
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
#
|
||||
# Copyright (C) 2026 iStoreOS
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=linkease-app-entry
|
||||
# This lightweight runtime is released independently from the full desktop.
|
||||
PKG_VERSION:=linkease-app-entry-runtime-v3.0.22
|
||||
PKG_RELEASE:=1
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
ARCH_HEXCODE=8664
|
||||
LINKEASE_RUNTIME_ARCH:=amd64
|
||||
PKG_HASH:=skip
|
||||
else ifeq ($(ARCH),aarch64)
|
||||
ARCH_HEXCODE=aa64
|
||||
LINKEASE_RUNTIME_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=d3d23965623e4182de554dde64418bb9a20c9078
|
||||
PKG_SOURCE:=linkease-app-entry-runtime-$(PKG_VERSION)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-app-entry-runtime-v$(PKG_VERSION)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-app-entry-runtime-$(PKG_VERSION)-linux-$(LINKEASE_RUNTIME_ARCH)
|
||||
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_USE_MIPS16:=0
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=LinkEase application entry arbiter and fallback gateway
|
||||
DEPENDS:=@(x86_64||aarch64) +ca-bundle +luci-lib-linkeaseauth
|
||||
PKGARCH:=all
|
||||
URL:=https://www.linkease.com/
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
Owns the shared /apps listener, launches LinkEaseFull directly when healthy,
|
||||
and runs the manifest-driven fallback gateway only when LinkEaseFull is not available.
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/linkease_app_entry
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
if [ -z "$${IPKG_INSTROOT}" ]; then
|
||||
[ -f /etc/uci-defaults/linkease-app-entry ] && /etc/uci-defaults/linkease-app-entry && rm -f /etc/uci-defaults/linkease-app-entry
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Configure
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin $(1)/etc/config $(1)/etc/init.d $(1)/etc/uci-defaults
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/bin/linkease-app-entry $(1)/usr/sbin/linkease-app-entry
|
||||
$(INSTALL_CONF) ./files/linkease-app-entry.config $(1)/etc/config/linkease_app_entry
|
||||
$(INSTALL_BIN) ./files/linkease-app-entry.init $(1)/etc/init.d/linkease-app-entry
|
||||
$(INSTALL_BIN) ./files/linkease-app-entry.uci-default $(1)/etc/uci-defaults/linkease-app-entry
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,7 @@
|
||||
config entry 'main'
|
||||
option enabled '1'
|
||||
option listen '127.0.0.1:19290'
|
||||
option ready_timeout '75s'
|
||||
option max_primary_failures '3'
|
||||
option crash_window '60s'
|
||||
option state_file '/var/run/linkease-app-entry/state.json'
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
STOP=11
|
||||
USE_PROCD=1
|
||||
|
||||
ENTRY=/usr/sbin/linkease-app-entry
|
||||
PRIMARY=/usr/bin/linkease-full
|
||||
FORCE_GATEWAY=/var/run/linkease-app-entry/force-gateway
|
||||
DATA_ROOT_WAIT_SECONDS=${LINKEASEFULL_DATA_ROOT_WAIT_SECONDS:-60}
|
||||
|
||||
append_primary_arg() {
|
||||
procd_append_param command --primary-arg "$1"
|
||||
}
|
||||
|
||||
primary_enabled() {
|
||||
[ -x "$PRIMARY" ] || return 1
|
||||
[ ! -e "$FORCE_GATEWAY" ] || return 1
|
||||
enabled="$(uci -q get linkeasefull.@linkeasefull[0].enabled 2>/dev/null || echo 1)"
|
||||
[ "$enabled" = 1 ]
|
||||
}
|
||||
|
||||
ensure_linkease_config() {
|
||||
if [ ! -f /etc/config/linkease ]; then
|
||||
touch /etc/config/linkease || return 1
|
||||
fi
|
||||
if ! uci -q get linkease.@linkease[0] >/dev/null; then
|
||||
uci -q add linkease linkease >/dev/null || return 1
|
||||
uci -q set linkease.@linkease[0].enabled='0' || return 1
|
||||
uci -q set linkease.@linkease[0].port='8897' || return 1
|
||||
uci -q set linkease.@linkease[0].allowPublic='0' || return 1
|
||||
uci -q commit linkease || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_linkeasefull_runtime() {
|
||||
local data_root_parent data_root recycle_root wait_left
|
||||
ensure_linkease_config || true
|
||||
data_root_parent="$(uci -q get linkease.@linkease[0].local_home 2>/dev/null || true)"
|
||||
case "$data_root_parent" in
|
||||
""|/tmp|/tmp/*|/var|/var/*|/run|/run/*|/dev|/dev/*|/proc|/proc/*|/sys|/sys/*|/rom|/rom/*|/overlay|/overlay/*) data_root_parent="" ;;
|
||||
esac
|
||||
if [ -n "$data_root_parent" ]; then
|
||||
wait_left="$DATA_ROOT_WAIT_SECONDS"
|
||||
case "$wait_left" in ""|*[!0-9]*) wait_left=60 ;; esac
|
||||
while [ "$wait_left" -gt 0 ] && [ ! -d "$data_root_parent" ]; do
|
||||
sleep 1
|
||||
wait_left=$((wait_left - 1))
|
||||
done
|
||||
[ -d "$data_root_parent" ] || data_root_parent=""
|
||||
fi
|
||||
if [ -n "$data_root_parent" ]; then
|
||||
data_root="$data_root_parent/.linkease_data"
|
||||
recycle_root="$data_root_parent/.linkease_recycle"
|
||||
mkdir -p "$data_root/users/admin" "$data_root/system" "$data_root/tmp" "$data_root/config" "$data_root/apptunnel" "$data_root/mountremote" "$recycle_root"
|
||||
fi
|
||||
mkdir -p /tmp/linkeasefull/logs
|
||||
rm -f /var/run/linkease.sock
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load linkease_app_entry
|
||||
config_get_bool entry_enabled main enabled 1
|
||||
[ "$entry_enabled" = 1 ] || return 1
|
||||
config_get listen main listen "127.0.0.1:19290"
|
||||
config_get ready_timeout main ready_timeout "75s"
|
||||
config_get max_failures main max_primary_failures "3"
|
||||
config_get crash_window main crash_window "60s"
|
||||
config_get state_file main state_file "/var/run/linkease-app-entry/state.json"
|
||||
|
||||
mkdir -p /var/run/linkease-app-entry
|
||||
primary=0
|
||||
primary_enabled && primary=1
|
||||
[ "$primary" = 1 ] && prepare_linkeasefull_runtime
|
||||
|
||||
procd_open_instance entry
|
||||
procd_set_param limits nofile="65535 65535"
|
||||
procd_set_param command "$ENTRY" supervisor \
|
||||
--listen "$listen" \
|
||||
--primary "$PRIMARY" \
|
||||
--primary-enabled="$primary" \
|
||||
--fallback "$ENTRY" \
|
||||
--fallback-arg gateway \
|
||||
--state-file "$state_file" \
|
||||
--ready-timeout "$ready_timeout" \
|
||||
--max-primary-failures "$max_failures" \
|
||||
--crash-window "$crash_window"
|
||||
|
||||
append_primary_arg --host
|
||||
append_primary_arg 127.0.0.1
|
||||
append_primary_arg --port
|
||||
append_primary_arg 19290
|
||||
append_primary_arg --mode
|
||||
append_primary_arg release
|
||||
append_primary_arg --basePath
|
||||
append_primary_arg /apps/
|
||||
append_primary_arg --deviceAddr
|
||||
append_primary_arg 0.0.0.0:8897
|
||||
append_primary_arg --localApi
|
||||
append_primary_arg /var/run/linkease.sock
|
||||
append_primary_arg --supplierCode
|
||||
append_primary_arg linkease
|
||||
|
||||
procd_set_param env \
|
||||
"LINKEASE_AUTH_PROVIDER=openwrt" \
|
||||
"KAIPLUS_ENABLED=0"
|
||||
procd_set_param respawn 3600 5 0
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "linkease_app_entry" "linkeasefull"
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /lib/functions.sh
|
||||
|
||||
mapping='/apps=http://127.0.0.1:19290'
|
||||
found=0
|
||||
|
||||
# During upgrade the old linkeasefull init may still own 19290. Stop that
|
||||
# legacy procd instance before starting the single-owner entry module.
|
||||
if grep -q 'procd_open_instance linkeasefull' /etc/init.d/linkeasefull 2>/dev/null; then
|
||||
/etc/init.d/linkeasefull stop >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
check_mapping() {
|
||||
[ "$1" = "$mapping" ] && found=1
|
||||
}
|
||||
|
||||
config_load uhttpd
|
||||
config_list_foreach main proxy_prefix check_mapping
|
||||
uci -q show uhttpd.main.proxy_prefix 2>/dev/null | grep -Fq "$mapping" && found=1
|
||||
if [ "$found" != 1 ]; then
|
||||
uci -q add_list "uhttpd.main.proxy_prefix=$mapping"
|
||||
uci -q commit uhttpd
|
||||
/etc/init.d/uhttpd reload >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
/etc/init.d/linkease-app-entry enable
|
||||
/etc/init.d/linkease-app-entry restart >/dev/null 2>&1 || true
|
||||
exit 0
|
||||
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease-common-bin
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=33
|
||||
PKG_RELEASE:=34
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=2e0e85147d88db69256be9b38f7b3128d0126877
|
||||
PKG_SOURCE_VERSION:=8e06ab67451edac1442855713ca3256c72f34be9
|
||||
PKG_SOURCE:=linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-common-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
+2
-2
@@ -12,7 +12,7 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
PKG_NAME:=linkease
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=1.7.5
|
||||
PKG_RELEASE:=36
|
||||
PKG_RELEASE:=37
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=2e0e85147d88db69256be9b38f7b3128d0126877
|
||||
PKG_SOURCE_VERSION:=8e06ab67451edac1442855713ca3256c72f34be9
|
||||
PKG_SOURCE:=linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkease-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-bin-$(PKG_SOURCE_DATE)-linux-$(PKG_ARCH_LINKEASE)
|
||||
|
||||
@@ -10,8 +10,8 @@ PKG_ARCH_LINKEASE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=linkeasefull
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=3.0.20
|
||||
PKG_RELEASE:=35
|
||||
PKG_SOURCE_DATE:=3.0.22
|
||||
PKG_RELEASE:=36
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=2e0e85147d88db69256be9b38f7b3128d0126877
|
||||
PKG_SOURCE_VERSION:=8e06ab67451edac1442855713ca3256c72f34be9
|
||||
PKG_SOURCE:=linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/istoreos/istoreos-app-hub/releases/download/linkeasefull-runtime-v$(PKG_SOURCE_DATE)/
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/linkease-runtime-$(PKG_SOURCE_DATE)-linux-$(LINKEASE_RUNTIME_ARCH)
|
||||
@@ -39,7 +39,7 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=LinkEase Full - the local web desktop
|
||||
DEPENDS:=@(x86_64||aarch64) +linkease-common-bin +ca-bundle +cifsmount +kmod-fs-cifs
|
||||
DEPENDS:=@(x86_64||aarch64) +linkease-app-entry +linkease-common-bin +ca-bundle +cifsmount +kmod-fs-cifs
|
||||
PKGARCH:=all
|
||||
URL:=https://www.linkease.com/
|
||||
endef
|
||||
|
||||
@@ -1,123 +1,61 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
# Compatibility control shim. linkease-app-entry is the only owner of port
|
||||
# 19290 and the only procd instance allowed to launch linkease-full.
|
||||
|
||||
START=100
|
||||
STOP=10
|
||||
USE_PROCD=1
|
||||
|
||||
PROG=/usr/bin/linkease-full
|
||||
LINKEASE_FULL_PORT=19290
|
||||
LINKEASE_BASE_PATH=/apps/
|
||||
LINKEASE_LEGACY_ADDR=0.0.0.0:8897
|
||||
LOCAL_API=/var/run/linkease.sock
|
||||
DATA_ROOT_WAIT_SECONDS=${LINKEASEFULL_DATA_ROOT_WAIT_SECONDS:-60}
|
||||
ENTRY_INIT=/etc/init.d/linkease-app-entry
|
||||
FORCE_GATEWAY=/var/run/linkease-app-entry/force-gateway
|
||||
STATE_FILE=/var/run/linkease-app-entry/state.json
|
||||
|
||||
ensure_linkease_config() {
|
||||
if [ ! -f /etc/config/linkease ]; then
|
||||
touch /etc/config/linkease || return 1
|
||||
fi
|
||||
if ! uci -q get linkease.@linkease[0] >/dev/null; then
|
||||
uci -q add linkease linkease >/dev/null || return 1
|
||||
uci -q set linkease.@linkease[0].enabled='0' || return 1
|
||||
uci -q set linkease.@linkease[0].port='8897' || return 1
|
||||
uci -q set linkease.@linkease[0].allowPublic='0' || return 1
|
||||
uci -q commit linkease || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_allowed_data_root_parent() {
|
||||
[ -n "$1" ] || return 1
|
||||
case "$1" in
|
||||
/tmp|/tmp/*|/var|/var/*|/run|/run/*|/dev|/dev/*|/proc|/proc/*|/sys|/sys/*|/rom|/rom/*|/overlay|/overlay/*) return 1 ;;
|
||||
esac
|
||||
return 0
|
||||
}
|
||||
|
||||
wait_for_data_root_parent() {
|
||||
is_allowed_data_root_parent "$1" || return 1
|
||||
wait_left="$DATA_ROOT_WAIT_SECONDS"
|
||||
case "$wait_left" in
|
||||
""|*[!0-9]*) wait_left=60 ;;
|
||||
esac
|
||||
while [ "$wait_left" -gt 0 ]; do
|
||||
[ -d "$1" ] && return 0
|
||||
sleep 1
|
||||
wait_left=$((wait_left - 1))
|
||||
done
|
||||
[ -d "$1" ]
|
||||
}
|
||||
|
||||
is_persistent_data_root_parent() {
|
||||
is_allowed_data_root_parent "$1" || return 1
|
||||
[ -d "$1" ] || return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
read_config() {
|
||||
section="$1"
|
||||
config_get_bool enabled "$section" enabled 1
|
||||
ensure_linkease_config || true
|
||||
data_root_parent="$(uci -q get linkease.@linkease[0].local_home 2>/dev/null || true)"
|
||||
|
||||
if [ -n "$data_root_parent" ] && ! wait_for_data_root_parent "$data_root_parent"; then
|
||||
data_root_parent=""
|
||||
fi
|
||||
if [ -n "$data_root_parent" ] && ! is_persistent_data_root_parent "$data_root_parent"; then
|
||||
data_root_parent=""
|
||||
fi
|
||||
if [ -n "$data_root_parent" ]; then
|
||||
data_root="$data_root_parent/.linkease_data"
|
||||
recycle_root="$data_root_parent/.linkease_recycle"
|
||||
fi
|
||||
log_root="/tmp/linkeasefull/logs"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load linkeasefull
|
||||
config_foreach read_config linkeasefull
|
||||
[ "$enabled" = "1" ] || return 1
|
||||
[ -x "$PROG" ] || return 1
|
||||
|
||||
rm -f "$LOCAL_API"
|
||||
if [ -n "$data_root_parent" ]; then
|
||||
mkdir -p "$data_root/users/admin" "$data_root/system" "$data_root/tmp" "$recycle_root"
|
||||
mkdir -p "$data_root/config" "$data_root/apptunnel" "$data_root/mountremote"
|
||||
sync_force_gateway() {
|
||||
mkdir -p "$(dirname "$FORCE_GATEWAY")"
|
||||
enabled="$(uci -q get linkeasefull.@linkeasefull[0].enabled 2>/dev/null || echo 0)"
|
||||
if [ "$enabled" = 1 ]; then
|
||||
rm -f "$FORCE_GATEWAY"
|
||||
else
|
||||
echo "linkeasefull: data root is not initialized; starting for first-run setup" >&2
|
||||
: > "$FORCE_GATEWAY"
|
||||
fi
|
||||
mkdir -p "$log_root"
|
||||
|
||||
# Do not set LINKEASE_CONFIG on OpenWrt. That variable belongs to
|
||||
# Docker/file-backend deployments where users explicitly mount
|
||||
# LINKEASE_CONFIG/preconfig.data. OpenWrt binding state is authoritative in
|
||||
# UCI and must be accessed through /usr/sbin/linkease-config.sh. This is
|
||||
# intentional: the user's data disk may be replaced, and the device must
|
||||
# still be able to recover its binding from flash-backed OpenWrt config.
|
||||
#
|
||||
# Do not export LINKEASE_APPTUNNEL_PRECONFIG_SCRIPT or
|
||||
# LINKEASE_APPTUNNEL_DEFAULT_ROOT_DIR here. Different devices have different
|
||||
# storage roots, and hard-coded env wiring can pin apptunnel to a removed
|
||||
# disk. The embedded clientruntime must discover linkease-config.sh and call
|
||||
# local_load/local_save-compatible business APIs for the current root.
|
||||
|
||||
procd_open_instance linkeasefull
|
||||
procd_set_param limits nofile="65535 65535"
|
||||
procd_set_param command "$PROG" \
|
||||
--host "0.0.0.0" \
|
||||
--port "$LINKEASE_FULL_PORT" \
|
||||
--mode "release" \
|
||||
--basePath "$LINKEASE_BASE_PATH" \
|
||||
--deviceAddr "$LINKEASE_LEGACY_ADDR" \
|
||||
--localApi "$LOCAL_API" \
|
||||
--supplierCode "linkease"
|
||||
procd_set_param env \
|
||||
"LINKEASE_AUTH_PROVIDER=openwrt" \
|
||||
"KAIPLUS_ENABLED=0"
|
||||
procd_set_param respawn
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "linkeasefull"
|
||||
start() {
|
||||
mkdir -p "$(dirname "$FORCE_GATEWAY")"
|
||||
rm -f "$FORCE_GATEWAY"
|
||||
uci -q set linkeasefull.@linkeasefull[0].enabled='1'
|
||||
uci -q commit linkeasefull
|
||||
"$ENTRY_INIT" restart
|
||||
}
|
||||
|
||||
stop() {
|
||||
mkdir -p "$(dirname "$FORCE_GATEWAY")"
|
||||
: > "$FORCE_GATEWAY"
|
||||
"$ENTRY_INIT" restart
|
||||
}
|
||||
|
||||
restart() {
|
||||
start
|
||||
}
|
||||
|
||||
reload() {
|
||||
sync_force_gateway
|
||||
"$ENTRY_INIT" restart
|
||||
}
|
||||
|
||||
running() {
|
||||
[ -r "$STATE_FILE" ] && grep -q '"active":"linkeasefull"' "$STATE_FILE"
|
||||
}
|
||||
|
||||
status() {
|
||||
if running; then
|
||||
echo "running"
|
||||
return 0
|
||||
fi
|
||||
if [ -r "$STATE_FILE" ]; then
|
||||
cat "$STATE_FILE"
|
||||
else
|
||||
echo "stopped"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
@@ -30,6 +30,9 @@ uci -q batch <<-EOF >/dev/null
|
||||
commit ucitrack
|
||||
EOF
|
||||
|
||||
/etc/init.d/linkeasefull enable
|
||||
/etc/init.d/linkeasefull disable
|
||||
/etc/init.d/linkease-app-entry enable
|
||||
rm -f /var/run/linkease-app-entry/force-gateway
|
||||
/etc/init.d/linkease-app-entry restart >/dev/null 2>&1 || true
|
||||
|
||||
exit 0
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=AgentFlow
|
||||
PKG_VERSION:=1.0.1
|
||||
PKG_RELEASE:=7
|
||||
LUCI_DEPENDS:=+agentflow +luci-compat +luci-lib-taskd
|
||||
PKG_RELEASE:=8
|
||||
LUCI_DEPENDS:=+agentflow +luci-compat +luci-lib-taskd +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
|
||||
@@ -1,18 +1,5 @@
|
||||
local http = require "luci.http"
|
||||
|
||||
module("luci.controller.agentflow", package.seeall)
|
||||
|
||||
local APPS_PROXY_PREFIX = "/apps=http://127.0.0.1:19290"
|
||||
local DEFAULT_BASE_PATH = "/apps/agentflow/"
|
||||
local DEFAULT_PORT = 9000
|
||||
local AGENTS = {
|
||||
{ id = "codexcli", package = "@openai/codex" },
|
||||
{ id = "claude-code", package = "@anthropic-ai/claude-code" },
|
||||
{ id = "opencode", package = "opencode-ai" },
|
||||
{ id = "kimi", package = "@moonshot-ai/kimi-code" },
|
||||
{ id = "reasonix", package = "reasonix" }
|
||||
}
|
||||
|
||||
function index()
|
||||
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
|
||||
local open = entry({"admin", "services", "agentflow", "open"}, call("agentflow_open"))
|
||||
@@ -20,277 +7,29 @@ function index()
|
||||
open.dependent = false
|
||||
open.sysauth = false
|
||||
|
||||
if not nixio.fs.access("/etc/config/agentflow") then
|
||||
return
|
||||
end
|
||||
|
||||
local install = entry({"admin", "services", "agentflow", "agent_install"}, call("agentflow_agent_install"))
|
||||
install.leaf = true
|
||||
|
||||
local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100)
|
||||
page.dependent = true
|
||||
if not nixio.fs.access("/etc/config/agentflow") then return end
|
||||
entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100).dependent = true
|
||||
end
|
||||
|
||||
local function write_json(obj)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(obj)
|
||||
end
|
||||
|
||||
local function require_post_csrf()
|
||||
local function compat()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
local context = dispatcher.context or {}
|
||||
local expected = context.authtoken or context.token
|
||||
|
||||
if (http.getenv("REQUEST_METHOD") or "") ~= "POST" then
|
||||
http.status(405, "Method Not Allowed")
|
||||
write_json({ ok = false, error = "method not allowed" })
|
||||
return false
|
||||
end
|
||||
|
||||
if not context.authsession or not expected or http.formvalue("token") ~= expected then
|
||||
http.status(403, "Forbidden")
|
||||
write_json({ ok = false, error = "invalid csrf token" })
|
||||
return false
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
local function uhttpd_has_apps_proxy_prefix()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
|
||||
|
||||
for _, mapping in ipairs(mappings) do
|
||||
if mapping == APPS_PROXY_PREFIX then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function uhttpd_supports_proxy_prefix()
|
||||
local sys = require "luci.sys"
|
||||
return sys.call("grep -qr 'proxy_prefix' /etc/init.d/uhttpd /lib/functions /usr/share/uhttpd 2>/dev/null") == 0
|
||||
end
|
||||
|
||||
local function uhttpd_apps_proxy_available()
|
||||
return uhttpd_supports_proxy_prefix() and uhttpd_has_apps_proxy_prefix()
|
||||
end
|
||||
|
||||
local function linkeasefull_running()
|
||||
local sys = require "luci.sys"
|
||||
return sys.call("[ -x /etc/init.d/linkeasefull ] && /etc/init.d/linkeasefull running >/dev/null 2>&1") == 0
|
||||
end
|
||||
|
||||
local function normalized_base_path(path)
|
||||
path = path or DEFAULT_BASE_PATH
|
||||
if path:sub(1, 1) ~= "/" then
|
||||
path = "/" .. path
|
||||
end
|
||||
if path:sub(-1) ~= "/" then
|
||||
path = path .. "/"
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
local function authority_host(authority)
|
||||
if not authority or authority == "" then
|
||||
return ""
|
||||
end
|
||||
if authority:sub(1, 1) == "[" then
|
||||
return authority:match("^%[([^%]]+)%]") or ""
|
||||
end
|
||||
return authority:match("^([^:]+)") or authority
|
||||
end
|
||||
|
||||
local function url_authority(host, port)
|
||||
if not host or host == "" then
|
||||
host = "127.0.0.1"
|
||||
end
|
||||
if host:find(":") and host:sub(1, 1) ~= "[" then
|
||||
host = "[" .. host .. "]"
|
||||
end
|
||||
return host .. ":" .. tostring(port)
|
||||
end
|
||||
|
||||
local function request_or_lan_host()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local host = authority_host(http.getenv("HTTP_HOST") or "")
|
||||
if host ~= "" then
|
||||
return host
|
||||
end
|
||||
return uci:get("network", "lan", "ipaddr") or "127.0.0.1"
|
||||
end
|
||||
|
||||
local function agentflow_config()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = tonumber(uci:get_first("agentflow", "agentflow", "port")) or DEFAULT_PORT
|
||||
if port < 1 or port > 65535 then
|
||||
port = DEFAULT_PORT
|
||||
end
|
||||
local base_path = normalized_base_path(uci:get_first("agentflow", "agentflow", "base_path"))
|
||||
return port, base_path
|
||||
end
|
||||
|
||||
local function agentflow_entry_url()
|
||||
local port, base_path = agentflow_config()
|
||||
if linkeasefull_running() and uhttpd_apps_proxy_available() then
|
||||
return base_path
|
||||
end
|
||||
return "http://" .. url_authority(request_or_lan_host(), port) .. base_path
|
||||
end
|
||||
|
||||
local function node_modules_root()
|
||||
local sys = require "luci.sys"
|
||||
local util = require "luci.util"
|
||||
local script = table.concat({
|
||||
'. /lib/functions/mise.sh 2>/dev/null || exit 1',
|
||||
'istore_runtime_env >/dev/null 2>&1 || exit 1',
|
||||
'mise_bin="$(command -v mise-istore || command -v mise)"',
|
||||
'[ -n "$mise_bin" ] || exit 1',
|
||||
'node_dir="$("$mise_bin" where node@lts 2>/dev/null)"',
|
||||
'[ -n "$node_dir" ] || exit 1',
|
||||
'printf "%s/lib/node_modules" "$node_dir"'
|
||||
}, "; ")
|
||||
local root = sys.exec("/bin/sh -c " .. util.shellquote(script)) or ""
|
||||
root = root:match("^%s*(.-)%s*$")
|
||||
if root == "" or root:sub(1, 1) ~= "/" then
|
||||
return nil
|
||||
end
|
||||
return root
|
||||
end
|
||||
|
||||
local function agent_statuses()
|
||||
local fs = require "nixio.fs"
|
||||
local jsonc = require "luci.jsonc"
|
||||
local root = node_modules_root()
|
||||
local statuses = {}
|
||||
|
||||
for _, agent in ipairs(AGENTS) do
|
||||
local status = { id = agent.id, installed = false }
|
||||
if root then
|
||||
local package_file = root .. "/" .. agent.package .. "/package.json"
|
||||
local package_data = fs.readfile(package_file)
|
||||
local package_json = package_data and jsonc.parse(package_data) or nil
|
||||
if type(package_json) == "table" then
|
||||
status.installed = true
|
||||
status.version = package_json.version
|
||||
end
|
||||
end
|
||||
statuses[#statuses + 1] = status
|
||||
end
|
||||
|
||||
return statuses, root ~= nil
|
||||
return require("luci.model.linkease.apps_compat").new({
|
||||
http = require "luci.http",
|
||||
resolver = require("luci.model.linkease.apps_openwrt").new(),
|
||||
auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth")
|
||||
})
|
||||
end
|
||||
|
||||
function agentflow_status()
|
||||
local sys = require "luci.sys"
|
||||
local port, base_path = agentflow_config()
|
||||
local entry_url = agentflow_entry_url()
|
||||
local agents, agents_available = agent_statuses()
|
||||
|
||||
local status = {
|
||||
running = (sys.call("pidof agentflow >/dev/null") == 0),
|
||||
port = port,
|
||||
base_path = base_path,
|
||||
entry_url = entry_url,
|
||||
proxy_prefix_supported = uhttpd_supports_proxy_prefix(),
|
||||
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
|
||||
linkeasefull_running = linkeasefull_running(),
|
||||
agents_available = agents_available,
|
||||
agents = agents
|
||||
}
|
||||
write_json(status)
|
||||
end
|
||||
|
||||
function agentflow_agent_install()
|
||||
local fs = require "nixio.fs"
|
||||
local jsonc = require "luci.jsonc"
|
||||
local sys = require "luci.sys"
|
||||
local util = require "luci.util"
|
||||
local task_id = "agentflow-agent-install"
|
||||
local task_script = "/tmp/agentflow-agent-install.sh"
|
||||
local installer_url = "https://fw.koolcenter.com/binary/geili/agentflow/releases/installapp/installapp-mise.sh"
|
||||
local supported_agents = {}
|
||||
for _, supported_agent in ipairs(AGENTS) do
|
||||
supported_agents[supported_agent.id] = true
|
||||
end
|
||||
|
||||
if not require_post_csrf() then
|
||||
return
|
||||
end
|
||||
|
||||
local agent = http.formvalue("agent") or ""
|
||||
if not supported_agents[agent] then
|
||||
write_json({ ok = false, error = "unsupported agent" })
|
||||
return
|
||||
end
|
||||
if not fs.access("/etc/init.d/tasks") then
|
||||
write_json({ ok = false, error = "taskd is not available" })
|
||||
return
|
||||
end
|
||||
if not fs.access("/lib/functions/mise.sh") then
|
||||
write_json({ ok = false, error = "mise environment helper is not available" })
|
||||
return
|
||||
end
|
||||
|
||||
local task_status = sys.exec("/etc/init.d/tasks task_status " .. task_id .. " 2>/dev/null")
|
||||
local status = jsonc.parse(task_status) or {}
|
||||
local running = type(status) == "table" and status.running
|
||||
if running == true or running == 1 or running == "1" or running == "true" then
|
||||
write_json({ ok = true, busy = true, task_id = task_id })
|
||||
return
|
||||
end
|
||||
|
||||
local install_script = table.concat({
|
||||
"set -e",
|
||||
"agent=" .. util.shellquote(agent),
|
||||
"installer_url=" .. util.shellquote(installer_url),
|
||||
'installer="/tmp/agentflow-installapp-mise.$$"',
|
||||
'cleanup() { rm -f "$installer" "$0"; }',
|
||||
"trap cleanup 0 HUP INT TERM",
|
||||
'. /lib/functions/mise.sh',
|
||||
'if ! istore_runtime_env; then echo "[agentflow] Failed to initialize the shared runtime environment" >&2; exit 1; fi',
|
||||
"set -u",
|
||||
"export MISE_YES=1",
|
||||
'echo "[agentflow] Downloading installer: $installer_url"',
|
||||
'if command -v wget >/dev/null 2>&1; then',
|
||||
'\twget -O "$installer" "$installer_url"',
|
||||
'elif command -v curl >/dev/null 2>&1; then',
|
||||
'\tcurl -fL -o "$installer" "$installer_url"',
|
||||
'elif command -v uclient-fetch >/dev/null 2>&1; then',
|
||||
'\tuclient-fetch -O "$installer" "$installer_url"',
|
||||
"else",
|
||||
'\techo "[agentflow] No HTTPS download tool is available" >&2',
|
||||
"\texit 1",
|
||||
"fi",
|
||||
'if [ ! -s "$installer" ]; then echo "[agentflow] Failed to download the agent installer" >&2; exit 1; fi',
|
||||
'chmod 0700 "$installer"',
|
||||
'echo "[agentflow] Running installapp-mise.sh for $agent in $HOME"',
|
||||
'/bin/sh "$installer" "$agent"'
|
||||
}, "\n")
|
||||
if not fs.writefile(task_script, install_script .. "\n") then
|
||||
write_json({ ok = false, error = "failed to create install task", task_id = task_id })
|
||||
return
|
||||
end
|
||||
if sys.call("chmod 0600 " .. util.shellquote(task_script)) ~= 0 then
|
||||
fs.unlink(task_script)
|
||||
write_json({ ok = false, error = "failed to secure install task", task_id = task_id })
|
||||
return
|
||||
end
|
||||
|
||||
local command = "/bin/sh " .. util.shellquote(task_script)
|
||||
local rc = sys.call("/etc/init.d/tasks task_add " .. task_id .. " " .. util.shellquote(command) .. " >/dev/null 2>&1")
|
||||
if rc ~= 0 then
|
||||
fs.unlink(task_script)
|
||||
write_json({ ok = false, error = "failed to start install task", task_id = task_id })
|
||||
return
|
||||
end
|
||||
|
||||
write_json({ ok = true, task_id = task_id })
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
compat():legacy_status("agentflow", {
|
||||
running = sys.call("pidof agentflow >/dev/null") == 0,
|
||||
port = uci:get_first("agentflow", "agentflow", "port") or "9000",
|
||||
base_path = uci:get_first("agentflow", "agentflow", "base_path") or "/apps/agentflow/"
|
||||
})
|
||||
end
|
||||
|
||||
function agentflow_open()
|
||||
local entry_url = agentflow_entry_url()
|
||||
http.redirect(entry_url)
|
||||
compat():open("agentflow")
|
||||
end
|
||||
@@ -9,21 +9,23 @@ class AgentFlowLuciOpenContractTest(unittest.TestCase):
|
||||
def read(self, relative):
|
||||
return (APP_DIR / relative).read_text(encoding="utf-8")
|
||||
|
||||
def test_luci_open_matches_linkease_proxy_pattern(self):
|
||||
def test_luci_open_delegates_to_shared_apps_entry(self):
|
||||
makefile = self.read("Makefile")
|
||||
controller = self.read("luasrc/controller/agentflow.lua")
|
||||
status = self.read("luasrc/view/agentflow/status.htm")
|
||||
|
||||
self.assertNotIn("+luci-lib-linkeaseauth", makefile)
|
||||
self.assertIn("+luci-lib-linkeaseauth", makefile)
|
||||
self.assertIn("+linkease-app-entry", makefile)
|
||||
self.assertIn('entry({"admin", "services", "agentflow", "open"}', controller)
|
||||
self.assertIn("open.sysauth = false", controller)
|
||||
self.assertIn("function agentflow_open()", controller)
|
||||
self.assertIn("uhttpd_apps_proxy_available()", controller)
|
||||
self.assertIn("linkeasefull_running()", controller)
|
||||
self.assertIn("return base_path", controller)
|
||||
self.assertIn("url_authority(request_or_lan_host(), port)", controller)
|
||||
self.assertIn("http.redirect(entry_url)", controller)
|
||||
self.assertNotIn('linkease_auth_url("auth")', controller)
|
||||
self.assertIn('compat():open("agentflow")', controller)
|
||||
self.assertIn('compat():legacy_status("agentflow"', controller)
|
||||
self.assertIn('http = require "luci.http"', controller)
|
||||
self.assertIn('resolver = require("luci.model.linkease.apps_openwrt").new()', controller)
|
||||
self.assertIn('auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth")', controller)
|
||||
self.assertNotIn("uhttpd_apps_proxy_available", controller)
|
||||
self.assertNotIn("linkeasefull_running", controller)
|
||||
self.assertIn('url("admin/services/agentflow/open")', status)
|
||||
self.assertNotIn('window.location.hostname + ":" + st.port', status)
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=baidudrive
|
||||
PKG_VERSION:=1.0.2
|
||||
PKG_RELEASE:=3
|
||||
LUCI_DEPENDS:=+baidudrive +luci-compat
|
||||
PKG_RELEASE:=4
|
||||
LUCI_DEPENDS:=+baidudrive +luci-compat +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
local http = require "luci.http"
|
||||
|
||||
module("luci.controller.baidudrive", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/baidudrive") then
|
||||
return
|
||||
end
|
||||
local page
|
||||
page = entry({"admin", "services", "baidudrive"}, cbi("baidudrive"), _("BaiduDrive"), 100)
|
||||
page.dependent = true
|
||||
if not nixio.fs.access("/etc/config/baidudrive") then return end
|
||||
entry({"admin", "services", "baidudrive"}, cbi("baidudrive"), _("BaiduDrive"), 100).dependent = true
|
||||
entry({"admin", "services", "baidudrive_status"}, call("baidudrive_status"))
|
||||
|
||||
local open = entry({"admin", "services", "baidudrive", "open"}, call("baidudrive_open"))
|
||||
open.leaf = true
|
||||
open.dependent = false
|
||||
open.sysauth = "root"
|
||||
open.sysauth_authenticator = "htmlauth"
|
||||
end
|
||||
|
||||
local function compat()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
return require("luci.model.linkease.apps_compat").new({
|
||||
http = require "luci.http",
|
||||
resolver = require("luci.model.linkease.apps_openwrt").new(),
|
||||
auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth")
|
||||
})
|
||||
end
|
||||
|
||||
function baidudrive_status()
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = uci:get_first("baidudrive", "baidudrive", "port") or "10780"
|
||||
local app_running = (sys.call("pidof baidudrive >/dev/null") == 0)
|
||||
local sdk_running = (sys.call("pidof baiduNas >/dev/null") == 0)
|
||||
local status = {
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local app_running = sys.call("pidof baidudrive >/dev/null") == 0
|
||||
local sdk_running = sys.call("pidof baiduNas >/dev/null") == 0
|
||||
compat():legacy_status("baidudrive", {
|
||||
running = app_running and sdk_running,
|
||||
app_running = app_running,
|
||||
sdk_running = sdk_running,
|
||||
port = port
|
||||
}
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(status)
|
||||
port = uci:get_first("baidudrive", "baidudrive", "port") or "10780"
|
||||
})
|
||||
end
|
||||
|
||||
function baidudrive_open()
|
||||
compat():open("baidudrive")
|
||||
end
|
||||
@@ -9,7 +9,7 @@ XHR.poll(5, '<%=url("admin/services/baidudrive_status")%>', null, function(x, st
|
||||
}
|
||||
if (st.running) {
|
||||
el.innerHTML = '<br/><em style=\"color:green\"><%:The BaiduDrive service is running.%></em>'
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open BaiduDrive%> \" onclick=\"window.open('http://" + window.location.hostname + ":" + st.port + "/')\"/>";
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open BaiduDrive%> \" onclick=\"window.open('<%=url("admin/services/baidudrive/open")%>')\"/>";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for Docker Manager
|
||||
LUCI_DEPENDS:=+dockermanager +luci-lib-linkeaseauth
|
||||
LUCI_DEPENDS:=+dockermanager +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_RELEASE:=2
|
||||
PKG_RELEASE:=3
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
module("luci.controller.dockermanager", package.seeall)
|
||||
|
||||
local APPS_PROXY_PREFIX = "/apps=http://127.0.0.1:19290"
|
||||
|
||||
function index()
|
||||
entry({"admin", "services", "dockermanager_status"}, call("dockermanager_status"))
|
||||
local open = entry({"admin", "services", "dockermanager", "open"}, call("dockermanager_open"))
|
||||
@@ -10,189 +8,33 @@ function index()
|
||||
open.sysauth = "root"
|
||||
open.sysauth_authenticator = "htmlauth"
|
||||
|
||||
if not nixio.fs.access("/etc/config/dockermanager") then
|
||||
return
|
||||
end
|
||||
|
||||
if not nixio.fs.access("/etc/config/dockermanager") then return end
|
||||
entry({"admin", "services", "dockermanager"}, cbi("dockermanager"), _("Docker Manager"), 22).dependent = true
|
||||
end
|
||||
|
||||
local function uhttpd_has_apps_proxy_prefix()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
|
||||
|
||||
for _, mapping in ipairs(mappings) do
|
||||
if mapping == APPS_PROXY_PREFIX then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function uhttpd_supports_proxy_prefix()
|
||||
local sys = require "luci.sys"
|
||||
return sys.call("grep -qr 'proxy_prefix' /etc/init.d/uhttpd /lib/functions /usr/share/uhttpd 2>/dev/null") == 0
|
||||
end
|
||||
|
||||
local function uhttpd_apps_proxy_available()
|
||||
return uhttpd_supports_proxy_prefix() and uhttpd_has_apps_proxy_prefix()
|
||||
end
|
||||
|
||||
local function linkeasefull_running()
|
||||
local sys = require "luci.sys"
|
||||
return sys.call("[ -x /etc/init.d/linkeasefull ] && /etc/init.d/linkeasefull running >/dev/null 2>&1") == 0
|
||||
end
|
||||
|
||||
local function normalized_base_path(path)
|
||||
path = path or "/apps/dockermanager/"
|
||||
if path:sub(1, 1) ~= "/" then
|
||||
path = "/" .. path
|
||||
end
|
||||
if path:sub(-1) ~= "/" then
|
||||
path = path .. "/"
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
local function cookie_encode(value)
|
||||
return tostring(value or ""):gsub("([^A-Za-z0-9._~-])", function(char)
|
||||
return string.format("%%%02X", char:byte())
|
||||
end)
|
||||
end
|
||||
|
||||
local function authority_host(authority)
|
||||
if not authority or authority == "" then
|
||||
return ""
|
||||
end
|
||||
if authority:sub(1, 1) == "[" then
|
||||
return authority:match("^%[([^%]]+)%]") or ""
|
||||
end
|
||||
return authority:match("^([^:]+)") or authority
|
||||
end
|
||||
|
||||
local function url_authority(host, port)
|
||||
if not host or host == "" then
|
||||
host = "127.0.0.1"
|
||||
end
|
||||
if host:find(":") and host:sub(1, 1) ~= "[" then
|
||||
host = "[" .. host .. "]"
|
||||
end
|
||||
return host .. ":" .. tostring(port)
|
||||
end
|
||||
|
||||
local function request_or_lan_host()
|
||||
local http = require "luci.http"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local host = authority_host(http.getenv("HTTP_HOST") or "")
|
||||
if host ~= "" then
|
||||
return host
|
||||
end
|
||||
return uci:get("network", "lan", "ipaddr") or "127.0.0.1"
|
||||
end
|
||||
|
||||
local function linkease_auth_url(name)
|
||||
local function compat()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
return dispatcher.build_url("admin", "services", "linkease_auth", name)
|
||||
end
|
||||
|
||||
local function dockermanager_config()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = uci:get_first("dockermanager", "dockermanager", "port") or "8192"
|
||||
local base_path = normalized_base_path(uci:get_first("dockermanager", "dockermanager", "base_path"))
|
||||
local external_port_enabled = uci:get_first("dockermanager", "dockermanager", "external_port_enabled") == "1"
|
||||
local listen_mode = uci:get_first("dockermanager", "dockermanager", "listen_mode") or "auto"
|
||||
local socket_path = uci:get_first("dockermanager", "dockermanager", "socket_path") or "/var/run/dockermanager.sock"
|
||||
return port, base_path, external_port_enabled, listen_mode, socket_path
|
||||
end
|
||||
|
||||
local function dockermanager_entry_url()
|
||||
local port, base_path, external_port_enabled = dockermanager_config()
|
||||
if linkeasefull_running() and uhttpd_apps_proxy_available() then
|
||||
return base_path
|
||||
end
|
||||
if not external_port_enabled then
|
||||
return nil
|
||||
end
|
||||
return "http://" .. url_authority(request_or_lan_host(), port) .. base_path
|
||||
end
|
||||
|
||||
local function enable_external_port()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local section = nil
|
||||
uci:foreach("dockermanager", "dockermanager", function(s)
|
||||
if not section then
|
||||
section = s[".name"]
|
||||
end
|
||||
end)
|
||||
if section then
|
||||
uci:set("dockermanager", section, "external_port_enabled", "1")
|
||||
uci:set("dockermanager", section, "listen_mode", "tcp")
|
||||
uci:commit("dockermanager")
|
||||
end
|
||||
local sys = require "luci.sys"
|
||||
sys.call("/etc/init.d/dockermanager restart >/dev/null 2>&1; sleep 1")
|
||||
end
|
||||
|
||||
local function render_enable_port_confirm()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
local confirm_url = dispatcher.build_url("admin", "services", "dockermanager", "open") .. "?enable_port=1"
|
||||
luci.http.prepare_content("text/html; charset=utf-8")
|
||||
luci.http.write("<!doctype html><html><head><meta charset='utf-8'><title>Docker Manager</title></head><body>")
|
||||
luci.http.write("<h2>Docker Manager</h2>")
|
||||
luci.http.write("<p>当前系统无法通过 LinkEase 桌面代理打开 Docker Manager。</p>")
|
||||
luci.http.write("<p>可以启用局域网端口 8192 后继续打开。</p>")
|
||||
luci.http.write("<p><a class='cbi-button cbi-button-apply' href='" .. confirm_url .. "'>启用 8192 并打开</a></p>")
|
||||
luci.http.write("</body></html>")
|
||||
end
|
||||
|
||||
local function render_open_failed()
|
||||
luci.http.prepare_content("text/html; charset=utf-8")
|
||||
luci.http.write("<!doctype html><html><head><meta charset='utf-8'><title>Docker Manager</title></head><body>")
|
||||
luci.http.write("<h2>Docker Manager</h2>")
|
||||
luci.http.write("<p>Docker Manager 已安装,但当前没有可用访问入口。</p>")
|
||||
luci.http.write("<p>请启用 LinkEaseFull 的 /apps 代理,或在 Docker Manager 设置中开放 8192 端口。</p>")
|
||||
luci.http.write("</body></html>")
|
||||
return require("luci.model.linkease.apps_compat").new({
|
||||
http = require "luci.http",
|
||||
resolver = require("luci.model.linkease.apps_openwrt").new(),
|
||||
auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth")
|
||||
})
|
||||
end
|
||||
|
||||
function dockermanager_status()
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port, base_path, external_port_enabled, listen_mode, socket_path = dockermanager_config()
|
||||
local entry_url = dockermanager_entry_url()
|
||||
|
||||
local status = {
|
||||
running = (sys.call("pidof docker-manager >/dev/null") == 0),
|
||||
port = port,
|
||||
base_path = base_path,
|
||||
listen_mode = listen_mode,
|
||||
socket_path = socket_path,
|
||||
external_port_enabled = external_port_enabled,
|
||||
entry_url = entry_url or "",
|
||||
lan_ip = uci:get("network", "lan", "ipaddr") or "",
|
||||
proxy_prefix_supported = uhttpd_supports_proxy_prefix(),
|
||||
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
|
||||
linkeasefull_running = linkeasefull_running()
|
||||
}
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(status)
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
compat():legacy_status("dockermanager", {
|
||||
running = sys.call("pidof docker-manager >/dev/null") == 0,
|
||||
port = uci:get_first("dockermanager", "dockermanager", "port") or "8192",
|
||||
base_path = uci:get_first("dockermanager", "dockermanager", "base_path") or "/apps/dockermanager/",
|
||||
listen_mode = uci:get_first("dockermanager", "dockermanager", "listen_mode") or "auto",
|
||||
socket_path = uci:get_first("dockermanager", "dockermanager", "socket_path") or "/var/run/dockermanager.sock",
|
||||
external_port_enabled = uci:get_first("dockermanager", "dockermanager", "external_port_enabled") == "1",
|
||||
lan_ip = uci:get("network", "lan", "ipaddr") or ""
|
||||
})
|
||||
end
|
||||
|
||||
function dockermanager_open()
|
||||
local http = require "luci.http"
|
||||
if http.formvalue("enable_port") == "1" then
|
||||
enable_external_port()
|
||||
end
|
||||
|
||||
local entry_url = dockermanager_entry_url()
|
||||
if entry_url then
|
||||
http.redirect(linkease_auth_url("auth") .. "?return=" .. cookie_encode(entry_url))
|
||||
return
|
||||
end
|
||||
|
||||
if http.formvalue("enable_port") == "1" then
|
||||
render_open_failed()
|
||||
return
|
||||
end
|
||||
render_enable_port_confirm()
|
||||
compat():open("dockermanager")
|
||||
end
|
||||
@@ -8,7 +8,7 @@
|
||||
var status = st.running
|
||||
? '<br/><em style=\"color:green\"><%:The Docker Manager service is running.%></em>'
|
||||
: '<br/><em style=\"color:red\"><%:The Docker Manager service is not running.%></em>';
|
||||
if (st.proxy_prefix_enabled && st.linkeasefull_running)
|
||||
if (st.proxy_prefix_enabled && st.app_entry_running)
|
||||
{
|
||||
status = status + '<br/><em><%:Access mode: LinkEase Desktop proxy.%></em>';
|
||||
}
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for fastnet
|
||||
LUCI_DEPENDS:=+fastnet
|
||||
LUCI_DEPENDS:=+fastnet +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=0.7.7-r1
|
||||
# PKG_RELEASE MUST be empty for luci.mk
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=0.7.7
|
||||
PKG_RELEASE:=3
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
|
||||
@@ -1,44 +1,34 @@
|
||||
module("luci.controller.fastnet", package.seeall)
|
||||
|
||||
function index()
|
||||
local fs = require "nixio.fs"
|
||||
if not fs.access("/etc/config/fastnet") then
|
||||
return
|
||||
end
|
||||
if not nixio.fs.access("/etc/config/fastnet") then return end
|
||||
|
||||
entry({"admin", "services", "fastnet"}, cbi("fastnet"), _("FastNet"), 50).dependent = true
|
||||
entry({"admin", "services", "fastnet", "status"}, call("action_status")).leaf = true
|
||||
local open = entry({"admin", "services", "fastnet", "open"}, call("action_open"))
|
||||
open.leaf = true
|
||||
open.dependent = false
|
||||
open.sysauth = false
|
||||
end
|
||||
|
||||
local function compat()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
return require("luci.model.linkease.apps_compat").new({
|
||||
http = require "luci.http",
|
||||
resolver = require("luci.model.linkease.apps_openwrt").new(),
|
||||
auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth")
|
||||
})
|
||||
end
|
||||
|
||||
function action_status()
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local http = require "luci.http"
|
||||
|
||||
local function get_host()
|
||||
local host = http.getenv("HTTP_HOST") or http.getenv("SERVER_NAME") or ""
|
||||
host = host:gsub(":%d+$", "")
|
||||
if host == "_redirect2ssl" or host == "redirect2ssl" or host == "" then
|
||||
host = http.getenv("SERVER_ADDR") or "localhost"
|
||||
end
|
||||
return host
|
||||
end
|
||||
|
||||
local running = (sys.call("pidof FastNet >/dev/null") == 0)
|
||||
local host = get_host()
|
||||
local port = uci:get_first("fastnet", "fastnet", "port") or "3200"
|
||||
local token = uci:get_first("fastnet", "fastnet", "token") or ""
|
||||
|
||||
local url = "http://" .. host .. ":" .. port .. "/"
|
||||
if token ~= "" then
|
||||
url = url .. "?token=" .. token
|
||||
end
|
||||
|
||||
http.prepare_content("application/json")
|
||||
http.write_json({
|
||||
running = running,
|
||||
host = host,
|
||||
port = port,
|
||||
url = url
|
||||
compat():legacy_status("fastnet", {
|
||||
running = sys.call("pidof FastNet >/dev/null") == 0,
|
||||
port = uci:get_first("fastnet", "fastnet", "port") or "3200"
|
||||
})
|
||||
end
|
||||
|
||||
function action_open()
|
||||
compat():open("fastnet")
|
||||
end
|
||||
@@ -1,27 +1,12 @@
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local http = require "luci.http"
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
|
||||
local m = Map("fastnet", translate("FastNet"))
|
||||
m.description = translate("FastNet provides network testing tools and a Web UI.")
|
||||
|
||||
local function get_host()
|
||||
local host = http.getenv("HTTP_HOST") or http.getenv("SERVER_NAME") or ""
|
||||
host = host:gsub(":%d+$", "")
|
||||
if host == "_redirect2ssl" or host == "redirect2ssl" or host == "" then
|
||||
host = http.getenv("SERVER_ADDR") or "localhost"
|
||||
end
|
||||
return host
|
||||
end
|
||||
|
||||
local st = m:section(SimpleSection, translate("Status"))
|
||||
local running = (sys.call("pidof FastNet >/dev/null") == 0)
|
||||
local listen_port = uci:get_first("fastnet", "fastnet", "port") or "3200"
|
||||
local token = uci:get_first("fastnet", "fastnet", "token") or ""
|
||||
local url = "http://" .. get_host() .. ":" .. listen_port .. "/"
|
||||
if token ~= "" then
|
||||
url = url .. "?token=" .. token
|
||||
end
|
||||
local url = dispatcher.build_url("admin", "services", "linkease_apps", "open") .. "?id=fastnet"
|
||||
|
||||
st.template = "fastnet/status"
|
||||
st.running = running
|
||||
|
||||
@@ -12,7 +12,7 @@ LUCI_DEPENDS:=+luci-base +logd +jsonfilter
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=0.1.45
|
||||
PKG_RELEASE:=34
|
||||
PKG_RELEASE:=35
|
||||
|
||||
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
|
||||
ifdef SOURCE_DATE_EPOCH
|
||||
|
||||
@@ -36,7 +36,9 @@ function chipValueNodes(spec, val) {
|
||||
return [
|
||||
label + ': ',
|
||||
E('strong', { 'class': 'fwlive-chip-not' }, [_('not')]),
|
||||
' contains ',
|
||||
' ',
|
||||
_('contains'),
|
||||
' ',
|
||||
valueNode
|
||||
];
|
||||
|
||||
|
||||
@@ -11,5 +11,5 @@
|
||||
* GENERATED — do not edit. Edit fwlive.css and run: node scripts/embed-fwlive-css.js
|
||||
*/
|
||||
return baseclass.extend({
|
||||
styleText: "\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-display-bar-label {\n\tflex: 0 0 auto;\n\tfoLine truncated
|
||||
styleText: "\n/* SPDX-License-Identifier: Apache-2.0 */\n.fwlive-map {\n\tmax-width: none;\n\twidth: 100%;\n\t--fwlive-pass-color: var(--success-color-high, var(--success-color, #46a546));\n\t--fwlive-deny-color: var(--error-color-high, var(--error-color, #ca3c3c));\n\t--fwlive-bg-medium: var(--background-color-medium, var(--white-color-low, #f9f9f9));\n\t--fwlive-paused-color: #b45309;\n}\n/* Wide page in Detail only — Simple keeps LuCI’s 940px column. */\n#maincontent.container:has(.fwlive-map[data-view=\"detailed\"]) {\n\tmax-width: none;\n\tpadding-left: 16px;\n\tpadding-right: 16px;\n}\n.fwlive-map[data-row-tint=\"accessible\"] {\n\t--fwlive-pass-color: #0d9488;\n\t--fwlive-deny-color: #c2410c;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row,\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tbackground: rgba(180, 83, 9, 0.08);\n\tbackground: color-mix(in srgb, var(--fwlive-paused-color) 8%, transparent);\n}\n.fwlive-map.fwlive-watch-paused .fwlive-title-row {\n\tmargin: 0 -8px;\n\tpadding: 6px 8px 0;\n\tborder-radius: 3px 3px 0 0;\n}\n.fwlive-map.fwlive-watch-paused .fwlive-watch-strip {\n\tmargin: 0 -8px 10px;\n\tpadding: 8px;\n\tborder-radius: 0 0 3px 3px;\n\tborder-bottom-color: transparent;\n}\n.fwlive-title-row {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: baseline;\n\tjustify-content: space-between;\n\tgap: 8px 16px;\n\tmargin: 0 0 4px;\n}\n.fwlive-title-row h2 {\n\tmargin: 0;\n\tflex: 0 1 auto;\n}\n.fwlive-title-status {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 8px;\n\tflex-wrap: wrap;\n\tmin-width: 0;\n\tmargin-left: auto;\n}\n.fwlive-watch-strip {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: stretch;\n\tjustify-content: flex-start;\n\tgap: 0;\n\tmin-height: 40px;\n\tpadding: 8px 0 10px;\n\tborder-bottom: 1px solid var(--border-color-medium);\n\tmargin-bottom: 10px;\n}\n.fwlive-watch-group {\n\tdisplay: flex;\n\talign-items: center;\n\tgap: 6px;\n\tmin-height: 28px;\n\tpadding: 0 10px;\n\tborder-left: 1px solid var(--border-color-medium);\n\tflex-wrap: wrap;\n}\n.fwlive-watch-group:last-child {\n\tpadding-right: 0;\n}\n.fwlive-watch-group-label {\n\tmargin-right: 2px;\n\tcolor: var(--text-color-medium);\n\tfont-size: 0.72em;\n\tfont-weight: 700;\n\tletter-spacing: 0.08em;\n\ttext-transform: uppercase;\n\twhite-space: nowrap;\n}\n.fwlive-watch-seg {\n\tdisplay: inline-flex;\n\talign-items: stretch;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\toverflow: hidden;\n}\n.fwlive-watch-seg .fwlive-seg-btn {\n\tmargin: 0;\n\tborder: 0;\n\tborder-radius: 0;\n\tborder-right: 1px solid var(--border-color-medium);\n\tbackground: transparent;\n\tpadding: 4px 10px;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n}\n.fwlive-watch-seg .fwlive-seg-btn:last-child {\n\tborder-right: 0;\n}\n.fwlive-watch-seg .fwlive-seg-btn[aria-pressed=\"true\"] {\n\tbackground: rgba(26, 111, 156, 0.12);\n\tbackground: color-mix(in srgb, var(--primary-color-high) 12%, transparent);\n\tcolor: var(--primary-color-high);\n\tfont-weight: 600;\n}\n.fwlive-watch-label { font-weight: 600; }\n.fwlive-watch-paused .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-map.fwlive-watch-paused .fwlive-title-status .fwlive-watch-label { color: var(--fwlive-paused-color); }\n.fwlive-dot {\n\twidth: 8px;\n\theight: 8px;\n\tborder-radius: 50%;\n\tbackground: var(--text-color-low, var(--border-color-high));\n\tdisplay: inline-block;\n\tflex-shrink: 0;\n}\n.fwlive-dot.fwlive-dot-on {\n\tbackground: var(--fwlive-pass-color);\n\tbox-shadow: 0 0 0 3px color-mix(in srgb, var(--fwlive-pass-color) 22%, transparent);\n}\n.fwlive-btn-ghost {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n}\n.fwlive-btn-quiet {\n\tbackground: transparent;\n\tborder-color: var(--border-color-medium);\n\tcolor: var(--text-color-medium);\n\tfont-weight: 500;\n}\n.fwlive-log-merged {\n\tdisplay: inline-flex;\n\talign-items: center;\n\tgap: 6px;\n\tpadding: 4px 10px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-medium, var(--white-color-low, var(--background-color-high)));\n\tcolor: var(--text-color-high);\n\tfont-weight: 500;\n\tfont-size: 0.88em;\n\tline-height: 1.25;\n\tmin-height: 28px;\n\tcursor: pointer;\n}\n.fwlive-log-merged:hover {\n\tbackground: var(--background-color-low, var(--background-color-medium));\n}\n.fwlive-log-merged:disabled {\n\topacity: 0.65;\n\tcursor: default;\n}\n.fwlive-log-on-dot {\n\twidth: 6px;\n\theight: 6px;\n\tborder-radius: 50%;\n\tbackground: var(--fwlive-pass-color);\n\tflex-shrink: 0;\n}\n.fwlive-log-rate {\n\tcolor: var(--text-color-medium);\n\tfont-weight: 400;\n}\n.fwlive-display-bar {\n\tdisplay: flex;\n\tflex-wrap: wrap;\n\talign-items: center;\n\tgap: 8px 14px;\n\tmargin: 0 0 12px;\n\tpadding: 8px 12px;\n\tborder: 1px solid var(--border-color-medium);\n\tborder-radius: 3px;\n\tbackground: var(--background-color-high);\n}\n.fwlive-dLine truncated
|
||||
});
|
||||
@@ -1,3 +1,4 @@
|
||||
/* SPDX-License-Identifier: Apache-2.0 */
|
||||
.fwlive-map {
|
||||
max-width: none;
|
||||
width: 100%;
|
||||
|
||||
@@ -5,81 +5,103 @@
|
||||
|
||||
/**
|
||||
* Shared classify logic mirrors core/fwlive-log.js CLASSIFY_SPEC — keep in sync
|
||||
* (gen-luci-wrapper.js gates full-spec drift; ./scripts/gen-all.sh verifies).
|
||||
* (gen-luci-wrapper.js gates full-spec + regex drift; ./scripts/gen-all.sh verifies).
|
||||
* LuCI-only helpers live in the @fwlive-codegen:luci-preserve region.
|
||||
*/
|
||||
const CLASSIFY_SPEC = {
|
||||
glueKeys: [
|
||||
'IN',
|
||||
'OUT',
|
||||
'SRC',
|
||||
'DST',
|
||||
'PROTO',
|
||||
'SPT',
|
||||
'DPT',
|
||||
'LEN',
|
||||
'MAC',
|
||||
'TYPE',
|
||||
'CODE',
|
||||
'TTL',
|
||||
'TOS',
|
||||
'PREC',
|
||||
'DF'
|
||||
],
|
||||
nonFirewallPrefixes: [
|
||||
'dnsmasq',
|
||||
'procd',
|
||||
'ubusd',
|
||||
'netifd',
|
||||
'odhcpd',
|
||||
'logd',
|
||||
'dropbear',
|
||||
'uhttpd',
|
||||
'hostapd',
|
||||
'wpad'
|
||||
],
|
||||
nonFirewallPrefixHyphenContinuation: true,
|
||||
firewallHints: ['fw4', 'nft', 'iptables', 'kernel', 'firewall'],
|
||||
actionWords: ['ACCEPT', 'ALLOW', 'PASS', 'DROP', 'REJECT', 'DENY', 'BLOCK'],
|
||||
rules: [
|
||||
{
|
||||
or: [
|
||||
{ and: [{ kv: ['SRC'] }, { kv: ['DST'] }] },
|
||||
{
|
||||
and: [
|
||||
{ kvAny: ['IN', 'OUT'] },
|
||||
{ kvAny: ['SRC', 'DST', 'PROTO', 'SPT', 'DPT'] }
|
||||
]
|
||||
},
|
||||
{ and: [{ action: 'known' }, { kvAny: ['IN', 'OUT', 'PROTO', 'SRC', 'DST'] }] }
|
||||
]
|
||||
},
|
||||
{ and: [{ hint: true }, { action: 'known' }] },
|
||||
{ and: [{ hint: true }, { kvAny: ['IN', 'OUT', 'SRC', 'DST', 'PROTO'] }] }
|
||||
]
|
||||
};
|
||||
|
||||
function wordPattern(words) {
|
||||
const alt = words.join('|');
|
||||
return new RegExp('(^|[^A-Za-z0-9_])(' + alt + ')([^A-Za-z0-9_]|$)', 'i');
|
||||
}
|
||||
|
||||
/* ---- spec-derived classification regexes (mirror core/fwlive-log.js) ---- */
|
||||
const DENY_CLASS_WORDS = CLASSIFY_SPEC.actionWords.slice(3);
|
||||
const NON_FIREWALL_PREFIX = new RegExp(
|
||||
'^(' +
|
||||
CLASSIFY_SPEC.nonFirewallPrefixes.join('|') +
|
||||
')(' +
|
||||
(CLASSIFY_SPEC.nonFirewallPrefixHyphenContinuation ? '[^A-Za-z0-9_-]' : '[^A-Za-z0-9_]') +
|
||||
'|$)',
|
||||
'i'
|
||||
);
|
||||
const FIREWALL_HINT = wordPattern(CLASSIFY_SPEC.firewallHints);
|
||||
const ACTION_RE = wordPattern(CLASSIFY_SPEC.actionWords);
|
||||
const DENY_ACTION = wordPattern(DENY_CLASS_WORDS);
|
||||
const DENY_ACTION_UNDERSCORE = new RegExp(
|
||||
'(?:^|[^A-Za-z0-9])(?:' + DENY_CLASS_WORDS.join('|') + ')(?:[^A-Za-z0-9]|$)',
|
||||
'i'
|
||||
);
|
||||
const NETFILTER_KV_GLUE = new RegExp(
|
||||
'([^\\s])(?=(' + CLASSIFY_SPEC.glueKeys.join('|') + ')=)',
|
||||
'g'
|
||||
);
|
||||
|
||||
return baseclass.extend({
|
||||
CLASSIFY_SPEC: {
|
||||
glueKeys: [
|
||||
'IN',
|
||||
'OUT',
|
||||
'SRC',
|
||||
'DST',
|
||||
'PROTO',
|
||||
'SPT',
|
||||
'DPT',
|
||||
'LEN',
|
||||
'MAC',
|
||||
'TYPE',
|
||||
'CODE',
|
||||
'TTL',
|
||||
'TOS',
|
||||
'PREC',
|
||||
'DF'
|
||||
],
|
||||
nonFirewallPrefixes: [
|
||||
'dnsmasq',
|
||||
'procd',
|
||||
'ubusd',
|
||||
'netifd',
|
||||
'odhcpd',
|
||||
'logd',
|
||||
'dropbear',
|
||||
'uhttpd',
|
||||
'hostapd',
|
||||
'wpad'
|
||||
],
|
||||
nonFirewallPrefixHyphenContinuation: true,
|
||||
firewallHints: ['fw4', 'nft', 'iptables', 'kernel', 'firewall'],
|
||||
actionWords: ['ACCEPT', 'ALLOW', 'PASS', 'DROP', 'REJECT', 'DENY', 'BLOCK'],
|
||||
rules: [
|
||||
{
|
||||
or: [
|
||||
{ and: [{ kv: ['SRC'] }, { kv: ['DST'] }] },
|
||||
{
|
||||
and: [
|
||||
{ kvAny: ['IN', 'OUT'] },
|
||||
{ kvAny: ['SRC', 'DST', 'PROTO', 'SPT', 'DPT'] }
|
||||
]
|
||||
},
|
||||
{ and: [{ action: 'known' }, { kvAny: ['IN', 'OUT', 'PROTO', 'SRC', 'DST'] }] }
|
||||
]
|
||||
},
|
||||
{ and: [{ hint: true }, { action: 'known' }] },
|
||||
{ and: [{ hint: true }, { kvAny: ['IN', 'OUT', 'SRC', 'DST', 'PROTO'] }] }
|
||||
]
|
||||
},
|
||||
CLASSIFY_SPEC: CLASSIFY_SPEC,
|
||||
|
||||
TCP_FLAG_TAIL:
|
||||
/\b((?:SYN|ACK|FIN|RST|PSH|URG)(?:\s+(?:SYN|ACK|FIN|RST|PSH|URG))*)(?:\s+[A-Z][A-Z0-9_]*=[^\s]+)*\s*$/i,
|
||||
NETFILTER_KV_GLUE:
|
||||
/([^\s])(?=(IN|OUT|SRC|DST|PROTO|SPT|DPT|LEN|MAC|TYPE|CODE|TTL|TOS|PREC|DF)=)/g,
|
||||
|
||||
wordPattern: function (words) {
|
||||
const alt = words.join('|');
|
||||
return new RegExp('(^|[^A-Za-z0-9_])(' + alt + ')([^A-Za-z0-9_]|$)', 'i');
|
||||
},
|
||||
NETFILTER_KV_GLUE: NETFILTER_KV_GLUE,
|
||||
|
||||
kvHas: function (msg, key) {
|
||||
return new RegExp('(^|[^A-Za-z0-9_])' + key + '=').test(msg);
|
||||
},
|
||||
|
||||
NON_FIREWALL_PREFIX:
|
||||
/^(dnsmasq|procd|ubusd|netifd|odhcpd|logd|dropbear|uhttpd|hostapd|wpad)([^A-Za-z0-9_-]|$)/i,
|
||||
FIREWALL_HINT: /(^|[^A-Za-z0-9_])(fw4|nft|iptables|kernel|firewall)([^A-Za-z0-9_]|$)/i,
|
||||
ACTION_RE: /(^|[^A-Za-z0-9_])(ACCEPT|ALLOW|PASS|DROP|REJECT|DENY|BLOCK)([^A-Za-z0-9_]|$)/i,
|
||||
DENY_ACTION: /(^|[^A-Za-z0-9_])(DROP|REJECT|DENY|BLOCK)([^A-Za-z0-9_]|$)/i,
|
||||
DENY_ACTION_UNDERSCORE: /(?:^|[^A-Za-z0-9])(?:DROP|REJECT|DENY|BLOCK)(?:[^A-Za-z0-9]|$)/i,
|
||||
NON_FIREWALL_PREFIX: NON_FIREWALL_PREFIX,
|
||||
FIREWALL_HINT: FIREWALL_HINT,
|
||||
ACTION_RE: ACTION_RE,
|
||||
DENY_ACTION: DENY_ACTION,
|
||||
DENY_ACTION_UNDERSCORE: DENY_ACTION_UNDERSCORE,
|
||||
MAX_DATE_SECONDS: 8640000000000,
|
||||
|
||||
normalizeNetfilterMessage: function (message) {
|
||||
@@ -288,24 +310,6 @@ return baseclass.extend({
|
||||
return '%s:%s:%s'.format(pad(d.getHours()), pad(d.getMinutes()), pad(d.getSeconds()));
|
||||
},
|
||||
|
||||
formatFlowDisplay: function (row) {
|
||||
const src = row && row.src ? String(row.src) : '';
|
||||
const dst = row && row.dst ? String(row.dst) : '';
|
||||
const sport = row && row.sport ? String(row.sport) : '';
|
||||
const dport = row && row.dport ? String(row.dport) : '';
|
||||
let left = src;
|
||||
let right = dst;
|
||||
|
||||
if (sport) left = left ? left + ':' + sport : ':' + sport;
|
||||
if (dport) right = right ? right + ':' + dport : ':' + dport;
|
||||
|
||||
if (!left && !right) return '—';
|
||||
if (!right) return left;
|
||||
if (!left) return '→ ' + right;
|
||||
|
||||
return left + ' → ' + right;
|
||||
},
|
||||
|
||||
formatCell: function (value) {
|
||||
if (value == null || value === '') return '';
|
||||
|
||||
@@ -459,14 +463,7 @@ return baseclass.extend({
|
||||
const p = this.parseFilterValue(val);
|
||||
if (!p.value) return '';
|
||||
|
||||
if (p.negate) {
|
||||
if (field === 'q' || field === 'src' || field === 'dst')
|
||||
return '%s: not contains %s'.format(field, p.value);
|
||||
|
||||
return '%s: not %s'.format(field, p.value);
|
||||
}
|
||||
|
||||
return '%s: %s'.format(field, val);
|
||||
return _('%s: %s').format(field, val);
|
||||
},
|
||||
|
||||
matchesTextField: function (haystack, spec) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*
|
||||
* renderToolbar(host, state, callbacks) → void
|
||||
* host - #fwlive-logging-bar strip slot (cleared and rebuilt; element kept)
|
||||
* state - { loggingStatus, loggingBusy, entriesLength, loggingNotice }
|
||||
* state - { loggingStatus, loggingBusy, loggingNotice }
|
||||
* callbacks - { onEnable(), onDisable() }
|
||||
*
|
||||
* G Hybrid chrome: when WAN logging is on, one merged control carries status +
|
||||
|
||||
@@ -854,7 +854,6 @@ return view.extend({
|
||||
return {
|
||||
loggingStatus: this.loggingStatus,
|
||||
loggingBusy: this.loggingBusy,
|
||||
entriesLength: this.entries.length,
|
||||
loggingNotice: this.loggingNotice,
|
||||
showConsent: this.shouldShowLoggingConsent()
|
||||
};
|
||||
|
||||
@@ -16,6 +16,10 @@ msgstr ""
|
||||
msgid "%d matching · %d/%d stored"
|
||||
msgstr "%d matching · %d/%d stored"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:466
|
||||
msgid "%s: %s"
|
||||
msgstr "%s: %s"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929
|
||||
msgid "0 matching · %d/%d stored"
|
||||
msgstr "0 matching · %d/%d stored"
|
||||
@@ -643,6 +647,10 @@ msgstr "allow/deny rules, LAN logging, or anything else."
|
||||
msgid "buffer full"
|
||||
msgstr "Puffer voll"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:40
|
||||
msgid "contains"
|
||||
msgstr "enthält"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81
|
||||
msgid "default 10/minute"
|
||||
msgstr "Standard 10/Minute"
|
||||
|
||||
@@ -17,6 +17,10 @@ msgstr ""
|
||||
msgid "%d matching · %d/%d stored"
|
||||
msgstr "%d matching · %d/%d stored"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:466
|
||||
msgid "%s: %s"
|
||||
msgstr "%s: %s"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929
|
||||
msgid "0 matching · %d/%d stored"
|
||||
msgstr "0 matching · %d/%d stored"
|
||||
@@ -648,6 +652,10 @@ msgstr "allow/deny rules, LAN logging, or anything else."
|
||||
msgid "buffer full"
|
||||
msgstr "бÑÑÐµÑ Ð·Ð°Ð¿Ð¾Ð»Ð½ÐµÐ½"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:40
|
||||
msgid "contains"
|
||||
msgstr "содержит"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81
|
||||
msgid "default 10/minute"
|
||||
msgstr "по ÑмолÑÐ°Ð½Ð¸Ñ 10/мин"
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -16,6 +16,10 @@ msgstr ""
|
||||
msgid "%d matching · %d/%d stored"
|
||||
msgstr "%d matching · %d/%d stored"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/log.js:466
|
||||
msgid "%s: %s"
|
||||
msgstr "%s: %s"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/view/status/fwlive.js:929
|
||||
msgid "0 matching · %d/%d stored"
|
||||
msgstr "0 matching · %d/%d stored"
|
||||
@@ -612,6 +616,10 @@ msgstr "allow/deny rules, LAN logging, or anything else."
|
||||
msgid "buffer full"
|
||||
msgstr "ç¼å²åºå·²æ»¡"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/chips.js:40
|
||||
msgid "contains"
|
||||
msgstr "包含"
|
||||
|
||||
#: openwrt-feed/luci-app-fwlive/htdocs/luci-static/resources/fwlive/logging.js:81
|
||||
msgid "default 10/minute"
|
||||
msgstr "é»è®¤ 10 æ¡/åé"
|
||||
|
||||
@@ -64,6 +64,8 @@ function json_unhex4(h, n, i, c, v) {
|
||||
return n
|
||||
}
|
||||
function json_get_msg(obj, s, i, c, esc, out, hex, n) {
|
||||
# Summary decoder is ASCII/Latin-1 oriented. \uXXXX outside 1..255 is
|
||||
# collapsed (not UTF-8). Invalid \u appends u. NUL is dropped.
|
||||
if (!match(obj, /"msg"[[:space:]]*:[[:space:]]*"/)) return ""
|
||||
s = substr(obj, RSTART + RLENGTH)
|
||||
out = ""
|
||||
@@ -79,6 +81,8 @@ function json_get_msg(obj, s, i, c, esc, out, hex, n) {
|
||||
else if (c == "u") {
|
||||
hex = substr(s, i + 1, 4)
|
||||
n = (length(hex) == 4) ? json_unhex4(hex) : -1
|
||||
# 1..255: Latin-1 %c. n>255: collapse. n<0: append u.
|
||||
# n==0 (NUL): BusyBox awk strings cannot hold NUL; drop it.
|
||||
if (n >= 1 && n <= 255) out = out sprintf("%c", n)
|
||||
else if (n < 0) out = out "u"
|
||||
if (n >= 0) i += 4
|
||||
|
||||
@@ -28,12 +28,15 @@ NF_LOG_IPV6_READY=false
|
||||
# firewall.<zone>.log bit, computes a target, then uci set + uci commit. Two
|
||||
# concurrent callers could otherwise interleave and last-commit-wins.
|
||||
#
|
||||
# BusyBox flock constraint: it has NO -w timeout. A stuck lock holder blocks
|
||||
# any waiter until the holder exits or the device reboots. The critical
|
||||
# section MUST stay SHORT (a few uci commands). Do NOT hold the lock across
|
||||
# the /etc/init.d/firewall reload (can take seconds); the lock is released
|
||||
# before reload, and reload-failure rollback is a best-effort UCI write
|
||||
# outside the lock.
|
||||
# BusyBox flock constraint: it has NO -w timeout. The lock is opened with
|
||||
# `exec 9>>` + `flock 9`. In ash/dash, `>>` clears close-on-exec, so a live
|
||||
# child keeps fd 9 after this holder dies. A stuck lock therefore blocks any
|
||||
# waiter until the last fd-9 inheritor exits or the device reboots (killing
|
||||
# the holder alone may not release it). The critical section MUST stay SHORT
|
||||
# (a few uci commands). Do NOT hold the lock across the /etc/init.d/firewall
|
||||
# reload (can take seconds); the lock is released before reload.
|
||||
# Reload-failure rollback re-acquires the lock so the read-compare-restore
|
||||
# is atomic against concurrent writers; do not drop that re-acquire.
|
||||
# Overridable for tests/containers (default is root-only /etc/fwlive).
|
||||
WAN_LOG_LOCK_FILE="${FWLIVE_WAN_LOG_LOCK_FILE:-/etc/fwlive/logging.lock}"
|
||||
WAN_LOG_BASELINE_FILE="${FWLIVE_WAN_LOG_BASELINE_FILE:-/etc/fwlive/wan-log-baseline}"
|
||||
@@ -435,7 +438,17 @@ restore_wan_log_baseline() {
|
||||
logger -t fwlive "WAN log baseline restore skipped: firewall changes pending" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
if ! commit_wan_log_change "$zone" "$zone_json" "$baseline"; then
|
||||
_commit_rc=0
|
||||
commit_wan_log_change "$zone" "$zone_json" "$baseline" || _commit_rc=$?
|
||||
if [ "$_commit_rc" -eq 2 ]; then
|
||||
# Commit landed; live fw4 still needs a reload. Baseline file
|
||||
# stays so a later restore can retry — read-back was not ours.
|
||||
release_wan_log_lock
|
||||
reload_firewall || logger -t fwlive "WAN log baseline restore: firewall reload failed after raced commit" 2>/dev/null || true
|
||||
logger -t fwlive "WAN log baseline restore: post-commit verify raced" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
if [ "$_commit_rc" -ne 0 ]; then
|
||||
release_wan_log_lock
|
||||
logger -t fwlive "WAN log baseline restore: commit gate failed" 2>/dev/null || true
|
||||
return 1
|
||||
@@ -701,7 +714,19 @@ restore_wan_zone_log() {
|
||||
logger -t fwlive "WAN log rollback skipped after stage: firewall changes staged by another writer" 2>/dev/null || true
|
||||
return 1
|
||||
fi
|
||||
uci commit firewall 2>/dev/null || true
|
||||
if ! uci commit firewall 2>/dev/null; then
|
||||
# Drop our staged rollback delta so a later toggle is not stuck
|
||||
# on firewall_changes_pending from this package's own orphaned
|
||||
# write. Mirror commit_wan_log_change: revert only when the
|
||||
# remaining staging is entirely our log option.
|
||||
_staged=$(uci -q changes firewall 2>/dev/null || true)
|
||||
_total=$(printf '%s\n' "$_staged" | grep -c . 2>/dev/null || true)
|
||||
_ours=$(wan_log_count_our_staged_lines "$zone" "$_staged")
|
||||
if [ "${_total:-0}" -gt 0 ] && [ "${_total:-0}" -eq "${_ours:-0}" ]; then
|
||||
uci -q revert firewall 2>/dev/null || true
|
||||
fi
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# Stage + commit the WAN log bit. Caller MUST hold the logging lock; this
|
||||
@@ -721,10 +746,11 @@ restore_wan_zone_log() {
|
||||
# for a failed reload.
|
||||
# Residual window: a writer that stages between the post-stage check and
|
||||
# `uci commit` can still ride along; post-commit verification detects a
|
||||
# mismatched log bit. Same-option races (another writer also staging
|
||||
# firewall.<wan>.log) are not distinguishable in the changes list.
|
||||
# The caller-reported error is firewall_changes_pending: the LuCI view already
|
||||
# maps it to the accurate "another change is staged" notice.
|
||||
# mismatched log bit and reports firewall_commit_raced (no blind-revert).
|
||||
# Same-option races (another writer also staging firewall.<wan>.log) are
|
||||
# not distinguishable in the changes list. Pre-stage races report
|
||||
# firewall_changes_pending: the LuCI view already maps it to the accurate
|
||||
# "another change is staged" notice.
|
||||
#
|
||||
# target: value to stage; EMPTY means delete the option (bit fully cleared).
|
||||
# Prints the failure JSON and returns 1 on abort or failure.
|
||||
@@ -807,14 +833,44 @@ commit_wan_log_change() {
|
||||
# Post-commit verification: confirm the committed config really
|
||||
# carries what we wrote (empty target = option must now be gone/empty).
|
||||
# A mismatch means another writer overtook the commit: warn loudly but do
|
||||
# NOT blind-revert (that would destroy unrelated committed data); the
|
||||
# reload-failure rollback path still guards the ordinary failure case.
|
||||
# NOT blind-revert (that would destroy unrelated committed data). Return 2
|
||||
# so the caller still reloads (live fw4 must track committed UCI) and then
|
||||
# reports firewall_commit_raced instead of ok:true. Do not print JSON here:
|
||||
# reload may still fail. Return 1 is reserved for pre-commit aborts that
|
||||
# already printed an error body.
|
||||
if ! verify_wan_log_commit "$zone" "$target"; then
|
||||
logger -t fwlive "WAN log post-commit verify FAILED: wrote '${target:-<deleted>}', read back differs" 2>/dev/null || true
|
||||
return 2
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Caller MUST have released the logging lock. rc from commit_wan_log_change:
|
||||
# 0 — our target is committed; reload and report success.
|
||||
# 1 — abort JSON already printed; no commit, skip reload.
|
||||
# 2 — commit succeeded but verify raced; still reload, then report
|
||||
# firewall_commit_raced (or firewall_reload_failed). Do not use
|
||||
# reload_and_report_wan_log: a reload failure must not restore
|
||||
# `previous` over a foreign committed value.
|
||||
report_wan_log_after_commit() {
|
||||
_rc="$1"
|
||||
zone_json="$2"
|
||||
if [ "$_rc" -eq 1 ]; then
|
||||
return 0
|
||||
fi
|
||||
if [ "$_rc" -eq 2 ]; then
|
||||
if ! reload_firewall; then
|
||||
logger -t fwlive "Firewall reload failed after raced WAN log commit" 2>/dev/null || true
|
||||
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_reload_failed"}' "$zone_json"
|
||||
return 0
|
||||
fi
|
||||
printf '{"ok":false,"changed":false,"wan_zone":%s,"error":"firewall_commit_raced"}' "$zone_json"
|
||||
return 0
|
||||
fi
|
||||
shift 2
|
||||
reload_and_report_wan_log "$@"
|
||||
}
|
||||
|
||||
# Firewall reload + best-effort UCI rollback on reload failure. The reload
|
||||
# itself runs WITHOUT the logging lock (it can take seconds and a held lock
|
||||
# would block a concurrent toggle until the holder exits — BusyBox flock has
|
||||
@@ -918,13 +974,11 @@ enable_wan_logging() {
|
||||
fi
|
||||
|
||||
target=$(wan_filter_log_target_value "$current")
|
||||
if ! commit_wan_log_change "$zone" "$zone_json" "$target"; then
|
||||
release_wan_log_lock
|
||||
return 0
|
||||
fi
|
||||
_commit_rc=0
|
||||
commit_wan_log_change "$zone" "$zone_json" "$target" || _commit_rc=$?
|
||||
release_wan_log_lock
|
||||
|
||||
reload_and_report_wan_log "$zone" "$current" "$target" \
|
||||
report_wan_log_after_commit "$_commit_rc" "$zone_json" \
|
||||
"$zone" "$current" "$target" \
|
||||
'Firewall reload failed after enable; reverted UCI WAN log' \
|
||||
'WAN zone logging enabled' \
|
||||
"$zone_json"
|
||||
@@ -971,13 +1025,11 @@ disable_wan_logging() {
|
||||
# not restore that bit.
|
||||
|
||||
target=$(wan_filter_log_clear_value "$current")
|
||||
if ! commit_wan_log_change "$zone" "$zone_json" "$target"; then
|
||||
release_wan_log_lock
|
||||
return 0
|
||||
fi
|
||||
_commit_rc=0
|
||||
commit_wan_log_change "$zone" "$zone_json" "$target" || _commit_rc=$?
|
||||
release_wan_log_lock
|
||||
|
||||
reload_and_report_wan_log "$zone" "$current" "$target" \
|
||||
report_wan_log_after_commit "$_commit_rc" "$zone_json" \
|
||||
"$zone" "$current" "$target" \
|
||||
'Firewall reload failed after disable; reverted UCI WAN log' \
|
||||
'WAN zone logging disabled' \
|
||||
"$zone_json"
|
||||
|
||||
@@ -39,6 +39,11 @@ RESOLVE_BUDGET=5
|
||||
POLL_LINES_MAX=2000
|
||||
# Bound nft ruleset dumps (corrupted/hung nft must not block LuCI rules load).
|
||||
NFT_TIMEOUT=5
|
||||
# Bound poll log.read (wedged logd must not hold an rpcd worker past the UI cadence).
|
||||
# Integer seconds — BusyBox timeout truncates fractions to 0 (see RESOLVE_TIMEOUT).
|
||||
POLL_TIMEOUT=5
|
||||
# Host tests override this to a pinned libubox helper; devices keep the package path.
|
||||
: "${FWLIVE_JSHN_SH:=/usr/share/libubox/jshn.sh}"
|
||||
|
||||
# json_escape lives in fwlive-logging.sh so prerm can source that file alone.
|
||||
# shellcheck disable=SC1090,SC1091 # LOGGING_SH resolves to sibling ../fwlive-logging.sh
|
||||
@@ -170,15 +175,70 @@ $(uci_rule_names)
|
||||
EOF
|
||||
}
|
||||
|
||||
# One awk pass: prefix<TAB>comment. Last quoted log prefix / comment on
|
||||
# the line, including \" units (same capture as the former sed).
|
||||
nft_dump_fields() {
|
||||
LC_ALL=C awk '
|
||||
function last_at(s, needle, p, i, found) {
|
||||
found = 0
|
||||
p = 1
|
||||
while ((i = index(substr(s, p), needle)) > 0) {
|
||||
found = p + i - 1
|
||||
p = found + 1
|
||||
}
|
||||
return found
|
||||
}
|
||||
function quoted_at(s, needle, i, rest, out, c, n, esc, j) {
|
||||
i = last_at(s, needle)
|
||||
if (i == 0)
|
||||
return ""
|
||||
rest = substr(s, i + length(needle))
|
||||
n = length(rest)
|
||||
out = ""
|
||||
esc = 0
|
||||
for (j = 1; j <= n; j++) {
|
||||
c = substr(rest, j, 1)
|
||||
if (esc) {
|
||||
out = out c
|
||||
esc = 0
|
||||
continue
|
||||
}
|
||||
if (c == "\\") {
|
||||
out = out c
|
||||
esc = 1
|
||||
continue
|
||||
}
|
||||
if (c == "\"")
|
||||
return out
|
||||
out = out c
|
||||
}
|
||||
return ""
|
||||
}
|
||||
{
|
||||
prefix = quoted_at($0, "log prefix \"")
|
||||
comment = quoted_at($0, "comment \"")
|
||||
if (prefix == "" && comment == "")
|
||||
next
|
||||
print prefix "\t" comment
|
||||
}
|
||||
'
|
||||
}
|
||||
|
||||
# pass=labeled|unlabeled|all — labeled before unlabeled so !fw4: beats
|
||||
# an earlier cosmetic for the same prefix under first-wins.
|
||||
# Reads TSV from nft_dump_fields; stops once the map cap trips.
|
||||
map_from_nft_stream() {
|
||||
pass="${1:-all}"
|
||||
while IFS= read -r line; do
|
||||
# Capture [^"]* plus escaped-quote units (\"...) so a log prefix or
|
||||
# comment containing an escaped quote is not truncated at the quote.
|
||||
prefix=$(echo "$line" | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
|
||||
comment=$(echo "$line" | sed -n 's/.*comment "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
|
||||
_tab=$(printf '\t')
|
||||
# IFS= keeps a leading tab (empty prefix + comment-only !fw4: line).
|
||||
while IFS= read -r line || [ -n "$line" ]; do
|
||||
[ -n "$_map_overflow" ] && break
|
||||
[ -n "$line" ] || continue
|
||||
prefix="${line%%${_tab}*}"
|
||||
case "$line" in
|
||||
*"$_tab"*) comment="${line#*"$_tab"}" ;;
|
||||
*) comment="" ;;
|
||||
esac
|
||||
label=''
|
||||
|
||||
case "$comment" in
|
||||
@@ -209,14 +269,6 @@ nft_list_ruleset() {
|
||||
run_with_timeout "$NFT_TIMEOUT" nft list ruleset
|
||||
}
|
||||
|
||||
detect_firewall_backend() {
|
||||
if command -v nft >/dev/null 2>&1 && nft_list_ruleset >/dev/null; then
|
||||
printf '%s' nft
|
||||
return 0
|
||||
fi
|
||||
printf '%s' unknown
|
||||
}
|
||||
|
||||
_fwlive_tmp_dir_ok() {
|
||||
# Fail closed unless the dump dir is a real directory with the sticky
|
||||
# bit. Without sticky, another uid can unlink a just-created mktemp
|
||||
@@ -251,15 +303,29 @@ enrich_rules_dump() {
|
||||
|
||||
_rules_dump=$(_fwlive_mktemp "$_dump_prefix") || _rules_dump=''
|
||||
if [ -n "$_rules_dump" ]; then
|
||||
# One dump serves detect + parse. Empty/failed dump is not nft
|
||||
# (unknown/no_backend, same as a failed detect probe).
|
||||
if nft_list_ruleset >"$_rules_dump" 2>/dev/null; then
|
||||
map_from_nft_stream labeled <"$_rules_dump"
|
||||
map_from_nft_stream unlabeled <"$_rules_dump"
|
||||
elif [ -z "$_enrich_error" ]; then
|
||||
_enrich_error='nft_failed'
|
||||
backend=nft
|
||||
# Stream awk output to a second temp so a huge dump is
|
||||
# not materialized in the rpcd worker's shell heap.
|
||||
_nft_tsv=$(_fwlive_mktemp "fwlive-nft-tsv")
|
||||
if [ -z "$_nft_tsv" ]; then
|
||||
_enrich_error='mktemp_failed'
|
||||
else
|
||||
nft_dump_fields <"$_rules_dump" >"$_nft_tsv"
|
||||
map_from_nft_stream labeled <"$_nft_tsv"
|
||||
if [ -z "$_map_overflow" ]; then
|
||||
map_from_nft_stream unlabeled <"$_nft_tsv"
|
||||
fi
|
||||
rm -f "$_nft_tsv"
|
||||
fi
|
||||
fi
|
||||
rm -f "$_rules_dump"
|
||||
else
|
||||
# nft is on PATH (caller checked); skip enrichment without a dump.
|
||||
_enrich_error='mktemp_failed'
|
||||
backend=nft
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -268,13 +334,14 @@ build_rules_map() {
|
||||
_map_keys=0
|
||||
_map_overflow=''
|
||||
_enrich_error=''
|
||||
backend=$(detect_firewall_backend)
|
||||
backend=unknown
|
||||
|
||||
# UCI first, then labeled stream prefixes, then unlabeled cosmetics
|
||||
# First-wins keeps UCI over nft and !fw4: over cosmetic.
|
||||
map_uci_rule_names
|
||||
|
||||
if [ "$backend" = nft ]; then
|
||||
# Temp file first, then a single nft_list_ruleset; backend from success.
|
||||
if command -v nft >/dev/null 2>&1; then
|
||||
enrich_rules_dump fwlive-nft
|
||||
fi
|
||||
|
||||
@@ -301,7 +368,8 @@ poll_clamp_lines() {
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
_digits=$(printf '%s' "$_val" | sed 's/^0*//')
|
||||
# Strip leading zeros in-shell so default-50 poll does not pay for sed.
|
||||
_digits=${_val#${_val%%[!0]*}}
|
||||
if [ -z "$_digits" ]; then
|
||||
printf '%s' "50"
|
||||
return 0
|
||||
@@ -317,33 +385,52 @@ poll_clamp_lines() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Failed jshn reads still default to 50 (silent numeric contract; syslog named cause only).
|
||||
poll_lines_from_input() {
|
||||
input="$1"
|
||||
lines=50
|
||||
_cause=
|
||||
|
||||
if ! command -v jshn >/dev/null 2>&1; then
|
||||
printf '%s' "$lines"
|
||||
return 0
|
||||
_cause=jshn_missing
|
||||
else
|
||||
_rc=0
|
||||
# jshn uses intentionally unset state variables. Keep nounset disabled in
|
||||
# this subshell so strict mode is restored automatically on every exit.
|
||||
# exit 2: library missing or source failed; exit 1: JSON parse failed.
|
||||
# Missing addresses[0] is an intentional silent default, not a parse error.
|
||||
_first=$( (
|
||||
set +u
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "${FWLIVE_JSHN_SH:-/usr/share/libubox/jshn.sh}" 2>/dev/null || exit 2
|
||||
json_init >/dev/null 2>&1 || exit 2
|
||||
# json_load masks a failing jshn exit through eval. Check the real
|
||||
# parser once, then evaluate only its generated shell assignments.
|
||||
_json_no_warning=1
|
||||
_json_code=$(jshn -r "$input" 2>/dev/null) || exit 1
|
||||
eval "$_json_code" || exit 1
|
||||
first=
|
||||
if json_select addresses 2>/dev/null; then
|
||||
json_get_var first 1 || first=
|
||||
fi
|
||||
printf '%s' "${first:-}"
|
||||
) ) || _rc=$?
|
||||
if [ "$_rc" -eq 2 ]; then
|
||||
_cause=jshn_lib_missing
|
||||
elif [ "$_rc" -ne 0 ]; then
|
||||
_cause=invalid_input
|
||||
else
|
||||
case "${_first:-}" in
|
||||
''|*[!0-9]*) ;;
|
||||
*) lines="$_first" ;;
|
||||
esac
|
||||
fi
|
||||
fi
|
||||
|
||||
# jshn uses intentionally unset state variables. Keep nounset disabled in
|
||||
# this subshell so strict mode is restored automatically on every exit.
|
||||
_first=$( (
|
||||
set +u
|
||||
. /usr/share/libubox/jshn.sh
|
||||
# json_load masks a failing jshn exit through eval. Check the real
|
||||
# parser once, then evaluate only its generated shell assignments.
|
||||
_json_no_warning=1
|
||||
_json_code=$(jshn -r "$input" 2>/dev/null) || exit 1
|
||||
eval "$_json_code" || exit 1
|
||||
if ! json_select addresses 2>/dev/null; then exit 1; fi
|
||||
json_get_var first 1 || exit 1
|
||||
printf '%s' "${first:-}"
|
||||
) ) || _first=
|
||||
case "${_first:-}" in
|
||||
''|*[!0-9]*) ;;
|
||||
*) lines="$_first" ;;
|
||||
esac
|
||||
# Named cause goes to syslog only: poll JSON "error" is connection-lost in the view.
|
||||
if [ -n "$_cause" ]; then
|
||||
logger -t fwlive "$_cause" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
lines=$(poll_clamp_lines "$lines")
|
||||
|
||||
@@ -360,7 +447,7 @@ fetch_firewall_logs() {
|
||||
# Adaptive plan prints: lines shed bucket
|
||||
start_cs=$(fwlive_adaptive_clock_cs)
|
||||
# Drop dead oneshot flag — only consumed on logd's stream branch.
|
||||
if ! raw=$(ubus call log read "{\"lines\":$lines,\"stream\":false}" 2>/dev/null); then
|
||||
if ! raw=$(run_with_timeout "$POLL_TIMEOUT" ubus call log read "{\"lines\":$lines,\"stream\":false}"); then
|
||||
# Do not record ~0 ms as cold: failure is not healthy fast processing
|
||||
# and must not clear an existing hot/shed cap.
|
||||
out='{"log":[],"error":"log_read_failed"}'
|
||||
@@ -383,7 +470,7 @@ fetch_firewall_logs() {
|
||||
# Empty success is not valid poll JSON — the shipped filter always
|
||||
# prints {"log":[...]}, but a stub/regression must not emit a blank body.
|
||||
_filter_ok=1
|
||||
if ! out=$(printf '%s' "$raw" | "$FILTER_SH" /tmp); then
|
||||
if ! out=$(printf '%s' "$raw" | run_with_timeout "$POLL_TIMEOUT" "$FILTER_SH" /tmp); then
|
||||
_filter_ok=0
|
||||
if [ -z "$out" ]; then
|
||||
out='{"log":[],"error":"filter_failed"}'
|
||||
@@ -423,16 +510,29 @@ poll_logs() {
|
||||
# "<ptr> name = <host>"; fall back to "Address N: <ip> <host>".
|
||||
# Trailing dots are stripped. Empty / server-only output is not a name.
|
||||
parse_nslookup_name() {
|
||||
# Prefer "<ptr> name = <host>". The "Address N: ip host" fallback
|
||||
# must ignore the Server block (BusyBox mini nslookup prints the
|
||||
# resolver via the same Address N: format first). Only accept
|
||||
# Address N: after a blank line (the answer section).
|
||||
# Prefer "<ptr> name = <host>". "name =" must be a whole token
|
||||
# (not a suffix of hostname, not the phrase "domain name =").
|
||||
# The "Address N: ip host" fallback must ignore the Server
|
||||
# block (BusyBox mini nslookup prints the resolver via the
|
||||
# same Address N: format first). Only accept Address N: after
|
||||
# a blank line (the answer section).
|
||||
printf '%s\n' "$1" | awk '
|
||||
tolower($0) ~ /name[ \t]*=/ {
|
||||
sub(/.*[Nn][Aa][Mm][Ee][ \t]*=[ \t]*/, "")
|
||||
gsub(/[ \t].*$/, "")
|
||||
gsub(/\.$/, "")
|
||||
if ($0 != "") { print; exit }
|
||||
{
|
||||
low = tolower($0)
|
||||
host = ""
|
||||
if (match(low, /^name[ \t]*=[ \t]*/)) {
|
||||
host = substr($0, RLENGTH + 1)
|
||||
} else if (match(low, /[^a-z0-9_]name[ \t]*=[ \t]*/)) {
|
||||
pre = substr(low, 1, RSTART - 1)
|
||||
sub(/[ \t]+$/, "", pre)
|
||||
if (pre != "domain" && pre !~ /[^a-z0-9_]domain$/)
|
||||
host = substr($0, RSTART + RLENGTH)
|
||||
}
|
||||
if (host != "") {
|
||||
gsub(/[ \t].*$/, "", host)
|
||||
gsub(/\.$/, "", host)
|
||||
if (host != "") { print host; exit }
|
||||
}
|
||||
}
|
||||
NF == 0 { in_answer = 1; next }
|
||||
in_answer && $1 == "Address" && $2 ~ /^[0-9]+:/ && NF >= 4 {
|
||||
@@ -575,7 +675,8 @@ resolve_addresses() {
|
||||
|
||||
_addresses=$( (
|
||||
set +u
|
||||
. /usr/share/libubox/jshn.sh
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "${FWLIVE_JSHN_SH:-/usr/share/libubox/jshn.sh}"
|
||||
# json_load masks a failing jshn exit through eval. Check the real
|
||||
# parser once, then evaluate only its generated shell assignments.
|
||||
_json_no_warning=1
|
||||
@@ -752,19 +853,23 @@ run_selftest() {
|
||||
|
||||
# Escaped-quote capture: a log prefix containing \" must not be truncated
|
||||
# at the escaped quote (nft dumps strings with backslash escapes).
|
||||
got=$(printf '%s\n' 'log prefix "My \" Rule"' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
|
||||
_tab=$(printf '\t')
|
||||
got=$(printf '%s\n' 'log prefix "My \" Rule"' | nft_dump_fields)
|
||||
got="${got%%${_tab}*}"
|
||||
if [ "$got" != 'My \" Rule' ]; then
|
||||
echo "escaped-quote prefix capture: expected 'My \\\" Rule' got '$got'" >&2
|
||||
return 1
|
||||
fi
|
||||
# Plain prefix still captures unchanged.
|
||||
got=$(printf '%s\n' 'log prefix "Plain Rule"' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
|
||||
got=$(printf '%s\n' 'log prefix "Plain Rule"' | nft_dump_fields)
|
||||
got="${got%%${_tab}*}"
|
||||
if [ "$got" != 'Plain Rule' ]; then
|
||||
echo "plain prefix capture: expected 'Plain Rule' got '$got'" >&2
|
||||
return 1
|
||||
fi
|
||||
# Empty prefix edge.
|
||||
got=$(printf '%s\n' 'log prefix ""' | sed -n 's/.*log prefix "\([^"\\]*\(\\.[^"\\]*\)*\)".*/\1/p')
|
||||
got=$(printf '%s\n' 'log prefix ""' | nft_dump_fields)
|
||||
got="${got%%${_tab}*}"
|
||||
if [ "$got" != '' ]; then
|
||||
echo "empty prefix capture: expected '' got '$got'" >&2
|
||||
return 1
|
||||
@@ -774,6 +879,10 @@ run_selftest() {
|
||||
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "over-long expected $POLL_LINES_MAX got $got" || return 1
|
||||
got=$(poll_clamp_lines "0")
|
||||
check_eq poll_clamp_lines "$got" "50" "zero expected 50 got $got" || return 1
|
||||
got=$(poll_clamp_lines "000")
|
||||
check_eq poll_clamp_lines "$got" "50" "all-zeros expected 50 got $got" || return 1
|
||||
got=$(poll_clamp_lines "050")
|
||||
check_eq poll_clamp_lines "$got" "50" "leading-zero expected 50 got $got" || return 1
|
||||
got=$(poll_clamp_lines "2001")
|
||||
check_eq poll_clamp_lines "$got" "$POLL_LINES_MAX" "2001 expected $POLL_LINES_MAX got $got" || return 1
|
||||
got=$(poll_clamp_lines "500")
|
||||
@@ -816,6 +925,16 @@ Address 1: 8.8.8.8 dns.google")
|
||||
got=$(parse_nslookup_name "Server: 127.0.0.1
|
||||
Address: 127.0.0.1:53")
|
||||
check_eq parse_nslookup_name "$got" '' "server-only expected empty got '$got'" || return 1
|
||||
got=$(parse_nslookup_name "hostname = evil.example.")
|
||||
check_eq parse_nslookup_name "$got" '' "hostname = must not parse as name got '$got'" || return 1
|
||||
got=$(parse_nslookup_name "domain name = other.example.")
|
||||
check_eq parse_nslookup_name "$got" '' "domain name = must not parse as name got '$got'" || return 1
|
||||
got=$(parse_nslookup_name "domain name = two.example.")
|
||||
check_eq parse_nslookup_name "$got" '' "domain name = must not parse as name got '$got'" || return 1
|
||||
got=$(parse_nslookup_name "domain name = tab.example.")
|
||||
check_eq parse_nslookup_name "$got" '' "domain tab name = must not parse as name got '$got'" || return 1
|
||||
got=$(parse_nslookup_name "name = dns.google. hostname = evil.example.")
|
||||
check_eq parse_nslookup_name "$got" "dns.google" "standalone name = must beat hostname = got '$got'" || return 1
|
||||
|
||||
run_logging_selftest || return 1
|
||||
|
||||
@@ -834,6 +953,18 @@ Address: 127.0.0.1:53")
|
||||
echo "skip: jshn not available (poll cap via jshn not tested)" >&2
|
||||
return 0
|
||||
fi
|
||||
# Dash rejects libubox `export -n` in json_init. Skip numeric poll-cap
|
||||
# checks unless this interpreter can actually use the helper; poll still
|
||||
# defaults to 50 and logs a named cause in that case.
|
||||
if ! (
|
||||
set +u
|
||||
# shellcheck disable=SC1090,SC1091
|
||||
. "${FWLIVE_JSHN_SH:-/usr/share/libubox/jshn.sh}" 2>/dev/null || exit 1
|
||||
json_init >/dev/null 2>&1
|
||||
); then
|
||||
echo "skip: jshn library not usable (poll cap via jshn not tested)" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
got=$(poll_lines_from_input '{"addresses":["999999"]}')
|
||||
check_eq poll_lines_from_input "$got" "$POLL_LINES_MAX" "expected $POLL_LINES_MAX got $got" || return 1
|
||||
@@ -851,6 +982,64 @@ Address: 127.0.0.1:53")
|
||||
got=$(poll_lines_from_input '{"addresses":["100"]}')
|
||||
check_eq poll_lines_from_input "$got" "100" "expected 100 got $got" || return 1
|
||||
|
||||
# jshn failures must log a named cause; stdout stays the numeric default.
|
||||
# logger() override is inherited by the $() subshell; command -v still finds
|
||||
# the binary, so the unqualified logger call is what this intercepts.
|
||||
_poll_log="/tmp/fwlive-poll-cause.$$"
|
||||
: > "$_poll_log"
|
||||
logger() { printf '%s\n' "$*" >> "$_poll_log"; }
|
||||
|
||||
got=$(poll_lines_from_input '{"addresses":["500"]}')
|
||||
check_eq poll_lines_from_input "$got" "500" "override path expected 500 got $got" || return 1
|
||||
case "$(cat "$_poll_log")" in
|
||||
*jshn_missing*|*jshn_lib_missing*|*invalid_input*)
|
||||
echo "poll_lines_from_input: successful cap must stay silent, got $(cat "$_poll_log")" >&2
|
||||
rm -f "$_poll_log"
|
||||
unset -f logger
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
: > "$_poll_log"
|
||||
got=$(poll_lines_from_input '{}')
|
||||
check_eq poll_lines_from_input "$got" "50" "missing addresses expected 50 got $got" || return 1
|
||||
case "$(cat "$_poll_log")" in
|
||||
*jshn_missing*|*jshn_lib_missing*|*invalid_input*)
|
||||
echo "poll_lines_from_input: missing addresses[0] must stay silent, got $(cat "$_poll_log")" >&2
|
||||
rm -f "$_poll_log"
|
||||
unset -f logger
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
: > "$_poll_log"
|
||||
got=$(poll_lines_from_input 'not-json{{{')
|
||||
check_eq poll_lines_from_input "$got" "50" "invalid JSON expected 50 got $got" || return 1
|
||||
case "$(cat "$_poll_log")" in
|
||||
*invalid_input*) ;;
|
||||
*) echo "poll_lines_from_input: expected invalid_input log, got $(cat "$_poll_log")" >&2
|
||||
rm -f "$_poll_log"
|
||||
unset -f logger
|
||||
return 1 ;;
|
||||
esac
|
||||
|
||||
: > "$_poll_log"
|
||||
_saved_jshn_sh="$FWLIVE_JSHN_SH"
|
||||
FWLIVE_JSHN_SH="/no/such/fwlive-jshn.sh"
|
||||
got=$(poll_lines_from_input '{"addresses":["500"]}')
|
||||
FWLIVE_JSHN_SH="$_saved_jshn_sh"
|
||||
check_eq poll_lines_from_input "$got" "50" "missing library expected 50 got $got" || return 1
|
||||
case "$(cat "$_poll_log")" in
|
||||
*jshn_lib_missing*) ;;
|
||||
*) echo "poll_lines_from_input: expected jshn_lib_missing log, got $(cat "$_poll_log")" >&2
|
||||
rm -f "$_poll_log"
|
||||
unset -f logger
|
||||
return 1 ;;
|
||||
esac
|
||||
|
||||
rm -f "$_poll_log"
|
||||
unset -f logger
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -863,6 +1052,10 @@ case "${1:-}" in
|
||||
poll_clamp_lines "${2:-}"
|
||||
exit $?
|
||||
;;
|
||||
__poll_lines)
|
||||
poll_lines_from_input "${2:-}"
|
||||
exit $?
|
||||
;;
|
||||
__tmp_dir_ok)
|
||||
_fwlive_tmp_dir_ok "${2:-}"
|
||||
exit $?
|
||||
|
||||
@@ -6,11 +6,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI support for KSpeeder
|
||||
LUCI_DEPENDS:=+istoreenhance
|
||||
LUCI_DEPENDS:=+istoreenhance +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_PKGARCH:=all
|
||||
PKG_VERSION:=0.6.6-r2
|
||||
# PKG_RELEASE MUST be empty for luci.mk
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=0.8.0
|
||||
PKG_RELEASE:=3
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
module("luci.controller.istoreenhance", package.seeall)
|
||||
|
||||
function index()
|
||||
if not nixio.fs.access("/etc/config/istoreenhance") then
|
||||
return
|
||||
end
|
||||
if not nixio.fs.access("/etc/config/istoreenhance") then return end
|
||||
|
||||
entry({"admin", "services", "istoreenhance"}, cbi("istoreenhance"), _("KSpeeder"), 20).dependent = true
|
||||
|
||||
entry({"admin", "services", "istoreenhance_status"}, call("istoreenhance_status"))
|
||||
local open = entry({"admin", "services", "istoreenhance", "open"}, call("istoreenhance_open"))
|
||||
open.leaf = true
|
||||
open.dependent = false
|
||||
open.sysauth = false
|
||||
end
|
||||
|
||||
local function compat()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
return require("luci.model.linkease.apps_compat").new({
|
||||
http = require "luci.http",
|
||||
resolver = require("luci.model.linkease.apps_openwrt").new(),
|
||||
auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth")
|
||||
})
|
||||
end
|
||||
|
||||
function istoreenhance_status()
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = tonumber(uci:get_first("istoreenhance", "istoreenhance", "adminport"))
|
||||
|
||||
local status = {
|
||||
running = (sys.call("pidof iStoreEnhance >/dev/null") == 0),
|
||||
port = (port or 5003)
|
||||
}
|
||||
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(status)
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
compat():legacy_status("kspeeder", {
|
||||
running = sys.call("pidof iStoreEnhance >/dev/null") == 0,
|
||||
port = uci:get_first("istoreenhance", "istoreenhance", "adminport") or "5003"
|
||||
})
|
||||
end
|
||||
|
||||
function istoreenhance_open()
|
||||
compat():open("kspeeder")
|
||||
end
|
||||
@@ -8,7 +8,7 @@
|
||||
if (st.running)
|
||||
{
|
||||
tb.innerHTML = '<br/><em style=\"color:green\"><%:The KSpeeder service is running.%></em>'
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open KSpeeder%> \" onclick=\"window.open('http://" + window.location.hostname + ":" + st.port + "/')\"/>";
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open KSpeeder%> \" onclick=\"window.open('<%=url("admin/services/istoreenhance/open")%>')\"/>";
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=kai
|
||||
PKG_VERSION:=1.0.2
|
||||
PKG_RELEASE:=10
|
||||
LUCI_DEPENDS:=+kai +kai_session +luci-compat
|
||||
PKG_RELEASE:=11
|
||||
LUCI_DEPENDS:=+kai +luci-compat +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
|
||||
@@ -16,10 +16,14 @@ end
|
||||
function kai_status()
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = tonumber(uci:get_first("kai", "kai", "port")) or 8197
|
||||
if port < 1 or port > 65535 or port % 1 ~= 0 then
|
||||
port = 8197
|
||||
end
|
||||
local status = {
|
||||
running = (sys.call("pidof kai_bin >/dev/null") == 0),
|
||||
port = 8197
|
||||
port = port
|
||||
}
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(status)
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(status)
|
||||
end
|
||||
@@ -1,4 +1,6 @@
|
||||
<script <script type="text/javascript">//<![CDATA[
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var kaiOpenUrl = '<%=url("admin/services/linkease_apps/open")%>?id=kai';
|
||||
|
||||
XHR.poll(5, '<%=url("admin/services/kai_status")%>', null,function(x, st){
|
||||
var el = document.getElementById('kai_status');
|
||||
if (st && el){
|
||||
@@ -7,7 +9,7 @@ XHR.poll(5, '<%=url("admin/services/kai_status")%>', null,function(x, st){
|
||||
}
|
||||
if (st.running){
|
||||
el.innerHTML = '<br/><em style=\"color:green\"><%:The KAI service is running.%></em>'
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open KAI%> \" onclick=\"window.open('http://" + window.location.hostname + ":" + st.port + "/')\"/>";
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open KAI%> \" onclick=\"window.open(kaiOpenUrl, '_blank', 'noopener')\"/>";
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -19,4 +21,3 @@ XHR.poll(5, '<%=url("admin/services/kai_status")%>', null,function(x, st){
|
||||
<em><%:Collecting data...%></em>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
@@ -8,8 +8,8 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=KaiPlus
|
||||
PKG_VERSION:=1.0.9
|
||||
PKG_RELEASE:=10
|
||||
LUCI_DEPENDS:=+kaiplus +luci-compat +luci-lib-linkeaseauth
|
||||
PKG_RELEASE:=11
|
||||
LUCI_DEPENDS:=+kaiplus +luci-compat +luci-lib-linkeaseauth +linkease-app-entry
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
module("luci.controller.kaiplus", package.seeall)
|
||||
|
||||
local APPS_PROXY_PREFIX = "/apps=http://127.0.0.1:19290"
|
||||
|
||||
function index()
|
||||
entry({"admin", "services", "kaiplus_status"}, call("kaiplus_status"))
|
||||
local open = entry({"admin", "services", "kaiplus", "open"}, call("kaiplus_open"))
|
||||
@@ -10,188 +8,34 @@ function index()
|
||||
open.sysauth = "root"
|
||||
open.sysauth_authenticator = "htmlauth"
|
||||
|
||||
if not nixio.fs.access("/etc/config/kaiplus") then
|
||||
return
|
||||
end
|
||||
local page
|
||||
page = entry({"admin", "services", "kaiplus"}, cbi("kaiplus"), _("KaiPlus"), 100)
|
||||
page.dependent = true
|
||||
if not nixio.fs.access("/etc/config/kaiplus") then return end
|
||||
entry({"admin", "services", "kaiplus"}, cbi("kaiplus"), _("KaiPlus"), 100).dependent = true
|
||||
end
|
||||
|
||||
local function uhttpd_has_apps_proxy_prefix()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local mappings = uci:get_list("uhttpd", "main", "proxy_prefix") or {}
|
||||
|
||||
for _, mapping in ipairs(mappings) do
|
||||
if mapping == APPS_PROXY_PREFIX then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function uhttpd_supports_proxy_prefix()
|
||||
local sys = require "luci.sys"
|
||||
return sys.call("grep -qr 'proxy_prefix' /etc/init.d/uhttpd /lib/functions /usr/share/uhttpd 2>/dev/null") == 0
|
||||
end
|
||||
|
||||
local function uhttpd_apps_proxy_available()
|
||||
return uhttpd_supports_proxy_prefix() and uhttpd_has_apps_proxy_prefix()
|
||||
end
|
||||
|
||||
local function linkeasefull_running()
|
||||
local sys = require "luci.sys"
|
||||
return sys.call("[ -x /etc/init.d/linkeasefull ] && /etc/init.d/linkeasefull running >/dev/null 2>&1") == 0
|
||||
end
|
||||
|
||||
local function normalized_base_path(path)
|
||||
path = path or "/apps/kaiplus/"
|
||||
if path:sub(1, 1) ~= "/" then
|
||||
path = "/" .. path
|
||||
end
|
||||
if path:sub(-1) ~= "/" then
|
||||
path = path .. "/"
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
local function cookie_encode(value)
|
||||
return tostring(value or ""):gsub("([^A-Za-z0-9._~-])", function(char)
|
||||
return string.format("%%%02X", char:byte())
|
||||
end)
|
||||
end
|
||||
|
||||
local function authority_host(authority)
|
||||
if not authority or authority == "" then
|
||||
return ""
|
||||
end
|
||||
if authority:sub(1, 1) == "[" then
|
||||
return authority:match("^%[([^%]]+)%]") or ""
|
||||
end
|
||||
return authority:match("^([^:]+)") or authority
|
||||
end
|
||||
|
||||
local function url_authority(host, port)
|
||||
if not host or host == "" then
|
||||
host = "127.0.0.1"
|
||||
end
|
||||
if host:find(":") and host:sub(1, 1) ~= "[" then
|
||||
host = "[" .. host .. "]"
|
||||
end
|
||||
return host .. ":" .. tostring(port)
|
||||
end
|
||||
|
||||
local function request_or_lan_host()
|
||||
local http = require "luci.http"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local host = authority_host(http.getenv("HTTP_HOST") or "")
|
||||
if host ~= "" then
|
||||
return host
|
||||
end
|
||||
return uci:get("network", "lan", "ipaddr") or "127.0.0.1"
|
||||
end
|
||||
|
||||
local function linkease_auth_url(name)
|
||||
local function compat()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
return dispatcher.build_url("admin", "services", "linkease_auth", name)
|
||||
end
|
||||
|
||||
local function kaiplus_config()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = uci:get_first("kaiplus", "kaiplus", "port", "8189")
|
||||
local base_path = normalized_base_path(uci:get_first("kaiplus", "kaiplus", "base_path"))
|
||||
local external_port_enabled = uci:get_first("kaiplus", "kaiplus", "external_port_enabled") == "1"
|
||||
local listen_mode = uci:get_first("kaiplus", "kaiplus", "listen_mode") or "auto"
|
||||
local socket_path = uci:get_first("kaiplus", "kaiplus", "socket_path") or "/var/run/kaiplus.sock"
|
||||
return port, base_path, external_port_enabled, listen_mode, socket_path
|
||||
end
|
||||
|
||||
local function kaiplus_entry_url()
|
||||
local port, base_path, external_port_enabled = kaiplus_config()
|
||||
if linkeasefull_running() and uhttpd_apps_proxy_available() then
|
||||
return base_path
|
||||
end
|
||||
if not external_port_enabled then
|
||||
return nil
|
||||
end
|
||||
return "http://" .. url_authority(request_or_lan_host(), port) .. base_path
|
||||
end
|
||||
|
||||
local function enable_external_port()
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local section = nil
|
||||
uci:foreach("kaiplus", "kaiplus", function(s)
|
||||
if not section then
|
||||
section = s[".name"]
|
||||
end
|
||||
end)
|
||||
if section then
|
||||
uci:set("kaiplus", section, "external_port_enabled", "1")
|
||||
uci:set("kaiplus", section, "listen_mode", "tcp")
|
||||
uci:commit("kaiplus")
|
||||
end
|
||||
local sys = require "luci.sys"
|
||||
sys.call("/etc/init.d/kaiplus restart >/dev/null 2>&1; sleep 1")
|
||||
end
|
||||
|
||||
local function render_enable_port_confirm()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
local confirm_url = dispatcher.build_url("admin", "services", "kaiplus", "open") .. "?enable_port=1"
|
||||
luci.http.prepare_content("text/html; charset=utf-8")
|
||||
luci.http.write("<!doctype html><html><head><meta charset='utf-8'><title>KaiPlus</title></head><body>")
|
||||
luci.http.write("<h2>KaiPlus</h2>")
|
||||
luci.http.write("<p>当前系统无法通过 LinkEase 桌面代理打开 KaiPlus。</p>")
|
||||
luci.http.write("<p>可以启用局域网端口 8189 后继续打开。</p>")
|
||||
luci.http.write("<p><a class='cbi-button cbi-button-apply' href='" .. confirm_url .. "'>启用 8189 并打开</a></p>")
|
||||
luci.http.write("</body></html>")
|
||||
end
|
||||
|
||||
local function render_open_failed()
|
||||
luci.http.prepare_content("text/html; charset=utf-8")
|
||||
luci.http.write("<!doctype html><html><head><meta charset='utf-8'><title>KaiPlus</title></head><body>")
|
||||
luci.http.write("<h2>KaiPlus</h2>")
|
||||
luci.http.write("<p>KaiPlus 已安装,但当前没有可用访问入口。</p>")
|
||||
luci.http.write("<p>请启用 LinkEaseFull 的 /apps 代理,或在 KaiPlus 设置中开放 8189 端口。</p>")
|
||||
luci.http.write("</body></html>")
|
||||
return require("luci.model.linkease.apps_compat").new({
|
||||
http = require "luci.http",
|
||||
resolver = require("luci.model.linkease.apps_openwrt").new(),
|
||||
auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth")
|
||||
})
|
||||
end
|
||||
|
||||
function kaiplus_status()
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port, base_path, external_port_enabled, listen_mode, socket_path = kaiplus_config()
|
||||
local entry_url = kaiplus_entry_url()
|
||||
local status = {
|
||||
running = (sys.call("pidof kaiplus_bin >/dev/null") == 0),
|
||||
port = port,
|
||||
base_path = base_path,
|
||||
listen_mode = listen_mode,
|
||||
socket_path = socket_path,
|
||||
local uci = require("luci.model.uci").cursor()
|
||||
local external_port_enabled = uci:get_first("kaiplus", "kaiplus", "external_port_enabled") == "1"
|
||||
compat():legacy_status("kaiplus", {
|
||||
running = sys.call("pidof kaiplus_bin >/dev/null") == 0,
|
||||
port = uci:get_first("kaiplus", "kaiplus", "port") or "8189",
|
||||
base_path = uci:get_first("kaiplus", "kaiplus", "base_path") or "/apps/kaiplus/",
|
||||
listen_mode = uci:get_first("kaiplus", "kaiplus", "listen_mode") or "auto",
|
||||
socket_path = uci:get_first("kaiplus", "kaiplus", "socket_path") or "/var/run/kaiplus.sock",
|
||||
external_port_enabled = external_port_enabled,
|
||||
entry_url = entry_url or "",
|
||||
lan_ip = uci:get("network", "lan", "ipaddr") or "",
|
||||
proxy_prefix_supported = uhttpd_supports_proxy_prefix(),
|
||||
proxy_prefix_enabled = uhttpd_apps_proxy_available(),
|
||||
linkeasefull_running = linkeasefull_running()
|
||||
}
|
||||
luci.http.prepare_content("application/json")
|
||||
luci.http.write_json(status)
|
||||
lan_ip = uci:get("network", "lan", "ipaddr") or ""
|
||||
})
|
||||
end
|
||||
|
||||
function kaiplus_open()
|
||||
local http = require "luci.http"
|
||||
if http.formvalue("enable_port") == "1" then
|
||||
enable_external_port()
|
||||
end
|
||||
|
||||
local entry_url = kaiplus_entry_url()
|
||||
if entry_url then
|
||||
http.redirect(linkease_auth_url("auth") .. "?return=" .. cookie_encode(entry_url))
|
||||
return
|
||||
end
|
||||
|
||||
if http.formvalue("enable_port") == "1" then
|
||||
render_open_failed()
|
||||
return
|
||||
end
|
||||
render_enable_port_confirm()
|
||||
compat():open("kaiplus")
|
||||
end
|
||||
@@ -5,7 +5,7 @@ XHR.poll(5, '<%=url("admin/services/kaiplus_status")%>', null, function(x, st) {
|
||||
var status = st.running
|
||||
? '<br/><em style=\"color:green\"><%:The KaiPlus service is running.%></em>'
|
||||
: '<br/><em style=\"color:red\"><%:The KaiPlus service is not running.%></em>';
|
||||
if (st.proxy_prefix_enabled && st.linkeasefull_running) {
|
||||
if (st.proxy_prefix_enabled && st.app_entry_running) {
|
||||
status = status + '<br/><em><%:Access mode: LinkEase Desktop proxy.%></em>';
|
||||
}
|
||||
else if (st.external_port_enabled) {
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
#
|
||||
# Copyright (C) 2026 iStoreOS
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-linkeasefull-embed
|
||||
PKG_VERSION:=1.1.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_USE_MIPS16:=0
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=luci
|
||||
CATEGORY:=LuCI
|
||||
SUBMENU:=3. Applications
|
||||
TITLE:=LinkEaseFull LuCI desktop embed
|
||||
DEPENDS:=+linkeasefull +luci-base +luci-theme-istorenas
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
Register OpenWrt LuCI as a LinkEaseFull desktop module and provide embedded-mode assets.
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/bin/sh
|
||||
set -e
|
||||
root="$${IPKG_INSTROOT:-}"
|
||||
maintainer="$${root}/usr/libexec/linkeasefull/openwrt-luci-maintain"
|
||||
[ ! -x "$${maintainer}" ] || "$${maintainer}" install "$${root}"
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
set -e
|
||||
root="$${IPKG_INSTROOT:-}"
|
||||
maintainer="$${root}/usr/libexec/linkeasefull/openwrt-luci-maintain"
|
||||
[ ! -x "$${maintainer}" ] || "$${maintainer}" remove "$${root}"
|
||||
registration="$${root}/usr/share/linkeasefull/desktop-apps.d/15-openwrt-luci.json"
|
||||
if [ "$$(readlink "$${registration}" 2>/dev/null)" = "/usr/share/linkeasefull/openwrt-luci/openwrt-luci.json" ]; then
|
||||
rm -f "$${registration}"
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Configure
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/share/linkeasefull/openwrt-luci/protocol-fallbacks $(1)/usr/share/linkeasefull/desktop-apps.d
|
||||
$(INSTALL_DIR) $(1)/usr/libexec/linkeasefull
|
||||
$(INSTALL_DIR) $(1)/www/luci-static/linkeasefull-embed
|
||||
$(INSTALL_DATA) ./files/openwrt-luci/openwrt-luci.json $(1)/usr/share/linkeasefull/openwrt-luci/openwrt-luci.json
|
||||
$(INSTALL_DATA) ./files/openwrt-luci/openwrt.svg $(1)/usr/share/linkeasefull/openwrt-luci/openwrt.svg
|
||||
$(INSTALL_DATA) ./files/www/embed.css $(1)/www/luci-static/linkeasefull-embed/embed.css
|
||||
$(INSTALL_DATA) ./files/www/embed-prelude.js $(1)/www/luci-static/linkeasefull-embed/embed-prelude.js
|
||||
$(INSTALL_DATA) ./files/protocol/directip.js $(1)/usr/share/linkeasefull/openwrt-luci/protocol-fallbacks/directip.js
|
||||
$(INSTALL_DATA) ./files/protocol/bonding.js $(1)/usr/share/linkeasefull/openwrt-luci/protocol-fallbacks/bonding.js
|
||||
$(INSTALL_DATA) ./files/protocol/wwan.js $(1)/usr/share/linkeasefull/openwrt-luci/protocol-fallbacks/wwan.js
|
||||
$(INSTALL_BIN) ./files/openwrt-luci-maintain.sh $(1)/usr/libexec/linkeasefull/openwrt-luci-maintain
|
||||
ln -sf /usr/share/linkeasefull/openwrt-luci/openwrt-luci.json $(1)/usr/share/linkeasefull/desktop-apps.d/15-openwrt-luci.json
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,30 @@
|
||||
# luci-app-linkeasefull-embed
|
||||
|
||||
Registers OpenWrt LuCI as a LinkEaseFull desktop module without using an iframe.
|
||||
It is installed by the `app-meta-linkeasefull` product bundle while remaining
|
||||
separate from the independently installable `linkeasefull` runtime package.
|
||||
|
||||
This package installs:
|
||||
|
||||
- `/usr/share/linkeasefull/openwrt-luci/openwrt-luci.json`
|
||||
- `/usr/share/linkeasefull/desktop-apps.d/15-openwrt-luci.json`
|
||||
- `/www/luci-static/linkeasefull-embed/embed-prelude.js`
|
||||
- `/www/luci-static/linkeasefull-embed/embed.css`
|
||||
- privately owned protocol fallback shims under
|
||||
`/usr/share/linkeasefull/openwrt-luci/protocol-fallbacks/`
|
||||
|
||||
The package post-install maintainer exposes a fallback at the corresponding
|
||||
`/www/luci-static/resources/protocol/` path only when no real LuCI protocol
|
||||
module exists. Existing modules are never replaced, and package removal only
|
||||
removes symlinks that still point to this package's private fallback directory.
|
||||
This avoids file ownership conflicts with protocol packages installed later.
|
||||
|
||||
The desktop manifest uses the builtin `LuciContainer` component with
|
||||
`isolation=scoped-dom-css`. That default scopes common Vue/React mount points,
|
||||
redirects dynamic styles and portal nodes into the LuCI container, and scopes CSS
|
||||
to `.le-luci-document`.
|
||||
|
||||
Users can create additional LuCI plugin desktop entries from LinkEaseFull by
|
||||
adding a name, an icon URL, and a LuCI URL under `/cgi-bin/luci/...`. Content-only
|
||||
plugin entries, such as DDNSTO, should use `contentOnly=true` so the LuCI menu is
|
||||
hidden and only the plugin content is shown.
|
||||
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
ACTION=${1:-}
|
||||
ROOT=${2:-}
|
||||
PRIVATE_DIR=/usr/share/linkeasefull/openwrt-luci/protocol-fallbacks
|
||||
PUBLIC_DIR=/www/luci-static/resources/protocol
|
||||
|
||||
case "$ROOT" in
|
||||
""|/*) ;;
|
||||
*) echo "invalid root: $ROOT" >&2; exit 2 ;;
|
||||
esac
|
||||
case "/$ROOT/" in
|
||||
*/../*) echo "invalid root: $ROOT" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
install_fallbacks() {
|
||||
mkdir -p "$ROOT$PUBLIC_DIR"
|
||||
for name in bonding.js directip.js wwan.js; do
|
||||
source="$ROOT$PRIVATE_DIR/$name"
|
||||
destination="$ROOT$PUBLIC_DIR/$name"
|
||||
[ -f "$source" ] || continue
|
||||
if [ ! -e "$destination" ] && [ ! -L "$destination" ]; then
|
||||
ln -s "$PRIVATE_DIR/$name" "$destination"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
remove_fallbacks() {
|
||||
for name in bonding.js directip.js wwan.js; do
|
||||
destination="$ROOT$PUBLIC_DIR/$name"
|
||||
if [ "$(readlink "$destination" 2>/dev/null || true)" = "$PRIVATE_DIR/$name" ]; then
|
||||
rm -f "$destination"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
case "$ACTION" in
|
||||
install) install_fallbacks ;;
|
||||
remove) remove_fallbacks ;;
|
||||
*) echo "usage: $0 {install|remove} [root]" >&2; exit 2 ;;
|
||||
esac
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "openwrt-luci",
|
||||
"name": "OpenWrt",
|
||||
"icon": "openwrt.svg",
|
||||
"staticRoot": "/usr/share/linkeasefull/openwrt-luci",
|
||||
"desktop": {
|
||||
"mode": "builtin",
|
||||
"component": "LuciContainer",
|
||||
"props": {
|
||||
"initialPath": "/cgi-bin/luci/admin/status/overview",
|
||||
"isolation": "scoped-dom-css",
|
||||
"preloadScripts": [
|
||||
"/luci-static/linkeasefull-embed/embed-prelude.js"
|
||||
],
|
||||
"preloadStyles": [
|
||||
"/luci-static/linkeasefull-embed/embed.css"
|
||||
]
|
||||
}
|
||||
},
|
||||
"standalone": {
|
||||
"url": "/cgi-bin/luci/"
|
||||
},
|
||||
"window": {
|
||||
"width": 1180,
|
||||
"height": 760,
|
||||
"singleton": true
|
||||
},
|
||||
"capabilities": ["openwrt", "luci", "settings"],
|
||||
"categories": ["settings", "openwrt", "luci"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="OpenWrt">
|
||||
<rect width="64" height="64" rx="14" fill="#0f766e"/>
|
||||
<path fill="#ffffff" d="M18 36.5c0-7.7 6.3-14 14-14s14 6.3 14 14h-5c0-4.9-4.1-9-9-9s-9 4.1-9 9h-5z"/>
|
||||
<path fill="#a7f3d0" d="M12 39h40v6H12z"/>
|
||||
<circle cx="20" cy="20" r="4" fill="#ccfbf1"/>
|
||||
<circle cx="44" cy="20" r="4" fill="#ccfbf1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 403 B |
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
'require network';
|
||||
|
||||
return network.registerProtocol('bonding', {
|
||||
getI18n: function() {
|
||||
return _('Bonding');
|
||||
},
|
||||
|
||||
getPackageName: function() {
|
||||
return 'luci-proto-bonding';
|
||||
},
|
||||
|
||||
isFloating: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
isVirtual: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
getDevices: function() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
'require network';
|
||||
|
||||
return network.registerProtocol('directip', {
|
||||
getI18n: function() {
|
||||
return _('Direct-IP');
|
||||
},
|
||||
|
||||
getPackageName: function() {
|
||||
return 'comgt-directip';
|
||||
},
|
||||
|
||||
isFloating: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
isVirtual: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
getDevices: function() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
'use strict';
|
||||
'require network';
|
||||
|
||||
return network.registerProtocol('wwan', {
|
||||
getI18n: function() {
|
||||
return _('WWAN');
|
||||
},
|
||||
|
||||
getPackageName: function() {
|
||||
return 'wwan';
|
||||
},
|
||||
|
||||
isFloating: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
isVirtual: function() {
|
||||
return true;
|
||||
},
|
||||
|
||||
getDevices: function() {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,403 @@
|
||||
'use strict';
|
||||
|
||||
(function() {
|
||||
window.__LINKEASE_LUCI_EMBEDDED__ = true;
|
||||
|
||||
function root() {
|
||||
return window.__LINKEASE_LUCI_ROOT__ ||
|
||||
document.querySelector('.le-luci-document') ||
|
||||
document;
|
||||
}
|
||||
|
||||
function query(selector) {
|
||||
return root().querySelector(selector);
|
||||
}
|
||||
|
||||
function queryAll(selector) {
|
||||
return root().querySelectorAll(selector);
|
||||
}
|
||||
|
||||
function config() {
|
||||
return window.__LINKEASE_LUCI_CONFIG__ || {};
|
||||
}
|
||||
|
||||
function isolationMode() {
|
||||
var mode = config().isolation;
|
||||
switch (mode) {
|
||||
case 'none':
|
||||
case 'scoped-dom':
|
||||
case 'scoped-dom-css':
|
||||
case 'strict':
|
||||
return mode;
|
||||
default:
|
||||
return 'scoped-dom-css';
|
||||
}
|
||||
}
|
||||
|
||||
function shouldScopeDOM() {
|
||||
return isolationMode() !== 'none';
|
||||
}
|
||||
|
||||
function shouldScopeDynamicNodes() {
|
||||
var mode = isolationMode();
|
||||
return mode === 'scoped-dom-css' || mode === 'strict';
|
||||
}
|
||||
|
||||
function shouldScopeCSS() {
|
||||
var mode = isolationMode();
|
||||
return mode === 'scoped-dom-css' || mode === 'strict';
|
||||
}
|
||||
|
||||
function shouldGuardGlobals() {
|
||||
return isolationMode() === 'strict';
|
||||
}
|
||||
|
||||
function ensureHost(name) {
|
||||
var scope = root();
|
||||
if (!scope || scope === document)
|
||||
return null;
|
||||
|
||||
var attr = 'data-linkease-luci-' + name + '-host';
|
||||
var host = scope.querySelector('[' + attr + ']');
|
||||
if (host)
|
||||
return host;
|
||||
|
||||
host = document.createElement('div');
|
||||
host.setAttribute(attr, 'true');
|
||||
if (name === 'style')
|
||||
host.hidden = true;
|
||||
scope.appendChild(host);
|
||||
return host;
|
||||
}
|
||||
|
||||
function findScopedId(scope, id) {
|
||||
if (scope.id === id)
|
||||
return scope;
|
||||
|
||||
var nodes = scope.getElementsByTagName ? scope.getElementsByTagName('*') : [];
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
if (nodes[i].id === id)
|
||||
return nodes[i];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function simpleIdSelector(selector) {
|
||||
var match = /^#([A-Za-z_][A-Za-z0-9_-]*)$/.exec(String(selector || '').trim());
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
function mountIds() {
|
||||
var defaults = ['root', 'app', 'app-root', 'mount', 'vue-app', 'react-root'];
|
||||
var configured = window.__LINKEASE_LUCI_CONFIG__ &&
|
||||
Array.isArray(window.__LINKEASE_LUCI_CONFIG__.mountIds)
|
||||
? window.__LINKEASE_LUCI_CONFIG__.mountIds
|
||||
: [];
|
||||
var seen = {};
|
||||
|
||||
return defaults.concat(configured).filter(function(id) {
|
||||
if (typeof id != 'string' || !id || seen[id])
|
||||
return false;
|
||||
|
||||
seen[id] = true;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function isMountId(id) {
|
||||
return mountIds().indexOf(id) >= 0;
|
||||
}
|
||||
|
||||
function getScopedElementById(id, fallback) {
|
||||
if (!shouldScopeDOM())
|
||||
return fallback();
|
||||
|
||||
if (!isMountId(id))
|
||||
return fallback();
|
||||
|
||||
if (window.__LINKEASE_LUCI_EMBEDDED__) {
|
||||
var scope = root();
|
||||
if (scope && scope !== document) {
|
||||
var scoped = findScopedId(scope, id);
|
||||
if (scoped)
|
||||
return scoped;
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return fallback();
|
||||
}
|
||||
|
||||
function installDocumentIdScope() {
|
||||
if (document.__linkeaseLuciIdScopeVersion >= 2)
|
||||
return;
|
||||
|
||||
var originalGetElementById =
|
||||
document.__linkeaseLuciOriginalGetElementById ||
|
||||
document.getElementById.bind(document);
|
||||
var originalQuerySelector =
|
||||
document.__linkeaseLuciOriginalQuerySelector ||
|
||||
document.querySelector.bind(document);
|
||||
|
||||
document.getElementById = function(id) {
|
||||
return getScopedElementById(String(id), function() {
|
||||
return originalGetElementById(id);
|
||||
});
|
||||
};
|
||||
|
||||
document.querySelector = function(selector) {
|
||||
var id = simpleIdSelector(selector);
|
||||
if (id) {
|
||||
return getScopedElementById(id, function() {
|
||||
return originalQuerySelector(selector);
|
||||
});
|
||||
}
|
||||
|
||||
return originalQuerySelector(selector);
|
||||
};
|
||||
|
||||
document.__linkeaseLuciOriginalGetElementById = originalGetElementById;
|
||||
document.__linkeaseLuciOriginalQuerySelector = originalQuerySelector;
|
||||
document.__linkeaseLuciIdScopeInstalled = true;
|
||||
document.__linkeaseLuciIdScopeVersion = 2;
|
||||
}
|
||||
|
||||
function isStylesheetNode(node) {
|
||||
if (!node || node.nodeType !== 1)
|
||||
return false;
|
||||
|
||||
var tag = node.tagName ? node.tagName.toLowerCase() : '';
|
||||
return tag === 'style' ||
|
||||
(tag === 'link' && /\bstylesheet\b/i.test(node.getAttribute('rel') || ''));
|
||||
}
|
||||
|
||||
function isPortalNode(node) {
|
||||
if (!node || node.nodeType !== 1)
|
||||
return false;
|
||||
|
||||
var value = [
|
||||
node.id || '',
|
||||
node.className || '',
|
||||
node.getAttribute('role') || '',
|
||||
node.getAttribute('data-testid') || ''
|
||||
].join(' ');
|
||||
|
||||
return /\b(modal|toast|message|notification|notify|overlay|popover|drawer|loading|tooltip)\b/i.test(value) ||
|
||||
/\b(dialog|alertdialog)\b/i.test(value);
|
||||
}
|
||||
|
||||
function rebaseCssUrls(css, baseHref) {
|
||||
return String(css || '').replace(/url\(\s*(['"]?)([^'")]+)\1\s*\)/gi, function(match, quote, rawUrl) {
|
||||
var url = String(rawUrl || '').trim();
|
||||
if (!url || url[0] === '/' || url[0] === '#' || /^[a-z][a-z0-9+.-]*:/i.test(url))
|
||||
return match;
|
||||
|
||||
try {
|
||||
return 'url("' + new URL(url, baseHref).href + '")';
|
||||
} catch (e) {
|
||||
return match;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function scopeSelector(selector) {
|
||||
var trimmed = String(selector || '').trim();
|
||||
if (!trimmed || trimmed.indexOf('.le-luci-document') === 0)
|
||||
return trimmed;
|
||||
|
||||
var rootScoped = trimmed.replace(/^(html|body|:root|#app)(?=$|[\s.#:[>+~])/, '.le-luci-document');
|
||||
if (rootScoped !== trimmed)
|
||||
return rootScoped;
|
||||
|
||||
return '.le-luci-document ' + trimmed;
|
||||
}
|
||||
|
||||
function scopeCss(css) {
|
||||
return String(css || '')
|
||||
.replace(/position\s*:\s*fixed/gi, 'position: absolute')
|
||||
.replace(/\b100vh\b|\b100vw\b/gi, '100%')
|
||||
.replace(/(^|})\s*([^@{}][^{]+)\{/g, function(match, close, selectorList) {
|
||||
var scoped = selectorList.split(',').map(scopeSelector).join(',');
|
||||
return close + scoped + '{';
|
||||
});
|
||||
}
|
||||
|
||||
function prepareStyleNode(node, baseHref) {
|
||||
if (!node || !node.tagName || node.tagName.toLowerCase() !== 'style')
|
||||
return node;
|
||||
|
||||
var css = rebaseCssUrls(node.textContent || '', baseHref || window.location.href);
|
||||
node.textContent = shouldScopeCSS() ? scopeCss(css) : css;
|
||||
return node;
|
||||
}
|
||||
|
||||
function loadScopedStylesheet(linkNode, host) {
|
||||
var href = linkNode.href || linkNode.getAttribute('href');
|
||||
if (!href || typeof fetch != 'function')
|
||||
return;
|
||||
|
||||
fetch(href, { credentials: 'include' })
|
||||
.then(function(response) {
|
||||
if (!response.ok)
|
||||
throw new Error('stylesheet load failed');
|
||||
return response.text();
|
||||
})
|
||||
.then(function(css) {
|
||||
var style = document.createElement('style');
|
||||
style.setAttribute('data-linkease-luci-from', href);
|
||||
css = rebaseCssUrls(css, href);
|
||||
style.textContent = shouldScopeCSS() ? scopeCss(css) : css;
|
||||
host.appendChild(style);
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
function installDynamicNodeScope() {
|
||||
if (document.__linkeaseLuciDynamicNodeScopeInstalled)
|
||||
return;
|
||||
|
||||
var originalHeadAppendChild = document.head.appendChild.bind(document.head);
|
||||
var originalHeadInsertBefore = document.head.insertBefore.bind(document.head);
|
||||
var originalBodyAppendChild = document.body.appendChild.bind(document.body);
|
||||
var originalBodyInsertBefore = document.body.insertBefore.bind(document.body);
|
||||
|
||||
function redirectHeadNode(node, fallback) {
|
||||
if (window.__LINKEASE_LUCI_EMBEDDED__ && shouldScopeDynamicNodes() && isStylesheetNode(node)) {
|
||||
var host = ensureHost('style');
|
||||
if (host) {
|
||||
if (node.tagName && node.tagName.toLowerCase() === 'link') {
|
||||
loadScopedStylesheet(node, host);
|
||||
return node;
|
||||
}
|
||||
|
||||
return host.appendChild(prepareStyleNode(node));
|
||||
}
|
||||
}
|
||||
|
||||
return fallback();
|
||||
}
|
||||
|
||||
function redirectBodyNode(node, fallback) {
|
||||
if (window.__LINKEASE_LUCI_EMBEDDED__ && shouldScopeDynamicNodes() && isPortalNode(node)) {
|
||||
var host = ensureHost('portal');
|
||||
if (host)
|
||||
return host.appendChild(node);
|
||||
}
|
||||
|
||||
return fallback();
|
||||
}
|
||||
|
||||
document.head.appendChild = function(node) {
|
||||
return redirectHeadNode(node, function() {
|
||||
return originalHeadAppendChild(node);
|
||||
});
|
||||
};
|
||||
|
||||
document.head.insertBefore = function(node, child) {
|
||||
return redirectHeadNode(node, function() {
|
||||
return originalHeadInsertBefore(node, child);
|
||||
});
|
||||
};
|
||||
|
||||
document.body.appendChild = function(node) {
|
||||
return redirectBodyNode(node, function() {
|
||||
return originalBodyAppendChild(node);
|
||||
});
|
||||
};
|
||||
|
||||
document.body.insertBefore = function(node, child) {
|
||||
return redirectBodyNode(node, function() {
|
||||
return originalBodyInsertBefore(node, child);
|
||||
});
|
||||
};
|
||||
|
||||
document.__linkeaseLuciDynamicNodeScopeInstalled = true;
|
||||
}
|
||||
|
||||
function installStrictGlobalGuard() {
|
||||
if (!shouldGuardGlobals()) {
|
||||
if (document.__linkeaseLuciStrictGlobalGuardObserver) {
|
||||
document.__linkeaseLuciStrictGlobalGuardObserver.disconnect();
|
||||
document.__linkeaseLuciStrictGlobalGuardObserver = null;
|
||||
}
|
||||
document.__linkeaseLuciStrictGlobalGuardInstalled = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (document.__linkeaseLuciStrictGlobalGuardInstalled || typeof MutationObserver === 'undefined')
|
||||
return;
|
||||
|
||||
var html = document.documentElement;
|
||||
var body = document.body;
|
||||
var original = [
|
||||
{ node: html, className: html.getAttribute('class'), style: html.getAttribute('style') },
|
||||
{ node: body, className: body.getAttribute('class'), style: body.getAttribute('style') }
|
||||
];
|
||||
var restoring = false;
|
||||
|
||||
function restoreAttribute(node, name, value) {
|
||||
if (value == null)
|
||||
node.removeAttribute(name);
|
||||
else
|
||||
node.setAttribute(name, value);
|
||||
}
|
||||
|
||||
function restore() {
|
||||
if (restoring)
|
||||
return;
|
||||
|
||||
restoring = true;
|
||||
original.forEach(function(item) {
|
||||
restoreAttribute(item.node, 'class', item.className);
|
||||
restoreAttribute(item.node, 'style', item.style);
|
||||
});
|
||||
restoring = false;
|
||||
}
|
||||
|
||||
var observer = new MutationObserver(function(mutations) {
|
||||
if (restoring)
|
||||
return;
|
||||
|
||||
if (mutations.some(function(mutation) {
|
||||
return mutation.type === 'attributes' &&
|
||||
(mutation.attributeName === 'class' || mutation.attributeName === 'style');
|
||||
})) {
|
||||
queueMicrotask(restore);
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(html, { attributes: true, attributeFilter: ['class', 'style'] });
|
||||
observer.observe(body, { attributes: true, attributeFilter: ['class', 'style'] });
|
||||
document.__linkeaseLuciStrictGlobalGuardInstalled = true;
|
||||
document.__linkeaseLuciStrictGlobalGuardObserver = observer;
|
||||
}
|
||||
|
||||
function keepSingleMainMenu() {
|
||||
var mainMenu = query('#mainmenu');
|
||||
if (!mainMenu)
|
||||
return;
|
||||
|
||||
var navs = Array.prototype.slice.call(mainMenu.querySelectorAll(':scope > ul.nav'));
|
||||
navs.slice(0, -1).forEach(function(node) {
|
||||
node.parentNode.removeChild(node);
|
||||
});
|
||||
|
||||
if (navs.length)
|
||||
mainMenu.style.display = '';
|
||||
}
|
||||
|
||||
window.__LINKEASE_LUCI_EMBED = {
|
||||
root: root,
|
||||
query: query,
|
||||
queryAll: queryAll,
|
||||
keepSingleMainMenu: keepSingleMainMenu,
|
||||
isolationMode: isolationMode
|
||||
};
|
||||
|
||||
installDocumentIdScope();
|
||||
installDynamicNodeScope();
|
||||
installStrictGlobalGuard();
|
||||
document.addEventListener('luci-loaded', keepSingleMainMenu, true);
|
||||
})();
|
||||
@@ -0,0 +1,52 @@
|
||||
.le-luci-document {
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.le-luci-document,
|
||||
.le-luci-document > * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.le-luci-document #maincontainer,
|
||||
.le-luci-document #maincontent,
|
||||
.le-luci-document .main {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.le-luci-document #mainmenu {
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.le-luci-document #maincontent {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.le-luci-document.le-luci-content-only .main-left,
|
||||
.le-luci-document.le-luci-content-only .main-right > header,
|
||||
.le-luci-document.le-luci-content-only .main-right > footer,
|
||||
.le-luci-document.le-luci-content-only #maincontent > footer,
|
||||
.le-luci-document.le-luci-content-only footer.mobile-hide,
|
||||
.le-luci-document.le-luci-content-only .main-right > .darkMask,
|
||||
.le-luci-document.le-luci-content-only .showSide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.le-luci-document.le-luci-content-only .main,
|
||||
.le-luci-document.le-luci-content-only .main-right,
|
||||
.le-luci-document.le-luci-content-only #maincontent {
|
||||
width: 100% !important;
|
||||
min-height: 100% !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.le-luci-document.le-luci-content-only #maincontent > .container {
|
||||
width: 100% !important;
|
||||
max-width: none !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.le-luci-document .modal-overlay,
|
||||
.le-luci-document #modal_overlay {
|
||||
position: absolute;
|
||||
}
|
||||
@@ -11,7 +11,7 @@ LUCI_DEPENDS:=+linkeasefull +luci-lib-linkeaseauth +luci-lib-linkeasefile
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=5
|
||||
PKG_RELEASE:=6
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -69,7 +69,24 @@ s = m:section(TypedSection, "linkeasefull", translate("Storage"))
|
||||
s.addremove = false
|
||||
s.anonymous = true
|
||||
|
||||
s:option(Flag, "enabled", translate("Enable")).rmempty = false
|
||||
local enabled = s:option(Flag, "enabled", translate("Enable"))
|
||||
enabled.rmempty = false
|
||||
|
||||
function enabled.write(self, section, value)
|
||||
local old_value = self.map.uci:get("linkeasefull", section, "enabled") or "0"
|
||||
self.map.uci:set("linkeasefull", section, "enabled", value)
|
||||
if old_value == value then
|
||||
return
|
||||
end
|
||||
|
||||
local force_gateway = "/var/run/linkease-app-entry/force-gateway"
|
||||
fs.mkdirr("/var/run/linkease-app-entry")
|
||||
if value == "1" then
|
||||
fs.remove(force_gateway)
|
||||
else
|
||||
fs.writefile(force_gateway, "")
|
||||
end
|
||||
end
|
||||
|
||||
local data = s:option(ListValue, "_local_home", translate("Storage path"), translate("Choose a mounted persistent disk. /tmp is not allowed."))
|
||||
data.rmempty = false
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.9.16
|
||||
PKG_RELEASE:=287
|
||||
PKG_RELEASE:=288
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@@ -272,9 +272,8 @@ o = s:option(Value, "user_agent", translate("User-Agent"))
|
||||
o.default = "passwall"
|
||||
o:value("passwall", "PassWall")
|
||||
o:value("v2rayN/9.99", "v2rayN")
|
||||
o:value("clash.meta", "Clash.Meta")
|
||||
o:value("Clash", "Clash")
|
||||
o:value("curl", "Curl")
|
||||
o:value("clash.meta/v9.99.99", "Clash.Meta")
|
||||
o:value("Shadowrocket/9999 CFNetwork/3896.100.1.2.1 Darwin/27.0.0 iPhone19,7", "Shadowrocket")
|
||||
o:value("Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Linux")
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36 Edg/122.0.0.0", "Edge for Windows")
|
||||
|
||||
|
||||
@@ -43,19 +43,25 @@ local current_node = map:get(section)
|
||||
}
|
||||
|
||||
function b64DecodeUnicode(str) {
|
||||
return decodeURIComponent(Array.prototype.map.call(atob(str), function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
try {
|
||||
return decodeURIComponent(Array.prototype.map.call(atob(str), function (c) {
|
||||
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
|
||||
}).join(''));
|
||||
} catch (e) {}
|
||||
return str;
|
||||
}
|
||||
|
||||
function b64decutf8safe(str) {
|
||||
var l;
|
||||
const orig = str;
|
||||
let l;
|
||||
str = str.replace(/-/g, "+").replace(/_/g, "/");
|
||||
l = str.length;
|
||||
l = (4 - l % 4) % 4;
|
||||
if (l)
|
||||
str = padright(str, l, "=");
|
||||
return b64DecodeUnicode(str);
|
||||
const text = b64DecodeUnicode(str);
|
||||
if (text !== str) return text;
|
||||
return orig;
|
||||
}
|
||||
|
||||
function b64encsafe(str) {
|
||||
@@ -81,6 +87,15 @@ local current_node = map:get(section)
|
||||
return b64decsafe(v);
|
||||
}
|
||||
|
||||
function is_json(str) {
|
||||
try {
|
||||
const result = JSON.parse(str);
|
||||
return typeof result === 'object' && result !== null;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function parseNodeUrl(url) {
|
||||
let protocol = url.substring(0, url.indexOf("://")) + ":"
|
||||
let str = "http" + url.substring(url.indexOf("://"))
|
||||
@@ -1312,7 +1327,6 @@ local current_node = map:get(section)
|
||||
}
|
||||
if (link_type === "vmess") {
|
||||
var sstr = b64DecodeUnicode((link_split[1] || "").replace(/#.*/, "").trim());
|
||||
var ploc = sstr.indexOf("/?");
|
||||
if (vmess_type == "sing-box" && has_singbox) {
|
||||
dom_prefix = "singbox_";
|
||||
opt.set('type', "sing-box");
|
||||
@@ -1321,19 +1335,65 @@ local current_node = map:get(section)
|
||||
opt.set('type', "Xray");
|
||||
}
|
||||
opt.set(dom_prefix + 'protocol', "vmess");
|
||||
var url0, param = "";
|
||||
if (ploc > 0) {
|
||||
url0 = sstr.substr(0, ploc);
|
||||
param = sstr.substr(ploc + 2);
|
||||
|
||||
let ssm = {};
|
||||
if (is_json(sstr)) {
|
||||
// vmess://base64(json)
|
||||
ssm = JSON.parse(sstr);
|
||||
} else if (sstr.indexOf("?") > 0) {
|
||||
// vmess://base64(auto:uuid@host:port)?tfo=1&remark=xxx&&alterId=0&obfs=websocket&path=%2F&obfsParam=host (obfs ~= ws obfsParam={})
|
||||
sstr = sstr.replace("/?", "?");
|
||||
const ploc = sstr.indexOf("?");
|
||||
let url0 = sstr.substr(0, ploc);
|
||||
let paramStr = sstr.substr(ploc + 2);
|
||||
|
||||
const m = new URL("http://" + b64decsafe(url0));
|
||||
if (m.username !== "") {
|
||||
ssm.security = m.username;
|
||||
ssm.id = m.password || m.username;
|
||||
} else {
|
||||
ssm.security = "auto";
|
||||
ssm.id = m.password || m.username;
|
||||
}
|
||||
ssm.add = unbracketIP(m.hostname);
|
||||
ssm.port = m.port || "443";
|
||||
|
||||
const params = {};
|
||||
const pairs = paramStr.split("&");
|
||||
for (let i = 0; i < pairs.length; i++) {
|
||||
const v = pairs[i];
|
||||
const s = v.indexOf("=");
|
||||
if (s > 0) {
|
||||
params[v.substring(0, s)] = decodeURIComponent(v.substring(s + 1));
|
||||
}
|
||||
}
|
||||
ssm.ps = params.remark || params.remarks;
|
||||
ssm.net = (params.obfs === "websocket") ? "ws" : (params.obfs || "tcp");
|
||||
ssm.path = params.path;
|
||||
ssm.aid = params.alterId || "0";
|
||||
ssm.tls = params.tls;
|
||||
ssm.sni = params.peer;
|
||||
ssm.tfo = params.tfo;
|
||||
if (is_json(params.obfsParam)) {
|
||||
const op_info = JSON.parse(params.obfsParam);
|
||||
if (op_info.header) ssm.type = op_info.header;
|
||||
if (op_info.Host) ssm.host = op_info.Host;
|
||||
} else {
|
||||
ssm.host = params.obfsParam;
|
||||
}
|
||||
ssm.allowinsecure = params.allowInsecure;
|
||||
} else {
|
||||
s.innerHTML = "<font color='red'><%:Invalid Share URL Format%></font>";
|
||||
return false;
|
||||
}
|
||||
var ssm = JSON.parse(sstr);
|
||||
|
||||
opt.set('remarks', ssm.ps);
|
||||
opt.set(dom_prefix + 'address', unbracketIP(ssm.add));
|
||||
opt.set(dom_prefix + 'port', ssm.port);
|
||||
opt.set(dom_prefix + 'uuid', ssm.id);
|
||||
opt.set(dom_prefix + 'security', ssm.security || ssm.scy || "auto");
|
||||
opt.set(dom_prefix + 'tls', ssm.tls === "tls");
|
||||
if (ssm.tls === "tls") {
|
||||
opt.set(dom_prefix + 'tls', ssm.tls === "tls" || ssm.tls === "1");
|
||||
if (ssm.tls === "tls" || ssm.tls === "1") {
|
||||
opt.set(dom_prefix + 'tls_serverName', ssm.sni || ssm.host);
|
||||
opt.set(dom_prefix + 'alpn', ssm.alpn || "default");
|
||||
if (ssm.fp && ssm.fp !== "") {
|
||||
|
||||
@@ -842,33 +842,37 @@ update_wan_sets() {
|
||||
|
||||
[ -z "$(command -v get_wan_ips)" ] && . "$UTILS_PATH"
|
||||
|
||||
local WAN_IP=$(get_wan_ips ip4)
|
||||
[ -n "$WAN_IP" ] && {
|
||||
ipset -F "$IPSET_WAN"
|
||||
for wan_ip in $WAN_IP; do
|
||||
ipset -! add "$IPSET_WAN" "$wan_ip"
|
||||
done
|
||||
[ "$log" = "log" ] && {
|
||||
local wan_ip
|
||||
for wan_ip in $WAN_IP; do
|
||||
echolog " - [$?]加入WAN IPv4到ipset[$IPSET_WAN]:${wan_ip}"
|
||||
done
|
||||
}
|
||||
}
|
||||
(
|
||||
flock -x 9 || exit 1
|
||||
|
||||
local WAN6_IP=$(get_wan_ips ip6)
|
||||
[ -n "$WAN6_IP" ] && {
|
||||
ipset -F "$IPSET_WAN6"
|
||||
for wan6_ip in $WAN6_IP; do
|
||||
ipset -! add "$IPSET_WAN6" "$wan6_ip"
|
||||
done
|
||||
[ "$log" = "log" ] && {
|
||||
local wan6_ip
|
||||
for wan6_ip in $WAN6_IP; do
|
||||
echolog " - [$?]加入WAN IPv6到ipset[$IPSET_WAN6]:${wan6_ip}"
|
||||
local WAN_IP=$(get_wan_ips ip4)
|
||||
[ -n "$WAN_IP" ] && {
|
||||
# ipset -F "$IPSET_WAN"
|
||||
for wan_ip in $WAN_IP; do
|
||||
ipset -! add "$IPSET_WAN" "$wan_ip"
|
||||
done
|
||||
[ "$log" = "log" ] && {
|
||||
local wan_ip
|
||||
for wan_ip in $WAN_IP; do
|
||||
echolog " - [$?]加入WAN IPv4到ipset[$IPSET_WAN]:${wan_ip}"
|
||||
done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local WAN6_IP=$(get_wan_ips ip6)
|
||||
[ -n "$WAN6_IP" ] && {
|
||||
# ipset -F "$IPSET_WAN6"
|
||||
for wan6_ip in $WAN6_IP; do
|
||||
ipset -! add "$IPSET_WAN6" "$wan6_ip"
|
||||
done
|
||||
[ "$log" = "log" ] && {
|
||||
local wan6_ip
|
||||
for wan6_ip in $WAN6_IP; do
|
||||
echolog " - [$?]加入WAN IPv6到ipset[$IPSET_WAN6]:${wan6_ip}"
|
||||
done
|
||||
}
|
||||
}
|
||||
) 9>"${LOCK_PATH}/${CONFIG}_update_wan_sets.lock"
|
||||
}
|
||||
|
||||
set_tproxy_sysctl() {
|
||||
|
||||
@@ -931,29 +931,33 @@ update_wan_sets() {
|
||||
|
||||
[ -z "$(command -v get_wan_ips)" ] && . "$UTILS_PATH"
|
||||
|
||||
local WAN_IP=$(get_wan_ips ip4)
|
||||
[ -n "$WAN_IP" ] && {
|
||||
nft flush set $NFTABLE_NAME $NFTSET_WAN
|
||||
echo "$WAN_IP" | insert_nftset $NFTSET_WAN
|
||||
[ "$log" = "log" ] && {
|
||||
local wan_ip
|
||||
for wan_ip in $WAN_IP; do
|
||||
echolog " - [$?]加入WAN IPv4到nftset[$NFTSET_WAN]:${wan_ip}"
|
||||
done
|
||||
}
|
||||
}
|
||||
(
|
||||
flock -x 9 || exit 1
|
||||
|
||||
local WAN6_IP=$(get_wan_ips ip6)
|
||||
[ -n "${WAN6_IP}" ] && {
|
||||
nft flush set $NFTABLE_NAME $NFTSET_WAN6
|
||||
echo "$WAN6_IP" | insert_nftset $NFTSET_WAN6
|
||||
[ "$log" = "log" ] && {
|
||||
local wan6_ip
|
||||
for wan6_ip in $WAN6_IP; do
|
||||
echolog " - [$?]加入WAN IPv6到nftset[$NFTSET_WAN6]:${wan6_ip}"
|
||||
done
|
||||
local WAN_IP=$(get_wan_ips ip4)
|
||||
[ -n "$WAN_IP" ] && {
|
||||
# nft flush set $NFTABLE_NAME $NFTSET_WAN
|
||||
echo "$WAN_IP" | insert_nftset $NFTSET_WAN
|
||||
[ "$log" = "log" ] && {
|
||||
local wan_ip
|
||||
for wan_ip in $WAN_IP; do
|
||||
echolog " - [$?]加入WAN IPv4到nftset[$NFTSET_WAN]:${wan_ip}"
|
||||
done
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
local WAN6_IP=$(get_wan_ips ip6)
|
||||
[ -n "${WAN6_IP}" ] && {
|
||||
# nft flush set $NFTABLE_NAME $NFTSET_WAN6
|
||||
echo "$WAN6_IP" | insert_nftset $NFTSET_WAN6
|
||||
[ "$log" = "log" ] && {
|
||||
local wan6_ip
|
||||
for wan6_ip in $WAN6_IP; do
|
||||
echolog " - [$?]加入WAN IPv6到nftset[$NFTSET_WAN6]:${wan6_ip}"
|
||||
done
|
||||
}
|
||||
}
|
||||
) 9>"${LOCK_PATH}/${CONFIG}_update_wan_sets.lock"
|
||||
}
|
||||
|
||||
set_tproxy_sysctl() {
|
||||
|
||||
@@ -596,7 +596,6 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
-- if ssr_group then result.ssr_group = ssr_group end
|
||||
result.remarks = base64Decode(params.remarks)
|
||||
elseif szType == 'vmess' then
|
||||
local info = jsonParse(content)
|
||||
if sub_vmess_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
elseif sub_vmess_type == "xray" and has_xray then
|
||||
@@ -605,6 +604,58 @@ local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
log("跳过 VMess 节点,因未适配到 VMess 核心程序,或未正确设置节点使用类型。")
|
||||
return nil
|
||||
end
|
||||
-- vmess://base64(json)
|
||||
local info = jsonParse(content)
|
||||
if not info then
|
||||
-- vmess://base64(auto:uuid@host:port)?tfo=1&remark=xxx&&alterId=0&obfs=websocket&path=%2F&obfsParam=host (obfs ~= ws obfsParam={})
|
||||
if content:find("?", 1, true) then
|
||||
info = {}
|
||||
local Info = split(content:gsub("/%?", "?"), "%?")
|
||||
local sp = split(base64Decode(Info[1]), "@")
|
||||
local id_info = split(sp[1], ":")
|
||||
info.security = (#id_info > 1 and id_info[1] ~= "") and id_info[1] or "auto"
|
||||
info.id = id_info[#id_info]
|
||||
|
||||
local addr, port = sp[2], "443"
|
||||
if api.is_ipv6addrport(addr) then
|
||||
local a, p = addr:match("^%[(.+)%]:(%d+)$")
|
||||
if a then addr, port = a, p end
|
||||
addr = api.get_ipv6_only(addr)
|
||||
else
|
||||
local host_port = split(addr, ":")
|
||||
addr = host_port[1]
|
||||
if #host_port > 1 then port = host_port[#host_port] end
|
||||
end
|
||||
info.add, info.port = addr, port
|
||||
|
||||
local params = {}
|
||||
for _, v in pairs(split(Info[2], '&')) do
|
||||
local s = v:find("=", 1, true)
|
||||
if s and s > 1 then
|
||||
params[v:sub(1, s - 1)] = UrlDecode(v:sub(s + 1))
|
||||
end
|
||||
end
|
||||
info.ps = params.remark or params.remarks
|
||||
info.net = (params.obfs == "websocket") and "ws" or (params.obfs or "tcp")
|
||||
info.path = params.path
|
||||
info.aid = params.alterId or "0"
|
||||
info.tls = params.tls
|
||||
info.sni = params.peer
|
||||
info.tfo = params.tfo
|
||||
local op_info = jsonParse(params.obfsParam)
|
||||
if op_info then
|
||||
if op_info.header then info.type = op_info.header end
|
||||
if op_info.Host then info.host = op_info.Host end
|
||||
else
|
||||
info.host = params.obfsParam
|
||||
end
|
||||
info.allowinsecure = params.allowInsecure
|
||||
else
|
||||
log("跳过 VMess 节点,该节点 URI 格式无法解析。")
|
||||
return nil
|
||||
end
|
||||
end
|
||||
|
||||
result.alter_id = info.aid
|
||||
result.address = info.add
|
||||
result.port = info.port
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-wwand
|
||||
PKG_VERSION:=1.6.7_p15
|
||||
PKG_RELEASE:=27
|
||||
PKG_VERSION:=1.6.8
|
||||
PKG_RELEASE:=28
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git
|
||||
PKG_SOURCE_VERSION:=33741b35256ff21c4322e9f9351c9749cd369a97
|
||||
PKG_SOURCE_VERSION:=2f0623c14baf7b6af44eaead8238bf9245693586
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-2.0-only
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=LuCI shared OpenWrt auth bridge for LinkEase apps
|
||||
LUCI_TITLE:=LuCI shared integration for LinkEase apps
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
PKG_VERSION:=1.0.3
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=1.2.1
|
||||
PKG_RELEASE:=6
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
module("luci.controller.linkease_apps", package.seeall)
|
||||
|
||||
function index()
|
||||
local open = entry({"admin", "services", "linkease_apps", "open"}, call("open"))
|
||||
open.leaf = true
|
||||
open.dependent = false
|
||||
open.sysauth = false
|
||||
|
||||
local status = entry({"admin", "services", "linkease_apps", "status"}, call("status"))
|
||||
status.leaf = true
|
||||
status.dependent = false
|
||||
status.sysauth = "root"
|
||||
status.sysauth_authenticator = "htmlauth"
|
||||
end
|
||||
|
||||
local function handler()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
return require("luci.model.linkease.apps_http").new({
|
||||
http = require "luci.http",
|
||||
resolver = require("luci.model.linkease.apps_openwrt").new(),
|
||||
auth_url = dispatcher.build_url("admin", "services", "linkease_auth", "auth")
|
||||
})
|
||||
end
|
||||
|
||||
function open()
|
||||
handler():open()
|
||||
end
|
||||
|
||||
function status()
|
||||
handler():status()
|
||||
end
|
||||
@@ -4,8 +4,7 @@ function index()
|
||||
local auth = entry({"admin", "services", "linkease_auth", "auth"}, call("linkease_auth"))
|
||||
auth.leaf = true
|
||||
auth.dependent = false
|
||||
auth.sysauth = "root"
|
||||
auth.sysauth_authenticator = "htmlauth"
|
||||
auth.sysauth = false
|
||||
|
||||
local auth_finish = entry({"admin", "services", "linkease_auth", "auth_finish"}, call("linkease_auth_finish"))
|
||||
auth_finish.leaf = true
|
||||
@@ -14,202 +13,14 @@ function index()
|
||||
auth_finish.sysauth_authenticator = "htmlauth"
|
||||
end
|
||||
|
||||
local function retrieve_luci_session()
|
||||
local http = require "luci.http"
|
||||
local util = require "luci.util"
|
||||
|
||||
for _, key in ipairs({"sysauth_https", "sysauth_http", "sysauth"}) do
|
||||
local sid = http.getcookie(key)
|
||||
if sid and sid ~= "" then
|
||||
local sdat = util.ubus("session", "get", { ubus_rpc_session = sid })
|
||||
if sdat and type(sdat.values) == "table" then
|
||||
return sid
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function authority_host(authority)
|
||||
if not authority or authority == "" then
|
||||
return ""
|
||||
end
|
||||
if authority:sub(1, 1) == "[" then
|
||||
return authority:match("^%[([^%]]+)%]") or ""
|
||||
end
|
||||
return authority:match("^([^:]+)") or authority
|
||||
end
|
||||
|
||||
local function valid_authority(authority)
|
||||
return authority and authority ~= "" and authority:match("^[A-Za-z0-9%._%-%[%]:]+$") ~= nil
|
||||
end
|
||||
|
||||
local function request_authority()
|
||||
local http = require "luci.http"
|
||||
local forwarded_host = http.getenv("HTTP_X_FORWARDED_HOST") or ""
|
||||
local request_host = http.getenv("HTTP_HOST") or ""
|
||||
|
||||
if valid_authority(forwarded_host) then
|
||||
return forwarded_host
|
||||
end
|
||||
if valid_authority(request_host) then
|
||||
return request_host
|
||||
end
|
||||
return ""
|
||||
end
|
||||
|
||||
local function request_scheme()
|
||||
local http = require "luci.http"
|
||||
local forwarded_proto = http.getenv("HTTP_X_FORWARDED_PROTO") or ""
|
||||
|
||||
if forwarded_proto == "https" then
|
||||
return "https"
|
||||
end
|
||||
if http.getenv("HTTPS") == "on" then
|
||||
return "https"
|
||||
end
|
||||
return "http"
|
||||
end
|
||||
|
||||
local function absolute_luci_url(path)
|
||||
if not path or path == "" or path:match("^https?://") then
|
||||
return path
|
||||
end
|
||||
|
||||
local authority = request_authority()
|
||||
if authority == "" then
|
||||
return path
|
||||
end
|
||||
|
||||
return request_scheme() .. "://" .. authority .. path
|
||||
end
|
||||
|
||||
local function valid_apps_return(value)
|
||||
if not value or value == "" then
|
||||
return false
|
||||
end
|
||||
local function valid_path(path)
|
||||
if path == "/apps" then
|
||||
return true
|
||||
end
|
||||
local prefix = path:sub(1, 6)
|
||||
return prefix == "/apps/" or prefix == "/apps?" or prefix == "/apps#"
|
||||
end
|
||||
if value:sub(1, 1) == "/" then
|
||||
return valid_path(value)
|
||||
end
|
||||
|
||||
local scheme, authority, path = value:match("^(https?://)([^/]+)(/.*)$")
|
||||
if not scheme or not authority or not valid_path(path) then
|
||||
return false
|
||||
end
|
||||
|
||||
local http = require "luci.http"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local request_host = request_authority()
|
||||
local lan_host = uci:get("network", "lan", "ipaddr") or ""
|
||||
local authority_host_value = authority_host(authority)
|
||||
|
||||
if authority_host_value ~= "" and authority_host_value == authority_host(request_host) then
|
||||
return true
|
||||
end
|
||||
if authority_host_value ~= "" and authority_host_value == authority_host(lan_host) then
|
||||
return true
|
||||
end
|
||||
|
||||
-- Example: http://192.168.30.93:8192/apps/dockermanager/
|
||||
return false
|
||||
end
|
||||
|
||||
local function valid_cookie_value(value)
|
||||
return value and value:match("^[A-Za-z0-9._%-_]+$") ~= nil
|
||||
end
|
||||
|
||||
local pending_return_cookie = "linkease_openwrt_pending_return"
|
||||
local bridge_return_cookie = "linkease_openwrt_return"
|
||||
local pending_return_cookie_path = "/cgi-bin/luci/admin/services/linkease_auth"
|
||||
local bridge_return_cookie_path = "/cgi-bin/luci/admin/services/linkease_auth/auth"
|
||||
|
||||
local function cookie_encode(value)
|
||||
return tostring(value or ""):gsub("([^A-Za-z0-9._~-])", function(char)
|
||||
return string.format("%%%02X", char:byte())
|
||||
end)
|
||||
end
|
||||
|
||||
local function cookie_decode(value)
|
||||
if not value or value == "" then
|
||||
return nil
|
||||
end
|
||||
return value:gsub("%%(%x%x)", function(hex)
|
||||
return string.char(tonumber(hex, 16))
|
||||
end)
|
||||
end
|
||||
|
||||
local function safe_return_target(value)
|
||||
if valid_apps_return(value) then
|
||||
return value
|
||||
end
|
||||
return "/apps/"
|
||||
end
|
||||
|
||||
local function set_pending_return_cookie(target)
|
||||
local http = require "luci.http"
|
||||
http.header("Set-Cookie", pending_return_cookie .. "=" .. cookie_encode(target) .. "; Path=" .. pending_return_cookie_path .. "; Max-Age=300; HttpOnly; SameSite=Lax")
|
||||
end
|
||||
|
||||
local function clear_pending_return_cookie()
|
||||
local http = require "luci.http"
|
||||
http.header("Set-Cookie", pending_return_cookie .. "=; Path=" .. pending_return_cookie_path .. "; Max-Age=0; HttpOnly; SameSite=Lax")
|
||||
end
|
||||
|
||||
local function clear_bridge_return_cookie()
|
||||
local http = require "luci.http"
|
||||
http.header("Set-Cookie", bridge_return_cookie .. "=; Path=" .. bridge_return_cookie_path .. "; Max-Age=0; HttpOnly; SameSite=Lax")
|
||||
end
|
||||
|
||||
local function requested_return_target()
|
||||
local http = require "luci.http"
|
||||
return safe_return_target(http.formvalue("return") or cookie_decode(http.getcookie(bridge_return_cookie)) or "/apps/")
|
||||
end
|
||||
|
||||
local function pending_return_target()
|
||||
local http = require "luci.http"
|
||||
return safe_return_target(cookie_decode(http.getcookie(pending_return_cookie)))
|
||||
end
|
||||
|
||||
local function auth_finish_url()
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
return absolute_luci_url(dispatcher.build_url("admin", "services", "linkease_auth", "auth_finish"))
|
||||
local function bridge()
|
||||
return require("luci.model.linkease.auth").new()
|
||||
end
|
||||
|
||||
function linkease_auth()
|
||||
local http = require "luci.http"
|
||||
local sid = retrieve_luci_session()
|
||||
local target = requested_return_target()
|
||||
|
||||
if valid_cookie_value(sid) then
|
||||
clear_bridge_return_cookie()
|
||||
http.header("Set-Cookie", "linkease_openwrt_sid=" .. sid .. "; Path=/apps; HttpOnly; SameSite=Lax")
|
||||
http.redirect(target)
|
||||
return
|
||||
end
|
||||
|
||||
set_pending_return_cookie(target)
|
||||
http.redirect(auth_finish_url())
|
||||
bridge():auth()
|
||||
end
|
||||
|
||||
function linkease_auth_finish()
|
||||
local http = require "luci.http"
|
||||
local sid = retrieve_luci_session()
|
||||
|
||||
if not valid_cookie_value(sid) then
|
||||
http.status(403, "Forbidden")
|
||||
return
|
||||
end
|
||||
|
||||
local target = pending_return_target()
|
||||
|
||||
clear_pending_return_cookie()
|
||||
http.header("Set-Cookie", "linkease_openwrt_sid=" .. sid .. "; Path=/apps; HttpOnly; SameSite=Lax")
|
||||
http.redirect(target)
|
||||
function linkease_auth_finish(state)
|
||||
bridge():auth_finish(state)
|
||||
end
|
||||
@@ -0,0 +1,113 @@
|
||||
local M = {}
|
||||
local Engine = {}
|
||||
Engine.__index = Engine
|
||||
|
||||
local function valid_id(id)
|
||||
return type(id) == "string"
|
||||
and id:match("^[a-z0-9][a-z0-9_-]*$") ~= nil
|
||||
and #id <= 64
|
||||
end
|
||||
|
||||
local function safe_apps_url(value, id)
|
||||
if type(value) ~= "string" then
|
||||
return "/apps/" .. id .. "/"
|
||||
end
|
||||
local expected = "/apps/" .. id
|
||||
if value == expected or value:sub(1, #expected + 1) == expected .. "/" then
|
||||
return value
|
||||
end
|
||||
return "/apps/" .. id .. "/"
|
||||
end
|
||||
|
||||
local function safe_external_url(value)
|
||||
return type(value) == "string"
|
||||
and #value <= 2048
|
||||
and value:match("^https?://[^/@]+%f[/]") ~= nil
|
||||
and value:find("[%c]") == nil
|
||||
end
|
||||
|
||||
local function base_result(id, entry)
|
||||
entry = type(entry) == "table" and entry or {}
|
||||
return {
|
||||
schemaVersion = 1,
|
||||
id = id,
|
||||
registered = false,
|
||||
available = false,
|
||||
mode = "unavailable",
|
||||
worker = entry.worker or "unknown",
|
||||
proxy = {
|
||||
supported = entry.proxy_supported == true,
|
||||
enabled = entry.proxy_enabled == true
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
local function adapter_call(adapter, name, ...)
|
||||
local fn = adapter[name]
|
||||
if type(fn) ~= "function" then
|
||||
return nil
|
||||
end
|
||||
local ok, value = pcall(fn, ...)
|
||||
if not ok then
|
||||
return nil
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
function M.new(adapter)
|
||||
assert(type(adapter) == "table", "apps decision adapter is required")
|
||||
return setmetatable({ adapter = adapter }, Engine)
|
||||
end
|
||||
|
||||
function Engine:resolve(id)
|
||||
if not valid_id(id) then
|
||||
local result = base_result(type(id) == "string" and id or "", nil)
|
||||
result.reason = "invalid_id"
|
||||
return result
|
||||
end
|
||||
|
||||
local app = adapter_call(self.adapter, "lookup", id)
|
||||
if type(app) ~= "table" then
|
||||
local result = base_result(id, nil)
|
||||
result.reason = "not_registered"
|
||||
return result
|
||||
end
|
||||
|
||||
local entry = adapter_call(self.adapter, "entry_status")
|
||||
local result = base_result(id, entry)
|
||||
result.registered = true
|
||||
if type(entry) == "table"
|
||||
and app.entry_supported ~= false
|
||||
and entry.available == true
|
||||
and entry.proxy_supported == true
|
||||
and entry.proxy_enabled == true
|
||||
and (entry.worker == "gateway" or entry.worker == "linkeasefull") then
|
||||
result.available = true
|
||||
result.mode = "apps"
|
||||
result.url = safe_apps_url(app.url, id)
|
||||
result.reason = "entry_available"
|
||||
return result
|
||||
end
|
||||
|
||||
local external = adapter_call(self.adapter, "external_status", app)
|
||||
if type(external) == "table" and external.available == true and safe_external_url(external.url) then
|
||||
result.available = true
|
||||
result.mode = "external"
|
||||
result.url = external.url
|
||||
result.reason = "external_available"
|
||||
return result
|
||||
end
|
||||
|
||||
local entry_reason = app.entry_supported == false and "entry_unsupported" or "entry_unavailable"
|
||||
local external_reason = type(external) == "table" and external.reason or "external_unavailable"
|
||||
result.reason = entry_reason .. "_and_" .. tostring(external_reason)
|
||||
return result
|
||||
end
|
||||
|
||||
function Engine:status(id)
|
||||
return self:resolve(id)
|
||||
end
|
||||
|
||||
M.valid_id = valid_id
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,38 @@
|
||||
local M = {}
|
||||
local Compat = {}
|
||||
Compat.__index = Compat
|
||||
|
||||
function M.new(options)
|
||||
assert(type(options) == "table" and options.http and options.resolver, "apps compatibility options are required")
|
||||
return setmetatable({
|
||||
http = options.http,
|
||||
resolver = options.resolver,
|
||||
auth_url = assert(options.auth_url, "auth_url is required")
|
||||
}, Compat)
|
||||
end
|
||||
|
||||
function Compat:open(id)
|
||||
local decision = self.resolver:resolve(id)
|
||||
if decision.available ~= true then
|
||||
self.http.status(decision.registered == true and 503 or 404, "Service Unavailable")
|
||||
self.http.prepare_content("application/json")
|
||||
self.http.write_json(decision)
|
||||
return
|
||||
end
|
||||
self.http.redirect(self.auth_url .. "?return=" .. self.http.urlencode(decision.url))
|
||||
end
|
||||
|
||||
function Compat:legacy_status(id, fields)
|
||||
local decision = self.resolver:status(id)
|
||||
local response = fields or {}
|
||||
response.entry_url = decision.url or ""
|
||||
response.proxy_prefix_supported = decision.proxy and decision.proxy.supported == true or false
|
||||
response.proxy_prefix_enabled = decision.proxy and decision.proxy.enabled == true or false
|
||||
response.app_entry_running = decision.mode == "apps" and decision.available == true
|
||||
response.access = decision
|
||||
self.http.status(200, "OK")
|
||||
self.http.prepare_content("application/json")
|
||||
self.http.write_json(response)
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,49 @@
|
||||
local M = {}
|
||||
local Handler = {}
|
||||
Handler.__index = Handler
|
||||
|
||||
local status_messages = {
|
||||
[200] = "OK",
|
||||
[400] = "Bad Request",
|
||||
[404] = "Not Found",
|
||||
[503] = "Service Unavailable"
|
||||
}
|
||||
|
||||
local function response_status(decision)
|
||||
if decision.reason == "invalid_id" then return 400 end
|
||||
if decision.registered ~= true then return 404 end
|
||||
if decision.available ~= true then return 503 end
|
||||
return 200
|
||||
end
|
||||
|
||||
function M.new(options)
|
||||
assert(type(options) == "table" and options.http and options.resolver, "apps HTTP options are required")
|
||||
return setmetatable({
|
||||
http = options.http,
|
||||
resolver = options.resolver,
|
||||
auth_url = assert(options.auth_url, "auth_url is required")
|
||||
}, Handler)
|
||||
end
|
||||
|
||||
function Handler:write(decision, code)
|
||||
self.http.status(code, status_messages[code])
|
||||
self.http.prepare_content("application/json")
|
||||
self.http.write_json(decision)
|
||||
end
|
||||
|
||||
function Handler:status()
|
||||
local decision = self.resolver:status(self.http.formvalue("id"))
|
||||
self:write(decision, response_status(decision))
|
||||
end
|
||||
|
||||
function Handler:open()
|
||||
local decision = self.resolver:resolve(self.http.formvalue("id"))
|
||||
local code = response_status(decision)
|
||||
if code ~= 200 then
|
||||
self:write(decision, code)
|
||||
return
|
||||
end
|
||||
self.http.redirect(self.auth_url .. "?return=" .. self.http.urlencode(decision.url))
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,189 @@
|
||||
local M = {}
|
||||
|
||||
local proxy_mapping = "/apps=http://127.0.0.1:19290"
|
||||
local registration_dirs = {
|
||||
"/usr/share/linkease/apps.d",
|
||||
"/usr/share/linkeasefull/desktop-apps.d"
|
||||
}
|
||||
|
||||
local function defaults()
|
||||
local sys = require "luci.sys"
|
||||
return {
|
||||
fs = require "nixio.fs",
|
||||
http = require "luci.http",
|
||||
json = require "luci.jsonc",
|
||||
sys = sys,
|
||||
read_uci = function(key)
|
||||
if type(key) ~= "string" or key:match("^[%w_@%[%].-]+$") == nil then return nil end
|
||||
local value = sys.exec("uci -q get " .. string.format("%q", key) .. " 2>/dev/null") or ""
|
||||
value = value:match("^%s*(.-)%s*$")
|
||||
if value == "" then return nil end
|
||||
return value
|
||||
end,
|
||||
uci = require("luci.model.uci").cursor()
|
||||
}
|
||||
end
|
||||
|
||||
local function normalize_path(value, id)
|
||||
local path = type(value) == "string" and value or "/apps/" .. id .. "/"
|
||||
if path:sub(1, 1) ~= "/" then path = "/" .. path end
|
||||
if path:sub(-1) ~= "/" then path = path .. "/" end
|
||||
return path
|
||||
end
|
||||
|
||||
local function authority_host(authority)
|
||||
if not authority or authority == "" then return "" end
|
||||
if authority:sub(1, 1) == "[" then return authority:match("^%[([^%]]+)%]") or "" end
|
||||
return authority:match("^([^:]+)") or authority
|
||||
end
|
||||
|
||||
local function url_host(host)
|
||||
if host:find(":") and host:sub(1, 1) ~= "[" then return "[" .. host .. "]" end
|
||||
return host
|
||||
end
|
||||
|
||||
local function uci_key(cursor, key, read_committed)
|
||||
if type(key) ~= "string" then return nil end
|
||||
local config, section_type, _, option = key:match("^([%w_-]+)%.@([%w_-]+)%[(%d+)%]%.([%w_-]+)$")
|
||||
if config and section_type and option then
|
||||
return cursor:get_first(config, section_type, option) or (read_committed and read_committed(key))
|
||||
end
|
||||
local direct_config, section, direct_option = key:match("^([%w_-]+)%.([%w_-]+)%.([%w_-]+)$")
|
||||
if direct_config then return cursor:get(direct_config, section, direct_option) or (read_committed and read_committed(key)) end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function configured_value(cursor, specification, read_committed)
|
||||
if type(specification) ~= "table" then return nil end
|
||||
local keys = specification.keys
|
||||
local value = type(keys) == "table" and uci_key(cursor, keys.uci, read_committed) or nil
|
||||
if value ~= nil then return value end
|
||||
return specification.default
|
||||
end
|
||||
|
||||
local function bool_value(value)
|
||||
return value == true or value == 1 or value == "1" or value == "true"
|
||||
end
|
||||
|
||||
local function standalone_external_path(app)
|
||||
local standalone = type(app.manifest.standalone) == "table" and app.manifest.standalone or {}
|
||||
local external = type(standalone.externalOpen) == "table" and standalone.externalOpen or {}
|
||||
if external.enabled == true and type(external.path) == "string" then
|
||||
return external.path
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function M.new(dependencies)
|
||||
local deps = dependencies or defaults()
|
||||
local function lookup(id)
|
||||
for _, directory in ipairs(deps.registration_dirs or registration_dirs) do
|
||||
local iterator = deps.fs.dir(directory)
|
||||
if iterator then
|
||||
for filename in iterator do
|
||||
if filename:match("%.json$") then
|
||||
local raw = deps.fs.readfile(directory .. "/" .. filename)
|
||||
local manifest = raw and deps.json.parse(raw) or nil
|
||||
if type(manifest) == "table" and manifest.id == id then
|
||||
local standalone = type(manifest.standalone) == "table" and manifest.standalone or {}
|
||||
local desktop = type(manifest.desktop) == "table" and manifest.desktop or {}
|
||||
return {
|
||||
id = id,
|
||||
url = normalize_path(standalone.url or standalone.basePath, id),
|
||||
entry_supported = desktop.mode ~= "builtin",
|
||||
manifest = manifest
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function entry_status()
|
||||
local mappings = deps.uci:get_list("uhttpd", "main", "proxy_prefix") or {}
|
||||
local enabled = false
|
||||
for _, mapping in ipairs(mappings) do
|
||||
if mapping == proxy_mapping then enabled = true end
|
||||
end
|
||||
-- A synthetic LuCI test session may make the session-aware UCI cursor
|
||||
-- hide list values. The committed config remains the source of truth.
|
||||
if not enabled then
|
||||
local config = deps.fs.readfile("/etc/config/uhttpd") or ""
|
||||
enabled = config:find(proxy_mapping, 1, true) ~= nil
|
||||
end
|
||||
local supported = deps.sys.call("grep -qr 'proxy_prefix' /etc/init.d/uhttpd /lib/functions /usr/share/uhttpd 2>/dev/null") == 0
|
||||
local state_path = deps.uci:get("linkease_app_entry", "main", "state_file") or "/var/run/linkease-app-entry/state.json"
|
||||
local raw = deps.fs.readfile(state_path)
|
||||
local state = raw and deps.json.parse(raw) or {}
|
||||
local worker = type(state) == "table" and state.active or "stopped"
|
||||
local running = worker == "gateway" or worker == "linkeasefull"
|
||||
return {
|
||||
available = running and supported and enabled,
|
||||
worker = worker,
|
||||
proxy_supported = supported,
|
||||
proxy_enabled = enabled
|
||||
}
|
||||
end
|
||||
|
||||
local function external_status(app)
|
||||
local desktop = app.manifest.desktop or {}
|
||||
local target = type(desktop.target) == "table" and desktop.target or nil
|
||||
if desktop.mode == "iframe" and target and target.hostMode == "request-host"
|
||||
and (target.scheme == "http" or target.scheme == "https") then
|
||||
local port = tonumber(configured_value(deps.uci, target.port, deps.read_uci))
|
||||
if not port or port < 1 or port > 65535 or port % 1 ~= 0 then
|
||||
return { available = false, reason = "external_invalid" }
|
||||
end
|
||||
local host = authority_host(deps.http.getenv("HTTP_HOST") or "")
|
||||
if host == "" then host = deps.uci:get("network", "lan", "ipaddr") or "127.0.0.1" end
|
||||
local external_path = standalone_external_path(app)
|
||||
or (type(target.path) == "string" and target.path or "/")
|
||||
if external_path:sub(1, 1) ~= "/" or external_path:sub(1, 2) == "//"
|
||||
or external_path:find("[%c]") then
|
||||
external_path = "/"
|
||||
end
|
||||
return {
|
||||
available = true,
|
||||
url = target.scheme .. "://" .. url_host(host) .. ":" .. tostring(port) .. external_path
|
||||
}
|
||||
end
|
||||
|
||||
local backend = app.manifest.backend or {}
|
||||
local values = type(backend.values) == "table" and backend.values or {}
|
||||
local enabled
|
||||
local port
|
||||
if type(values.externalPortEnabled) == "table" then
|
||||
enabled = bool_value(configured_value(deps.uci, values.externalPortEnabled, deps.read_uci))
|
||||
port = configured_value(deps.uci, values.port, deps.read_uci)
|
||||
else
|
||||
enabled = backend.portFromUci ~= nil or backend.defaultPort ~= nil
|
||||
port = uci_key(deps.uci, backend.portFromUci, deps.read_uci) or backend.defaultPort
|
||||
end
|
||||
if not enabled then return { available = false, reason = "external_disabled" } end
|
||||
port = tonumber(port)
|
||||
if not port or port < 1 or port > 65535 or port % 1 ~= 0 then
|
||||
return { available = false, reason = "external_invalid" }
|
||||
end
|
||||
local host = authority_host(deps.http.getenv("HTTP_HOST") or "")
|
||||
if host == "" then host = deps.uci:get("network", "lan", "ipaddr") or "127.0.0.1" end
|
||||
local external_path = standalone_external_path(app) or backend.externalBasePath or app.url
|
||||
if type(external_path) ~= "string" or external_path:sub(1, 1) ~= "/"
|
||||
or external_path:sub(1, 2) == "//" or external_path:find("[%c]") then
|
||||
external_path = app.url
|
||||
end
|
||||
return {
|
||||
available = true,
|
||||
url = "http://" .. url_host(host) .. ":" .. tostring(port) .. external_path
|
||||
}
|
||||
end
|
||||
|
||||
return require("luci.model.linkease.apps").new({
|
||||
lookup = lookup,
|
||||
entry_status = entry_status,
|
||||
external_status = external_status
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,191 @@
|
||||
local M = {}
|
||||
local Bridge = {}
|
||||
Bridge.__index = Bridge
|
||||
|
||||
local pending_return_cookie = "linkease_openwrt_pending_return"
|
||||
local bridge_return_cookie = "linkease_openwrt_return"
|
||||
local pending_return_cookie_path = "/cgi-bin/luci/admin/services/linkease_auth"
|
||||
local bridge_return_cookie_path = "/cgi-bin/luci/admin/services/linkease_auth/auth"
|
||||
local max_return_length = 2048
|
||||
|
||||
local function encode_state(value)
|
||||
if type(value) ~= "string" or #value > max_return_length then return nil end
|
||||
local encoded = require("nixio").bin.b64encode(value)
|
||||
return (encoded:gsub("%+", "-"):gsub("/", "_"):gsub("=+$", ""))
|
||||
end
|
||||
|
||||
local function decode_state(value)
|
||||
if type(value) ~= "string" or #value == 0 or #value > 4096
|
||||
or value:match("^[A-Za-z0-9_-]+$") == nil then
|
||||
return nil
|
||||
end
|
||||
local encoded = value:gsub("-", "+"):gsub("_", "/")
|
||||
encoded = encoded .. string.rep("=", (4 - #encoded % 4) % 4)
|
||||
local ok, decoded = pcall(require("nixio").bin.b64decode, encoded)
|
||||
if not ok or type(decoded) ~= "string" or #decoded > max_return_length then return nil end
|
||||
return decoded
|
||||
end
|
||||
|
||||
local function default_dependencies()
|
||||
local http = require "luci.http"
|
||||
local util = require "luci.util"
|
||||
local dispatcher = require "luci.dispatcher"
|
||||
return {
|
||||
http = http,
|
||||
ubus = function(object, method, request) return util.ubus(object, method, request) end,
|
||||
lan_ip = function()
|
||||
return require("luci.model.uci").cursor():get("network", "lan", "ipaddr") or ""
|
||||
end,
|
||||
build_url = function(...) return dispatcher.build_url(...) end
|
||||
}
|
||||
end
|
||||
|
||||
local function authority_host(authority)
|
||||
if not authority or authority == "" then return "" end
|
||||
if authority:sub(1, 1) == "[" then return authority:match("^%[([^%]]+)%]") or "" end
|
||||
return authority:match("^([^:]+)") or authority
|
||||
end
|
||||
|
||||
local function authority_port(authority)
|
||||
if not authority then return nil end
|
||||
local value = authority:match("^%[[^%]]+%]:(%d+)$") or authority:match("^[^:]+:(%d+)$")
|
||||
value = tonumber(value)
|
||||
if value and value >= 1 and value <= 65535 then return value end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function valid_authority(authority)
|
||||
return authority and authority ~= "" and authority:match("^[A-Za-z0-9%._%-%[%]:]+$") ~= nil
|
||||
end
|
||||
|
||||
local function cookie_encode(value)
|
||||
return (tostring(value or ""):gsub("([^A-Za-z0-9._~-])", function(char)
|
||||
return string.format("%%%02X", char:byte())
|
||||
end))
|
||||
end
|
||||
|
||||
local function cookie_decode(value)
|
||||
if not value or value == "" then return nil end
|
||||
return (value:gsub("%%(%x%x)", function(hex) return string.char(tonumber(hex, 16)) end))
|
||||
end
|
||||
|
||||
local function valid_cookie_value(value)
|
||||
return value and value:match("^[A-Za-z0-9._%-_]+$") ~= nil
|
||||
end
|
||||
|
||||
function M.new(dependencies)
|
||||
return setmetatable({ dependencies = dependencies or default_dependencies() }, Bridge)
|
||||
end
|
||||
|
||||
function Bridge:request_authority()
|
||||
local http = self.dependencies.http
|
||||
local forwarded_host = http.getenv("HTTP_X_FORWARDED_HOST") or ""
|
||||
local request_host = http.getenv("HTTP_HOST") or ""
|
||||
if valid_authority(forwarded_host) then return forwarded_host end
|
||||
if valid_authority(request_host) then return request_host end
|
||||
return ""
|
||||
end
|
||||
|
||||
function Bridge:request_scheme()
|
||||
local http = self.dependencies.http
|
||||
if http.getenv("HTTP_X_FORWARDED_PROTO") == "https" or http.getenv("HTTPS") == "on" then return "https" end
|
||||
return "http"
|
||||
end
|
||||
|
||||
function Bridge:absolute_luci_url(path)
|
||||
if not path or path == "" or path:match("^https?://") then return path end
|
||||
local authority = self:request_authority()
|
||||
if authority == "" then return path end
|
||||
return self:request_scheme() .. "://" .. authority .. path
|
||||
end
|
||||
|
||||
function Bridge:valid_apps_return(value)
|
||||
if not value or value == "" then return false end
|
||||
if #value > max_return_length or value:find("[%z\1-\31\127]") then return false end
|
||||
local function valid_path(path)
|
||||
if path == "/apps" then return true end
|
||||
local prefix = path:sub(1, 6)
|
||||
return prefix == "/apps/" or prefix == "/apps?" or prefix == "/apps#"
|
||||
end
|
||||
if value:sub(1, 1) == "/" then return valid_path(value) end
|
||||
|
||||
local scheme, authority, path = value:match("^(https?://)([^/]+)(/.*)$")
|
||||
if not authority or not valid_authority(authority) then return false end
|
||||
local candidate = authority_host(authority)
|
||||
local same_device = candidate ~= "" and (
|
||||
candidate == authority_host(self:request_authority())
|
||||
or candidate == authority_host(self.dependencies.lan_ip())
|
||||
)
|
||||
if not same_device then return false end
|
||||
if valid_path(path) then return true end
|
||||
if path:sub(1, 1) ~= "/" or path:sub(1, 2) == "//" then return false end
|
||||
local target_port = authority_port(authority)
|
||||
local request_port = authority_port(self:request_authority())
|
||||
or (self:request_scheme() == "https" and 443 or 80)
|
||||
local target_default = scheme == "https://" and 443 or 80
|
||||
return target_port ~= nil and target_port ~= request_port and target_port ~= target_default
|
||||
end
|
||||
|
||||
function Bridge:safe_return_target(value)
|
||||
if self:valid_apps_return(value) then return value end
|
||||
return "/apps/"
|
||||
end
|
||||
|
||||
function Bridge:retrieve_luci_session()
|
||||
local http = self.dependencies.http
|
||||
for _, key in ipairs({"sysauth_https", "sysauth_http", "sysauth"}) do
|
||||
local sid = http.getcookie(key)
|
||||
if sid and sid ~= "" then
|
||||
local data = self.dependencies.ubus("session", "get", { ubus_rpc_session = sid })
|
||||
if data and type(data.values) == "table" then return sid end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Bridge:requested_return_target()
|
||||
local http = self.dependencies.http
|
||||
return self:safe_return_target(http.formvalue("return") or cookie_decode(http.getcookie(bridge_return_cookie)) or "/apps/")
|
||||
end
|
||||
|
||||
function Bridge:pending_return_target()
|
||||
return self:safe_return_target(cookie_decode(self.dependencies.http.getcookie(pending_return_cookie)))
|
||||
end
|
||||
|
||||
function Bridge:auth()
|
||||
local http = self.dependencies.http
|
||||
local sid = self:retrieve_luci_session()
|
||||
local target = self:requested_return_target()
|
||||
if valid_cookie_value(sid) then
|
||||
http.header("Set-Cookie", bridge_return_cookie .. "=; Path=" .. bridge_return_cookie_path .. "; Max-Age=0; HttpOnly; SameSite=Lax")
|
||||
http.header("Set-Cookie", "linkease_openwrt_sid=" .. sid .. "; Path=/apps; HttpOnly; SameSite=Lax")
|
||||
http.redirect(target)
|
||||
return
|
||||
end
|
||||
|
||||
http.header("Set-Cookie", pending_return_cookie .. "=" .. cookie_encode(target) .. "; Path=" .. pending_return_cookie_path .. "; Max-Age=300; HttpOnly; SameSite=Lax")
|
||||
local state = encode_state(target)
|
||||
local finish = self.dependencies.build_url("admin", "services", "linkease_auth", "auth_finish", state)
|
||||
http.redirect(self:absolute_luci_url(finish))
|
||||
end
|
||||
|
||||
function Bridge:auth_finish(state)
|
||||
local http = self.dependencies.http
|
||||
local sid = self:retrieve_luci_session()
|
||||
if not valid_cookie_value(sid) then
|
||||
http.status(403, "Forbidden")
|
||||
return
|
||||
end
|
||||
local decoded = decode_state(state)
|
||||
local target
|
||||
if state and state ~= "" then
|
||||
target = self:safe_return_target(decoded)
|
||||
else
|
||||
target = self:pending_return_target()
|
||||
end
|
||||
http.header("Set-Cookie", pending_return_cookie .. "=; Path=" .. pending_return_cookie_path .. "; Max-Age=0; HttpOnly; SameSite=Lax")
|
||||
http.header("Set-Cookie", "linkease_openwrt_sid=" .. sid .. "; Path=/apps; HttpOnly; SameSite=Lax")
|
||||
http.redirect(target)
|
||||
end
|
||||
|
||||
return M
|
||||
Loaded 100 of 126 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user