mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-09-15 21:04:21 +08:00
Compare commits
107
Commits
5f9dbf5e2d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b5f70450b | ||
|
|
17d91bad15 | ||
|
|
4f589e6a41 | ||
|
|
d03b99505a | ||
|
|
c224e11cc7 | ||
|
|
578dfde655 | ||
|
|
17c94aacbb | ||
|
|
700d0d7e02 | ||
|
|
fa60b9df94 | ||
|
|
cf471bd833 | ||
|
|
2acd07fa2f | ||
|
|
90bee3aa55 | ||
|
|
d34867614a | ||
|
|
25adce2488 | ||
|
|
aaa3fd83ad | ||
|
|
ac76f4cbc9 | ||
|
|
a9e0557b71 | ||
|
|
1aa5f26196 | ||
|
|
7280abda02 | ||
|
|
7b7c6e31cd | ||
|
|
f6683ec9ac | ||
|
|
921c6ff346 | ||
|
|
7872f8755a | ||
|
|
cfa884d090 | ||
|
|
0bc6317028 | ||
|
|
127e9ab526 | ||
|
|
55f4afaae1 | ||
|
|
40019bd445 | ||
|
|
0dbf9b779b | ||
|
|
5a1af7b30a | ||
|
|
e1c9ca3f42 | ||
|
|
5fc27ca614 | ||
|
|
593e4e0ff8 | ||
|
|
1ffa1631e1 | ||
|
|
ed6695f0b6 | ||
|
|
8a76697528 | ||
|
|
d6d7df32a5 | ||
|
|
26d174538f | ||
|
|
d9f7c5c117 | ||
|
|
7614ee5c63 | ||
|
|
70d35bab31 | ||
|
|
bdd9cea5d7 | ||
|
|
795028f9ea | ||
|
|
cdcf288975 | ||
|
|
83bcd4708f | ||
|
|
07891fa751 | ||
|
|
5f6047bcba | ||
|
|
f56e31d6d8 | ||
|
|
84e13cad49 | ||
|
|
b5ce6b03b2 | ||
|
|
0ee799dbde | ||
|
|
91e0df6c86 | ||
|
|
d9c423b124 | ||
|
|
bea159edee | ||
|
|
9dcc7b440b | ||
|
|
e4a587bfc3 | ||
|
|
705f63c409 | ||
|
|
e895da1d6b | ||
|
|
87517f518f | ||
|
|
ca30fdbbbb | ||
|
|
81d5757031 | ||
|
|
18bc066e07 | ||
|
|
7b0b16dcd4 | ||
|
|
0e9e9c2496 | ||
|
|
3a12ffbcad | ||
|
|
e45467bc23 | ||
|
|
99e36fe9b0 | ||
|
|
f8f5792e57 | ||
|
|
a179224c4d | ||
|
|
302b45db1b | ||
|
|
b6b067e0ad | ||
|
|
b0cf524450 | ||
|
|
f4103e8c2a | ||
|
|
2d6112f2cd | ||
|
|
3d2cd8c73a | ||
|
|
629b00127c | ||
|
|
51ae3e2992 | ||
|
|
8ff254bfa0 | ||
|
|
6e956beb69 | ||
|
|
c05bdbe2bc | ||
|
|
7cef4bfa84 | ||
|
|
6348b68a14 | ||
|
|
95ded25b90 | ||
|
|
4b545cbffa | ||
|
|
65e584daaa | ||
|
|
cb845a1f2d | ||
|
|
3f15c041aa | ||
|
|
b2ff7cbe2f | ||
|
|
c27f496810 | ||
|
|
dee375ce66 | ||
|
|
7c496d5738 | ||
|
|
d3321f4c3c | ||
|
|
f5aa63986c | ||
|
|
a2faaa2a01 | ||
|
|
570b248f8b | ||
|
|
87d47fbcb2 | ||
|
|
380ee71a08 | ||
|
|
8ca86b3a5e | ||
|
|
dbdd70b00d | ||
|
|
e70e8fddee | ||
|
|
02c6c789ba | ||
|
|
f638f12257 | ||
|
|
259b690118 | ||
|
|
fc820ab4f5 | ||
|
|
8eec681054 | ||
|
|
01aa5b5f9e | ||
|
|
28a85181b8 |
@@ -0,0 +1,29 @@
|
||||
--- a/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
|
||||
+++ b/luci-app-homeproxy/root/etc/homeproxy/scripts/generate_client.uc
|
||||
@@ -996,7 +996,7 @@ if (!isEmpty(main_node)) {
|
||||
type: 'remote',
|
||||
tag: 'geoip-cn',
|
||||
format: 'binary',
|
||||
- url: 'https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs',
|
||||
+ url: 'https://fastly.jsdelivr.net/gh/SagerNet/sing-geoip@rule-set/geoip-cn.srs',
|
||||
update_interval: '24h',
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
@@ -1004,7 +1004,7 @@ if (!isEmpty(main_node)) {
|
||||
type: 'remote',
|
||||
tag: 'geosite-cn',
|
||||
format: 'binary',
|
||||
- url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-cn.srs',
|
||||
+ url: 'https://fastly.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-geolocation-cn.srs',
|
||||
update_interval: '24h',
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
@@ -1012,7 +1012,7 @@ if (!isEmpty(main_node)) {
|
||||
type: 'remote',
|
||||
tag: 'geosite-noncn',
|
||||
format: 'binary',
|
||||
- url: 'https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-geolocation-!cn.srs',
|
||||
+ url: 'https://fastly.jsdelivr.net/gh/SagerNet/sing-geosite@rule-set/geosite-geolocation-!cn.srs',
|
||||
update_interval: '24h',
|
||||
download_detour: 'main-out'
|
||||
});
|
||||
@@ -240,7 +240,7 @@
|
||||
@@ -825,7 +825,7 @@ for k, v in pairs(nodes_table) do
|
||||
end
|
||||
|
||||
m:appendTemplate("/global/footer", {shunt_list = api.jsonc.stringify(shunt_list)})
|
||||
m:appendTemplate("/global/footer")
|
||||
-
|
||||
+m:appendTemplate("/global/status_bottom")
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s2.sectiontype})
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
@@ -383,7 +383,7 @@ for k, v in pairs(nodes_table) do
|
||||
end
|
||||
|
||||
m:appendTemplate("/global/footer", {shunt_list = api.jsonc.stringify(shunt_list)})
|
||||
m:appendTemplate("/global/footer")
|
||||
-
|
||||
+m:appendTemplate("/global/status_bottom")
|
||||
m:appendTemplate("/cbi/sortable", {sectiontype = s2.sectiontype})
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
--- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/20_memory.js
|
||||
+++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/20_memory.js
|
||||
@@ -32,8 +32,7 @@ return baseclass.extend({
|
||||
swap = L.isObject(systeminfo.swap) ? systeminfo.swap : {};
|
||||
@@ -36,14 +36,14 @@ return baseclass.extend({
|
||||
available = Math.min(mem.total ?? Infinity, Math.max(0, available + (mem.buffered || 0)));
|
||||
|
||||
var fields = [
|
||||
- _('Total Available'), (mem.available) ? mem.available : (mem.total && mem.free && mem.buffered) ? mem.free + mem.buffered : null, mem.total,
|
||||
- _('Used'), (mem.total && mem.free) ? (mem.total - mem.free) : null, mem.total,
|
||||
+ _('Used'), (mem.total && mem.available) ? (mem.total - mem.free - mem.buffered - mem.cached) : null, mem.total,
|
||||
- _('Total Available'), available, mem.total,
|
||||
+ _('Used'), (mem.total && mem.available) ? (mem.total - available): null, mem.total,
|
||||
];
|
||||
|
||||
if (mem.buffered)
|
||||
@@ -43,7 +42,7 @@ return baseclass.extend({
|
||||
if (mem.cached)
|
||||
fields.push(_('Cached'), mem.cached, mem.total);
|
||||
|
||||
if (swap.total > 0)
|
||||
@@ -33,37 +31,16 @@
|
||||
|
||||
--- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js
|
||||
+++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js
|
||||
@@ -33,9 +33,9 @@ var callCPUInfo = rpc.declare({
|
||||
method: 'getCPUInfo'
|
||||
});
|
||||
@@ -36,7 +36,7 @@ var callCPUInfo = rpc.declare({
|
||||
|
||||
-var callCPUUsage = rpc.declare({
|
||||
+var callTempInfo = rpc.declare({
|
||||
var callCPUUsage = rpc.declare({
|
||||
object: 'luci',
|
||||
- method: 'getCPUUsage'
|
||||
+ method: 'getTempInfo'
|
||||
});
|
||||
|
||||
return baseclass.extend({
|
||||
@@ -48,7 +48,7 @@ return baseclass.extend({
|
||||
L.resolveDefault(callSystemInfo(), {}),
|
||||
L.resolveDefault(callCPUBench(), {}),
|
||||
L.resolveDefault(callCPUInfo(), {}),
|
||||
- L.resolveDefault(callCPUUsage(), {}),
|
||||
+ L.resolveDefault(callTempInfo(), {}),
|
||||
L.resolveDefault(callLuciVersion(), { revision: _('unknown version'), branch: 'LuCI' }),
|
||||
L.resolveDefault(callGetUnixtime(), 0)
|
||||
]);
|
||||
@@ -59,7 +59,7 @@ return baseclass.extend({
|
||||
systeminfo = data[1],
|
||||
cpubench = data[2],
|
||||
cpuinfo = data[3],
|
||||
- cpuusage = data[4],
|
||||
+ tempinfo = data[4],
|
||||
luciversion = data[5],
|
||||
unixtime = data[6];
|
||||
|
||||
@@ -78,15 +78,9 @@ return baseclass.extend({
|
||||
@@ -85,10 +85,9 @@ return baseclass.extend({
|
||||
|
||||
var fields = [
|
||||
_('Hostname'), boardinfo.hostname,
|
||||
@@ -72,16 +49,11 @@
|
||||
+ _('Architecture'), (cpuinfo.cpuinfo || boardinfo.system) + ' ' + cpubench.cpubench,
|
||||
_('Target Platform'), (L.isObject(boardinfo.release) ? boardinfo.release.target : ''),
|
||||
- _('Firmware Version'), (L.isObject(boardinfo.release)
|
||||
- ? '%s%s / '.format(
|
||||
- boardinfo.release.description || '',
|
||||
- boardinfo.release.revision ? boardinfo.release.revision : ''
|
||||
- )
|
||||
- : '') + (luciversion || ''),
|
||||
+ _('Firmware Version'), (L.isObject(boardinfo.release) ? boardinfo.release.description + ' / ' : '') + (luciversion || ''),
|
||||
_('Kernel Version'), boardinfo.kernel,
|
||||
_('Local Time'), datestr,
|
||||
_('Uptime'), systeminfo.uptime ? '%t'.format(systeminfo.uptime) : null,
|
||||
@@ -94,10 +88,18 @@ return baseclass.extend({
|
||||
? '%s%s / '.format(
|
||||
boardinfo.release.description || '',
|
||||
boardinfo.release.revision ? boardinfo.release.revision : ''
|
||||
@@ -101,10 +100,18 @@ return baseclass.extend({
|
||||
systeminfo.load[0] / 65535.0,
|
||||
systeminfo.load[1] / 65535.0,
|
||||
systeminfo.load[2] / 65535.0
|
||||
@@ -90,7 +62,7 @@
|
||||
+ ) : null
|
||||
];
|
||||
|
||||
+ if (tempinfo.tempinfo) {
|
||||
+ if (cpuusage.tempinfo) {
|
||||
+ fields.splice(6, 0, _('Temperature'));
|
||||
+ fields.splice(7, 0, tempinfo.tempinfo);
|
||||
+ }
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
--- a/luci-app-mosdns/root/etc/hotplug.d/iface/99-mosdns
|
||||
+++ b/luci-app-mosdns/root/etc/hotplug.d/iface/99-mosdns
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
-[ "$ACTION" = ifup ] && /etc/init.d/mosdns restart
|
||||
+[[ "$ACTION" = ifup && "$(uci -q get mosdns.mosdns.enabled)" == 1 ]] && /etc/init.d/mosdns restart
|
||||
@@ -3,7 +3,7 @@
|
||||
@@ -33,6 +33,13 @@ LUCI_DEPENDS:= \
|
||||
+libuci-lua +lua +luci-compat +coreutils +coreutils-base64 +dns2tcp +dnsmasq-full \
|
||||
+jq +ip-full +lua-neturl +libuci-lua +microsocks +ipt2socks +lyaml \
|
||||
+resolveip +curl +nping +unzip +xz-utils +xz \
|
||||
+resolveip +bind-dig +curl +nping +unzip +xz-utils +xz \
|
||||
+ +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:xray-core \
|
||||
+ +PACKAGE_$(PKG_NAME)_INCLUDE_Xray:coreutils-timeout \
|
||||
+ +PACKAGE_$(PKG_NAME)_INCLUDE_Http_Proxy:3proxy \
|
||||
|
||||
@@ -123,14 +123,14 @@ jobs:
|
||||
(
|
||||
git_clone https://github.com/sirpdboy/luci-app-advancedplus
|
||||
git_clone https://github.com/sirpdboy/luci-app-taskplan taskplan && mvdir taskplan
|
||||
git_clone https://github.com/sirpdboy/luci-app-lucky oplucky && mvdir oplucky
|
||||
# git_clone https://github.com/sirpdboy/luci-app-lucky oplucky && mvdir oplucky
|
||||
git_clone https://github.com/sirpdboy/luci-app-partexp partexp && mvdir partexp
|
||||
git_clone https://github.com/sirpdboy/netspeedtest speedtest && mvdir speedtest
|
||||
git_clone https://github.com/sirpdboy/luci-app-parentcontrol
|
||||
git_clone https://github.com/sirpdboy/luci-app-poweroffdevice poweroffdevice && mvdir poweroffdevice
|
||||
git_clone https://github.com/sirpdboy/luci-app-watchdog watchdog1 && mvdir watchdog1
|
||||
git_clone https://github.com/sirpdboy/luci-app-cupsd cupsd1 && mv -n cupsd1/{luci-app-cupsd,cups} ./ ; rm -rf cupsd1
|
||||
git_clone https://github.com/sirpdboy/luci-app-timecontrol timecontrol && mvdir timecontrol
|
||||
# git_clone https://github.com/sirpdboy/luci-app-timecontrol timecontrol && mvdir timecontrol
|
||||
git_clone https://github.com/sirpdboy/luci-theme-kucat
|
||||
git_clone https://github.com/sirpdboy/luci-app-kucat-config
|
||||
git_clone https://github.com/sirpdboy/luci-app-chatgpt-web
|
||||
@@ -152,6 +152,7 @@ jobs:
|
||||
git_clone https://github.com/Tencent-Cloud-Plugins/tencentcloud-openwrt-plugin-cos && mv -n tencentcloud-openwrt-plugin-cos/tencentcloud_cos ./luci-app-tencentcloud-cos; rm -rf tencentcloud-openwrt-plugin-cos
|
||||
git_clone https://github.com/doushang/luci-app-shortcutmenu luci-shortcutmenu && mv -n luci-shortcutmenu/luci-app-shortcutmenu ./ ; rm -rf luci-shortcutmenu
|
||||
git_clone https://github.com/aa65535/openwrt-dist-luci
|
||||
git_clone https://github.com/minicom365/openwrt-smart-reboot && mv -f openwrt-smart-reboot/*smart-reboot ./;rm -rf openwrt-smart-reboot
|
||||
# git_clone https://github.com/morytyann/OpenWrt-msd && mvdir OpenWrt-msd
|
||||
# git_clone https://github.com/messense/aliyundrive-webdav aliyundrive && mv -n aliyundrive/openwrt/* ./ ; rm -rf aliyundrive
|
||||
# git_clone https://github.com/sbilly/netmaker-openwrt && mv -n netmaker-openwrt/netmaker ./; rm -rf netmaker-openwrt
|
||||
@@ -165,6 +166,7 @@ jobs:
|
||||
git_clone https://github.com/linkease/nas-packages-luci && mv -n nas-packages-luci/luci/* ./; rm -rf nas-packages-luci
|
||||
git_clone https://github.com/linkease/istore && mv -n istore/luci/* ./; rm -rf istore
|
||||
#git_clone https://github.com/linkease/lcdsimple lcdsimple1 && mvdir lcdsimple1
|
||||
git_clone https://github.com/0x5826/openwrt-packages-extras 0x5826; rm -rf 0x5826/MAINTENANCE.md; mvdir 0x5826
|
||||
) &
|
||||
(
|
||||
# git_clone https://github.com/frainzy1477/luci-app-clash
|
||||
@@ -176,6 +178,7 @@ jobs:
|
||||
git_clone https://github.com/nikkinikki-org/OpenWrt-momo && mv -f OpenWrt-momo/{luci-app-momo,momo} ./;rm -rf OpenWrt-momo
|
||||
git_clone https://github.com/fcshark-org/openwrt-fchomo && mvdir openwrt-fchomo
|
||||
git clone https://github.com/fw876/helloworld -b dev && mvdir helloworld
|
||||
git_clone https://github.com/umk0/openwrt-luci-haproxy-manager && mv -f openwrt-luci-haproxy-manager/luci-app-haproxy-manager ./;rm -rf openwrt-luci-haproxy-manager
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/ZeaKyX/speedtest-web
|
||||
@@ -208,6 +211,7 @@ jobs:
|
||||
git_clone https://github.com/xptsp/luci-app-squid-adv
|
||||
git_clone https://github.com/xptsp/openwrt-e2guardian
|
||||
git_clone https://github.com/10000ge10000/luci-app-openclaw
|
||||
git_clone https://github.com/WROIATE/luci-app-hijpass
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/honwen/luci-app-shadowsocks-rust
|
||||
@@ -217,7 +221,7 @@ jobs:
|
||||
git_clone https://github.com/4IceG/luci-app-easyconfig-transfer easyconfig && mvdir easyconfig
|
||||
git_clone https://github.com/4IceG/luci-app-atcommands atcommands && mvdir atcommands
|
||||
git_clone https://github.com/wukongdaily/luci-app-run run1 && mvdir run1
|
||||
git_clone https://github.com/itv3/homeproxy && mv -f homeproxy luci-app-homeproxy
|
||||
git_clone https://github.com/szwjp/luci-app-homeproxy
|
||||
git_clone https://github.com/1andrevich/homeproxy-hiddify
|
||||
git_clone https://github.com/weicaixian86/luci-app-pingpacket
|
||||
) &
|
||||
@@ -242,6 +246,8 @@ jobs:
|
||||
git_sparse_clone master "https://github.com/coolsnowwolf/lede" package/lean package/network/services/shellsync package/qca/shortcut-fe
|
||||
git_clone https://github.com/linkease/istore-packages
|
||||
git clone https://github.com/laipeng668/luci-app-gecoosac gecoosac1 && mvdir gecoosac1
|
||||
git_clone https://github.com/eamonxg/luci-theme-shadcn
|
||||
git_sparse_clone dashboard-redesign "https://github.com/eamonxg/luci" modules/luci-mod-dashboard
|
||||
) &
|
||||
(
|
||||
git_clone https://github.com/muink/luci-app-dnsproxy
|
||||
@@ -355,6 +361,7 @@ jobs:
|
||||
(
|
||||
git_sparse_clone frp https://github.com/laipeng668/luci applications/luci-app-frpc
|
||||
git_sparse_clone frp https://github.com/laipeng668/luci applications/luci-app-frps
|
||||
git_sparse_clone frp https://github.com/laipeng668/packages net/frp
|
||||
) &
|
||||
(
|
||||
git_sparse_clone master "https://github.com/coolsnowwolf/lede" package/wwan package/lean package/network/services/shellsync package/qca/shortcut-fe && cp -rf wwan/*/* ./ ; rm -Rf wwan
|
||||
@@ -387,13 +394,13 @@ jobs:
|
||||
(
|
||||
git_sparse_clone develop "https://github.com/Ysurac/openmptcprouter-feeds" \
|
||||
dsvpn glorytun-udp glorytun grpcurl ipcalc luci-app-dsvpn luci-app-glorytun-tcp luci-app-glorytun-udp luci-app-mail luci-app-mlvpn luci-app-mptcp luci-app-nginx-ha luci-app-sqm-autorate luci-app-packet-capture luci-app-iperf luci-theme-openmptcprouter sqm-autorate speedtestc mlvpn mptcp tcptraceroute tracebox tsping atinout z8102
|
||||
git_sparse_clone chawrt/24.10 "https://github.com/liudf0716/luci" applications/luci-app-yt-dlp applications/luci-app-apfree-wifidog applications/luci-app-ss-redir
|
||||
git_sparse_clone chawrt/24.10 "https://github.com/liudf0716/packages" net/ss-redir
|
||||
git_sparse_clone chawrt/25.12 "https://github.com/liudf0716/luci" applications/luci-app-xkcptun applications/luci-app-apfree-wifidog
|
||||
git_sparse_clone chawrt/25.12 "https://github.com/liudf0716/packages" net/xkcptun net/apfree-wifidog
|
||||
) &
|
||||
git_sparse_clone master "https://github.com/immortalwrt/packages" net/n2n net/qbittorrent \
|
||||
net/amule net/cdnspeedtest net/minieap net/ddns-go net/sysuh3c net/3proxy net/cloudreve \
|
||||
net/amule net/cdnspeedtest net/minieap net/ddns-go net/sysuh3c net/3proxy utils/filebrowser net/cloudreve \
|
||||
net/go-nats net/go-wol net/bitsrunlogin-go net/transfer net/udp2raw net/msd_lite \
|
||||
net/subconverter net/ngrokc net/scutclient net/gost net/ua2f net/dufs net/frp net/qBittorrent-Enhanced-Edition \
|
||||
net/subconverter net/ngrokc net/scutclient net/gost net/ua2f net/dufs net/qBittorrent-Enhanced-Edition \
|
||||
net/tinyportmapper net/nexttrace net/rustdesk-server net/tuic-server net/transmission-web-control \
|
||||
net/ipset-lists net/ShadowVPN net/nps net/dnsforwarder \
|
||||
net/ps3netsrv net/brook net/q \
|
||||
@@ -404,7 +411,7 @@ jobs:
|
||||
utils/phicomm-k3screenctrl utils/joker utils/7z utils/dhrystone utils/supervisor utils/tinymembench utils/pcat-mgr utils/fan2go \
|
||||
utils/coremark utils/watchcat multimedia/you-get multimedia/lux multimedia/gmediarender multimedia/ykdl multimedia/gallery-dl \
|
||||
sound/spotifyd devel/go-rice devel/rust-bindgen admin/gotop \
|
||||
lang/lua/lua-periphery lang/lua/lua-neturl lang/lua/lua-maxminddb lang/golang devel/gn
|
||||
lang/lua/lua-periphery lang/lua/lua-neturl lang/lua/lua-maxminddb lang/golang devel/gn lang/node/node-pnpm
|
||||
git_clone https://github.com/koshev-msk/modemfeed && mv -n modemfeed/*/!(telephony)/!(ookla-speedtest) ./; rm -rf modemfeed
|
||||
git_sparse_clone openwrt-25.12 "https://github.com/immortalwrt/luci" applications collections/luci-nginx protocols/luci-proto-minieap protocols/luci-proto-quectel
|
||||
mv -f applications luciapp && rm -rf luciapp/luci-app-firewall
|
||||
@@ -424,7 +431,7 @@ jobs:
|
||||
run: |
|
||||
shopt -s extglob
|
||||
set +e
|
||||
mv -n luciapp/!(luci-app-noddos|luci-app-filebrowser-go|luci-app-openvpn-server|luci-app-chrony|luci-app-kodexplorer|luci-app-cshark|luci-app-dnscrypt-proxy|luci-app-https-dns-proxy|luci-app-ssr-mudb-server|luci-app-ledtrig-*|luci-app-antiblock) ./ ; rm -Rf luciapp
|
||||
mv -n luciapp/!(luci-app-noddos|luci-app-openvpn-server|luci-app-chrony|luci-app-kodexplorer|luci-app-cshark|luci-app-dnscrypt-proxy|luci-app-https-dns-proxy|luci-app-ssr-mudb-server|luci-app-ledtrig-*|luci-app-antiblock) ./ ; rm -Rf luciapp
|
||||
mv -n lean/!(r8101|r8125|r8126|r8168) ./ ; rm -Rf lean
|
||||
mv -n liep/!(luci-app-filebrowser) ./ ; rm -Rf liep
|
||||
mv -n istore-packages/!(qBittorrent*|luci-app-qbittorrent-ee|baidupcs-web|luci-app-LingTiGameAcc|inter_i40e) ./;rm -rf istore-packages
|
||||
@@ -464,7 +471,7 @@ jobs:
|
||||
-H "Authorization: Bearer ${{ secrets.TOKEN_KIDDIN9 }}" \
|
||||
-X POST -d '{ "query": "query {repository(owner: \"'"$1"'\", name: \"'"$2"'\"){latestRelease{tagName tagCommit{oid}}refs(refPrefix:\"refs/tags/\",last:1,orderBy:{field:TAG_COMMIT_DATE,direction:ASC}){edges{node{name target{oid}}}}defaultBranchRef{target{...on Commit {oid}}}}}"}' https://api.github.com/graphql)
|
||||
}
|
||||
for pkg in $(ls !(luci-*|nikki|mihomo*|openwrt-einat-ebpf|qBittorrent-Enhanced-Edition|tuic-client|glorytun|dae|daed|shadowsocks-libev|mosdns|spotifyd|miniupnpd|openwrt-Toolkit|libdouble-conversion|wxbase|3proxy|ucode|mergerfs|openwrt-nezhav1|homebox|sub-web|tcptraceroute|frp|openwrt-caddy|mentohust|brlaser|rapidjson|smartdns|hysteria|gecoosac|libcryptopp|naiveproxy|rustdesk-server|shadowsocksr-libev|tuic-server|joker|ps3netsrv|natter|netmaker|openwrt-ympd|subconverter|sms-tool)/Makefile); do
|
||||
for pkg in $(ls !(luci-*|nikki|mihomo*|dockermanager|sing-box|kaiplus|fastnet|torrserver|baidudrive|openwrt-einat-ebpf|qBittorrent-Enhanced-Edition|tuic-client|glorytun|dae|daed|shadowsocks-libev|mosdns|spotifyd|miniupnpd|openwrt-Toolkit|libdouble-conversion|wxbase|3proxy|ucode|mergerfs|openwrt-nezhav1|homebox|sub-web|tcptraceroute|frp|openwrt-caddy|mentohust|brlaser|rapidjson|smartdns|hysteria|gecoosac|libcryptopp|naiveproxy|rustdesk-server|shadowsocksr-libev|tuic-server|joker|ps3netsrv|natter|netmaker|openwrt-ympd|subconverter|sms-tool)/Makefile); do
|
||||
repo="$(grep ^PKG_SOURCE_URL $pkg | grep github | cut -f 4-5 -d '/' | sed -e 's/.git//' | grep -E '[0-9a-zA-Z_-]+$')" || true
|
||||
if [ "$repo" ]; then
|
||||
owner="$(echo $repo | cut -f 1 -d '/')"
|
||||
@@ -532,7 +539,8 @@ jobs:
|
||||
-e "s/body\[theme=light\]/:root[data-darkmode=false]/g" luci-app-quickstart/htdocs/luci-static/quickstart/style.css
|
||||
sed -i 's/body.getAttribute("theme");if(o)switch(o){case"dark":case"light":return o}/documentElement.getAttribute("data-darkmode");if(o)switch(o){case"true":return"dark";case"false":return"light"}/g' luci-app-quickstart/htdocs/luci-static/quickstart/index.js
|
||||
sed -i 's/icon!=="baby"/icon!==""/' luci-app-quickstart/htdocs/luci-static/quickstart/index.js
|
||||
sed -i 's/+php8-cgi /+php8-cgi +php8-fpm /' luci-theme-spectra/Makefile
|
||||
sed -i 's/+php8-cgi /+php8-cgi +php8-fpm /' luci-theme-spectra/Makefile
|
||||
sed -i 's/+luci-base/+luci-base +luci-app-oui/' luci-mod-status/Makefile
|
||||
|
||||
while [[ ! -f /tmp/ok || ! -f /tmp/ok2 || ! -f /tmp/ok3 ]]; do
|
||||
echo "wait"
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ShadowVPN
|
||||
PKG_VERSION:=0.2.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_RELEASE:=2
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/aa65535/openwrt-shadowvpn/releases/download/v$(PKG_VERSION)
|
||||
|
||||
+18
-5
@@ -10,12 +10,12 @@ AGENTFLOW_ARCH_x86_64:=amd64
|
||||
AGENTFLOW_ARCH_aarch64:=arm64
|
||||
AGENTFLOW_ARCH:=$(AGENTFLOW_ARCH_$(ARCH))
|
||||
|
||||
AGENTFLOW_HASH_x86_64:=996ccb6580043a187e83a3558c6bb2f0ff503bafbe46b7c995cff7f77629c088
|
||||
AGENTFLOW_HASH_aarch64:=6d733ebc49b095bf8670a1b655a13bb8ad98c90ced19701b67d5cdd4fe9a3db4
|
||||
AGENTFLOW_HASH_x86_64:=95cb43ba4e2ce5f060e3609d050caf25170dc151ba882a3a626494b7750d5fbc
|
||||
AGENTFLOW_HASH_aarch64:=93e92628b26087b87d8571400e83046197167e4795593e757d4dcf7dac5f8231
|
||||
|
||||
PKG_NAME:=agentflow
|
||||
PKG_VERSION:=0.3.0
|
||||
PKG_RELEASE:=6
|
||||
PKG_VERSION:=0.3.3
|
||||
PKG_RELEASE:=11
|
||||
|
||||
AGENTFLOW_URL_FILE:=agentflow-linux-$(AGENTFLOW_ARCH)
|
||||
AGENTFLOW_DOWNLOAD:=$(AGENTFLOW_URL_FILE)-$(AGENTFLOW_HASH_$(ARCH))
|
||||
@@ -52,6 +52,16 @@ define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/agentflow
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/prerm
|
||||
#!/bin/sh
|
||||
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
|
||||
fi
|
||||
exit 0
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
$(INSTALL_DIR) $(PKG_BUILD_DIR)
|
||||
$(CP) $(DL_DIR)/$(AGENTFLOW_DOWNLOAD) $(PKG_BUILD_DIR)/agentflow
|
||||
@@ -64,11 +74,14 @@ 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
|
||||
$(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_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
|
||||
$(INSTALL_CONF) ./files/agentflow.config $(1)/etc/config/agentflow
|
||||
$(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
|
||||
endef
|
||||
|
||||
$(eval $(call Download,agentflow))
|
||||
|
||||
@@ -1,84 +0,0 @@
|
||||
# AgentFlow 共享 Runtime HOME 实施里程碑
|
||||
|
||||
## 目标架构
|
||||
|
||||
AgentFlow 的应用数据和设备级开发运行时分离:
|
||||
|
||||
- AgentFlow 数据目录:`<quickstart.conf_dir>/AgentFlow`
|
||||
- 共享 Runtime HOME:`<quickstart.conf_dir>/Runtime/home`
|
||||
|
||||
共享 Runtime HOME 由 `mise` 包提供公共 helper 初始化,AgentFlow 和后续运行时感知应用复用同一套 `HOME`、XDG、`MISE_*` 和 `PATH`。
|
||||
|
||||
## Milestone 1:公共 Runtime HOME 合约
|
||||
|
||||
目标:
|
||||
|
||||
- `mise` 包安装 `/etc/config/mise`。
|
||||
- `mise` 包安装 `/lib/functions/istore_runtime.sh`。
|
||||
- helper 自动从 `quickstart.main.conf_dir` 推导 `<conf_dir>/Runtime/home`。
|
||||
- helper 支持 `ISTORE_RUNTIME_CONF_DIR` 兜底,便于应用从自身 Configs 目录派生 Runtime。
|
||||
- helper 统一导出 `HOME`、`XDG_DATA_HOME`、`XDG_CACHE_HOME`、`XDG_CONFIG_HOME`、`XDG_STATE_HOME`、`MISE_DATA_DIR`、`MISE_CACHE_DIR`、`MISE_CONFIG_DIR`、`MISE_STATE_DIR` 和 `PATH`。
|
||||
|
||||
验收:
|
||||
|
||||
- 新安装 `mise` 后,存在 `/etc/config/mise` 和 `/lib/functions/istore_runtime.sh`。
|
||||
- 已配置 quickstart 时,初始化路径为 `<quickstart.conf_dir>/Runtime/home`。
|
||||
- 未配置 quickstart 但应用提供 `ISTORE_RUNTIME_CONF_DIR` 时,初始化路径为 `$ISTORE_RUNTIME_CONF_DIR/Runtime/home`。
|
||||
|
||||
## Milestone 2:AgentFlow 接入共享 Runtime HOME
|
||||
|
||||
目标:
|
||||
|
||||
- AgentFlow 不再使用 `$data_dir/global` 作为 `HOME` 或 mise shim 根。
|
||||
- AgentFlow 启动时调用 `istore_runtime_export_env`。
|
||||
- `AGENT_FLOW_DATA` 继续固定在 AgentFlow 数据目录下的 `data` 子目录。
|
||||
- AgentFlow 服务进程及其子进程继承共享 Runtime HOME。
|
||||
|
||||
验收:
|
||||
|
||||
- `/etc/init.d/agentflow start` 后,AgentFlow 进程环境中 `HOME=<conf_dir>/Runtime/home`。
|
||||
- `MISE_DATA_DIR=$HOME/.local/share/mise`。
|
||||
- `PATH` 以 `$MISE_DATA_DIR/shims:$HOME/.local/bin` 开头。
|
||||
- AgentFlow 数据库仍写入 `<conf_dir>/AgentFlow/data/db/db.sqlite`。
|
||||
|
||||
## Milestone 3:LuCI 可见性
|
||||
|
||||
目标:
|
||||
|
||||
- AgentFlow LuCI 页面继续配置 AgentFlow 数据目录。
|
||||
- 页面展示推导出的共享 Runtime HOME。
|
||||
- 如果 `mise.runtime_dir` 被显式设置,页面展示其对应的 `home`。
|
||||
|
||||
验收:
|
||||
|
||||
- quickstart `conf_dir=/mnt/vio3-1/Configs` 时,页面显示 `/mnt/vio3-1/Configs/Runtime/home`。
|
||||
- AgentFlow 数据目录仍显示 `/mnt/vio3-1/Configs/AgentFlow`。
|
||||
|
||||
## Milestone 4:迁移与兼容
|
||||
|
||||
目标:
|
||||
|
||||
- 新安装默认使用共享 Runtime HOME。
|
||||
- 老安装中已有 `$data_dir/global/.local/share/mise` 时,不自动移动大目录。
|
||||
- 后续提供显式迁移命令或 LuCI 操作。
|
||||
|
||||
验收:
|
||||
|
||||
- 升级不会删除或搬移旧 runtime 数据。
|
||||
- 用户确认迁移后,旧 mise data 可迁移到 `<conf_dir>/Runtime/home/.local/share/mise`。
|
||||
- 迁移失败不会影响 AgentFlow 私有数据。
|
||||
|
||||
## Milestone 5:AgentFlow 后端项目环境集成
|
||||
|
||||
目标:
|
||||
|
||||
- OpenWrt 层面完成共享 HOME 后,再在 AgentFlow 后端引入项目环境 wrapper。
|
||||
- coding agent 进程使用 `mise exec -- <agent> ...` 应用项目 `mise.toml`。
|
||||
- 运行期设置 `MISE_EXEC_AUTO_INSTALL=0`,缺失工具只在准备阶段安装。
|
||||
|
||||
验收:
|
||||
|
||||
- 单 repo 项目中,agent 看到项目声明的 Node/Python/Go 版本。
|
||||
- 没有隐式运行期下载。
|
||||
- Codex/Claude/Kimi 等 agent 凭据继续来自共享 Runtime HOME。
|
||||
- 取消任务时 wrapper 和 agent 子进程都能退出。
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"id": "agentflow",
|
||||
"name": "AgentFlow",
|
||||
"icon": "logo.svg",
|
||||
"staticRoot": "/usr/share/agentflow/www",
|
||||
"desktop": {
|
||||
"mode": "module",
|
||||
"entry": "desktop-entry.js",
|
||||
"isolation": "shadow-dom"
|
||||
},
|
||||
"standalone": {
|
||||
"entry": "index.html",
|
||||
"basePath": "/apps/agentflow/",
|
||||
"url": "/cgi-bin/luci/admin/services/agentflow/open",
|
||||
"externalOpen": {
|
||||
"enabled": true,
|
||||
"label": "Open AgentFlow"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"mode": "passthrough"
|
||||
},
|
||||
"backend": {
|
||||
"type": "http",
|
||||
"scheme": "http",
|
||||
"host": "127.0.0.1",
|
||||
"portFromUci": "agentflow.@agentflow[0].port",
|
||||
"defaultPort": 9000,
|
||||
"upstreamBasePath": "/apps/agentflow/",
|
||||
"pathMode": "preserve",
|
||||
"proxyMode": "app-base"
|
||||
},
|
||||
"window": {
|
||||
"width": 1180,
|
||||
"height": 760,
|
||||
"singleton": true
|
||||
},
|
||||
"capabilities": ["agent", "coding", "workflow"],
|
||||
"categories": ["development", "ai"]
|
||||
}
|
||||
@@ -10,22 +10,6 @@ get_config() {
|
||||
config_get port "$1" port "9000"
|
||||
}
|
||||
|
||||
load_runtime_env() {
|
||||
local conf_dir
|
||||
|
||||
[ -n "$data_dir" ] || return 1
|
||||
conf_dir="$(dirname "$data_dir")"
|
||||
export ISTORE_RUNTIME_CONF_DIR="$conf_dir"
|
||||
|
||||
if [ -r /lib/functions/istore_runtime.sh ]; then
|
||||
. /lib/functions/istore_runtime.sh
|
||||
istore_runtime_export_env
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load agentflow
|
||||
config_foreach get_config agentflow
|
||||
@@ -35,11 +19,16 @@ start_service() {
|
||||
logger -t agentflow "missing executable: /usr/sbin/agentflow"
|
||||
return 1
|
||||
}
|
||||
mkdir -p "$data_dir" "$data_dir/data" || return 1
|
||||
if ! load_runtime_env; then
|
||||
logger -t agentflow "shared runtime home is not configured"
|
||||
[ -r /lib/functions/mise.sh ] || {
|
||||
logger -t agentflow "missing mise environment helper"
|
||||
return 1
|
||||
}
|
||||
. /lib/functions/mise.sh
|
||||
if ! istore_runtime_env; then
|
||||
logger -t agentflow "failed to initialize mise environment"
|
||||
return 1
|
||||
fi
|
||||
mkdir -p "$data_dir" "$data_dir/data" || return 1
|
||||
|
||||
logger -t agentflow "starting AgentFlow on $host:$port"
|
||||
procd_open_instance
|
||||
@@ -49,14 +38,6 @@ start_service() {
|
||||
"AGENT_FLOW_HOST=$host" \
|
||||
"AGENT_FLOW_PORT=$port" \
|
||||
"HOME=$HOME" \
|
||||
"XDG_DATA_HOME=$XDG_DATA_HOME" \
|
||||
"XDG_CACHE_HOME=$XDG_CACHE_HOME" \
|
||||
"XDG_CONFIG_HOME=$XDG_CONFIG_HOME" \
|
||||
"XDG_STATE_HOME=$XDG_STATE_HOME" \
|
||||
"MISE_DATA_DIR=$MISE_DATA_DIR" \
|
||||
"MISE_CACHE_DIR=$MISE_CACHE_DIR" \
|
||||
"MISE_CONFIG_DIR=$MISE_CONFIG_DIR" \
|
||||
"MISE_STATE_DIR=$MISE_STATE_DIR" \
|
||||
"PATH=$PATH"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<svg
|
||||
width="150"
|
||||
height="150"
|
||||
viewBox="0 0 150 150"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="150" height="150" rx="30" fill="#f5f1e8" />
|
||||
<text
|
||||
x="50%"
|
||||
y="54%"
|
||||
text-anchor="middle"
|
||||
dominant-baseline="middle"
|
||||
font-size="68"
|
||||
font-weight="700"
|
||||
font-family="Georgia, 'Times New Roman', serif"
|
||||
letter-spacing="-4"
|
||||
fill="#111111"
|
||||
>
|
||||
AF
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
@@ -0,0 +1,48 @@
|
||||
import json
|
||||
import pathlib
|
||||
import unittest
|
||||
|
||||
|
||||
APP_DIR = pathlib.Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class DesktopManifestContractTest(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.manifest = json.loads(
|
||||
(APP_DIR / "files" / "agentflow-plugin.json").read_text()
|
||||
)
|
||||
|
||||
def test_manifest_uses_agentflow_app_base_proxy(self):
|
||||
self.assertEqual(self.manifest["id"], "agentflow")
|
||||
self.assertEqual(self.manifest["staticRoot"], "/usr/share/agentflow/www")
|
||||
self.assertEqual(self.manifest["standalone"]["entry"], "index.html")
|
||||
self.assertEqual(self.manifest["standalone"]["basePath"], "/apps/agentflow/")
|
||||
self.assertEqual(
|
||||
self.manifest["standalone"]["url"],
|
||||
"/cgi-bin/luci/admin/services/agentflow/open",
|
||||
)
|
||||
self.assertEqual(
|
||||
self.manifest["standalone"]["externalOpen"],
|
||||
{"enabled": True, "label": "Open AgentFlow"},
|
||||
)
|
||||
self.assertEqual(self.manifest["auth"]["mode"], "passthrough")
|
||||
|
||||
backend = self.manifest["backend"]
|
||||
self.assertEqual(backend["upstreamBasePath"], "/apps/agentflow/")
|
||||
self.assertEqual(backend["pathMode"], "preserve")
|
||||
self.assertEqual(backend["proxyMode"], "app-base")
|
||||
|
||||
def test_desktop_entry_matches_manifest_contract(self):
|
||||
desktop = self.manifest["desktop"]
|
||||
self.assertEqual(desktop["mode"], "module")
|
||||
self.assertEqual(desktop["entry"], "desktop-entry.js")
|
||||
self.assertEqual(desktop["isolation"], "shadow-dom")
|
||||
|
||||
self.assertFalse(
|
||||
(APP_DIR / "files" / "www" / "desktop-entry.js").exists(),
|
||||
"desktop-entry.js must be served by AgentFlow backend, not the static iframe wrapper",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,65 +0,0 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[3]
|
||||
APPLICATIONS = ROOT / "applications"
|
||||
|
||||
|
||||
def read(relative):
|
||||
return (APPLICATIONS / relative).read_text()
|
||||
|
||||
|
||||
class MiseRuntimeContractTest(unittest.TestCase):
|
||||
def test_mise_package_installs_shared_runtime_contract(self):
|
||||
makefile = read("mise/Makefile")
|
||||
config = read("mise/files/mise.config")
|
||||
defaults = read("mise/files/mise.uci-default")
|
||||
helper = read("mise/files/istore_runtime.sh")
|
||||
|
||||
self.assertIn("/etc/config/mise", makefile)
|
||||
self.assertIn("/lib/functions/istore_runtime.sh", makefile)
|
||||
self.assertIn("$(INSTALL_BIN) ./files/mise.uci-default", makefile)
|
||||
self.assertIn("[ -f /etc/uci-defaults/mise ]", makefile)
|
||||
|
||||
self.assertIn("config mise 'main'", config)
|
||||
self.assertIn("option runtime_dir ''", config)
|
||||
self.assertIn("option auto_discover '1'", config)
|
||||
|
||||
self.assertIn(". /lib/functions/istore_runtime.sh", defaults)
|
||||
self.assertIn("istore_runtime_init", defaults)
|
||||
|
||||
self.assertIn("config_get conf_dir main conf_dir", helper)
|
||||
self.assertIn('"$conf_dir" "$ISTORE_RUNTIME_DEFAULT_SUBDIR"', helper)
|
||||
self.assertIn("ISTORE_RUNTIME_HOME_SUBDIR=\"home\"", helper)
|
||||
self.assertIn("ISTORE_RUNTIME_CONF_DIR", helper)
|
||||
self.assertIn('export HOME="$runtime_home"', helper)
|
||||
self.assertIn('export XDG_DATA_HOME="$HOME/.local/share"', helper)
|
||||
self.assertIn('export MISE_DATA_DIR="$XDG_DATA_HOME/mise"', helper)
|
||||
self.assertIn('export PATH="$MISE_DATA_DIR/shims:$HOME/.local/bin:$PATH"', helper)
|
||||
|
||||
def test_agentflow_consumes_shared_runtime_home(self):
|
||||
init = read("agentflow/files/agentflow.init")
|
||||
cbi = read("luci-app-agentflow/luasrc/model/cbi/agentflow.lua")
|
||||
model = read("luci-app-agentflow/luasrc/model/agentflow.lua")
|
||||
translations = read("luci-app-agentflow/po/zh-cn/agentflow.po")
|
||||
|
||||
self.assertIn(". /lib/functions/istore_runtime.sh", init)
|
||||
self.assertIn("istore_runtime_export_env", init)
|
||||
self.assertIn('AGENT_FLOW_DATA=$data_dir/data', init)
|
||||
self.assertIn('HOME=$HOME', init)
|
||||
self.assertIn('MISE_DATA_DIR=$MISE_DATA_DIR', init)
|
||||
self.assertNotIn("$data_dir/global", init)
|
||||
self.assertNotIn(".local/share/mise/shims:$PATH", init)
|
||||
|
||||
self.assertIn('translate("Shared runtime home")', cbi)
|
||||
self.assertIn("agentflow_model.runtime_home", cbi)
|
||||
self.assertIn('return runtime_dir .. "/home"', model)
|
||||
self.assertIn('return conf_dir .. "/Runtime"', model)
|
||||
|
||||
self.assertIn('msgid "Shared runtime home"', translations)
|
||||
self.assertIn('msgstr "共享运行时 HOME"', translations)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=airconnect
|
||||
PKG_VERSION:=1.11.2
|
||||
PKG_VERSION:=1.11.3
|
||||
PKG_RELEASE=1
|
||||
|
||||
PKG_SOURCE:=AirConnect-$(PKG_VERSION).zip
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
#
|
||||
# Copyright (C) 2018 Dengfeng Liu
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v3.
|
||||
# See /LICENSE for more information.
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=apfree-wifidog
|
||||
PKG_VERSION:=9.09.2938
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/liudf0716/$(PKG_NAME)/tar.gz/$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
PKG_MAINTAINER:=Dengfeng Liu <liudf0716@gmail.com>
|
||||
PKG_LICENSE:=GPL-3.0-or-later
|
||||
PKG_LICENSE_FILES:=COPYING
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(INCLUDE_DIR)/cmake.mk
|
||||
|
||||
define Package/apfree-wifidog
|
||||
SUBMENU:=Captive Portals
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
DEPENDS:=+zlib +libjson-c +libevent2 +libevent2-openssl +libuci \
|
||||
+openssl-util +libnetfilter-queue +conntrack +libmosquitto \
|
||||
+libnftnl +libmnl +libbpf +aw-bpf
|
||||
TITLE:=Apfree's wireless captive portal solution
|
||||
URL:=https://github.com/liudf0716/apfree_wifidog
|
||||
endef
|
||||
|
||||
define Package/apfree-wifidog/description
|
||||
The ApFree Wifidog project is a complete and embeddable captive portal
|
||||
solution for wireless community groups or individuals who wish to open a free
|
||||
Hotspot while still preventing abuse of their Internet connection.
|
||||
It's enhanced wifidog
|
||||
endef
|
||||
|
||||
define Package/apfree-wifidog/conffiles
|
||||
/etc/config/wifidogx
|
||||
endef
|
||||
|
||||
define Package/apfree-wifidog/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/wifidogx $(1)/usr/bin/wifidogx
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/wdctlx $(1)/usr/bin/wdctlx
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_DIR) $(1)/etc
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/wifidog-msg.html $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/wifidog-redir.html $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/wifidog-redir.html.front $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/wifidog-redir.html.rear $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/authserver-offline.html $(1)/etc/
|
||||
$(INSTALL_DATA) $(PKG_BUILD_DIR)/internet-offline.html $(1)/etc/
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/wifidogx.init $(1)/etc/init.d/wifidogx
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(CP) ./files/wifidogx.conf $(1)/etc/config/wifidogx
|
||||
$(INSTALL_DIR) $(1)/etc/wifidogx
|
||||
$(INSTALL_DIR) $(1)/www/cgi-bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/wifi-config $(1)/www/cgi-bin/
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/wifi-diag $(1)/www/cgi-bin/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,apfree-wifidog))
|
||||
@@ -0,0 +1,125 @@
|
||||

|
||||
|
||||
|
||||
[![license][1]][2]
|
||||
[![PRs Welcome][3]][4]
|
||||
[![Issues Welcome][5]][6]
|
||||
[![Release Version][7]][8]
|
||||
[![OpenWrt][11]][12]
|
||||
[![Join the QQ Group][15]][16]
|
||||
|
||||
|
||||
[1]: https://img.shields.io/badge/license-GPLV3-brightgreen.svg?style=plastic
|
||||
[2]: https://github.com/liudf0716/apfree_wifidog/blob/master/COPYING
|
||||
[3]: https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=plastic
|
||||
[4]: https://github.com/liudf0716/apfree_wifidog/pulls
|
||||
[5]: https://img.shields.io/badge/Issues-welcome-brightgreen.svg?style=plastic
|
||||
[6]: https://github.com/liudf0716/apfree_wifidog/issues/new
|
||||
[7]: https://img.shields.io/badge/release-3.11.1716-red.svg?style=plastic
|
||||
[8]: https://github.com/liudf0716/apfree_wifidog/releases
|
||||
[11]: https://img.shields.io/badge/Platform-%20OpenWrt%7C%20LEDE%20-brightgreen.svg?style=plastic
|
||||
[12]: https://github.com/KunTengRom/kunteng-lede-17.01.4
|
||||
[13]: https://img.shields.io/badge/KunTeng-Inside-blue.svg?style=plastic
|
||||
[14]: https://www.kunteng.org.cn
|
||||
[15]: https://img.shields.io/badge/chat-qq%20group-brightgreen.svg
|
||||
[16]: https://jq.qq.com/?_wv=1027&k=4ADDSev
|
||||
|
||||
## ApFree WiFiDog: A high performance captive portal solution for HTTP(s)
|
||||
|
||||
ApFree WiFiDog is a high performance captive portal solution for HTTP(s), which mainly used in ([LEDE](https://github.com/lede-project/source) & [OpenWrt](https://github.com/openwrt/openwrt)) platform.
|
||||
|
||||
|
||||
**[中文介绍](https://github.com/liudf0716/apfree_wifidog/blob/master/README_ZH.md)**
|
||||
|
||||
## Enhancement of apfree-wifidog
|
||||
|
||||
In fact, the title should be why we choose apfree-wifidog, the reason was the following:
|
||||
|
||||
> Stable
|
||||
|
||||
apfree-wifidog was widely used in tens of thousands device, which were running in business scene. In order to improve its stable, we rewrite all iptables rule by api instead of fork call, which will easily cause deadlock in multithread-fork running environment. we also re-write the code and replace libhttpd (which is unmaintained for years) with libevent
|
||||
|
||||
> Performance
|
||||
|
||||
apfree-wifidog's http request-response is more quick, u can find statistic data in our test document
|
||||
|
||||
> HTTPs redirect
|
||||
|
||||
apfree-wifidog support https redirect, in current internet environment, captive portal solution without supporting https redirect will become unsuitable gradually
|
||||
|
||||
|
||||
> More features
|
||||
|
||||
apfree-wifidog support mac temporary-pass, ip,domain,pan-domain,white-mac,black-mac rule and etc. all these rules can be applied without restarting wifidog
|
||||
|
||||
> MQTT support
|
||||
|
||||
by enable mqtt support, u can remotely deliver such as trusted ip, domain and pan-domain rules to apfree wifidog
|
||||
|
||||
> Compilable with wifidog protocol
|
||||
|
||||
u don't need to modify your wifidog authentication server to adapt apfree-wifidog; if u have pression on server-side, apfree wifidog's improved protocol can greatly relieve it, which disabled by default
|
||||
|
||||
|
||||
## Getting started
|
||||
|
||||
before starting apfree-wifidog, we must know how to configure it. apfree-wifidog use OpenWrt standard uci config system, all your apfree-wifidog configure information stored in `/etc/confg/wifidogx`, which will be parsed by `/etc/init.d/wifidogx` to /tmp/wifidog.conf, apfree-wifidog's real configure file is `/tmp/wifidog.conf`
|
||||
|
||||
The default apfree-wifidog UCI configuration file like this:
|
||||
|
||||
```
|
||||
config wifidog
|
||||
option gateway_interface 'br-lan'
|
||||
option auth_server_hostname 'wifidog.kunteng.org.cn'
|
||||
option auth_server_port 443
|
||||
option auth_server_path '/wifidog/'
|
||||
option check_interval 60
|
||||
option client_timeout 5
|
||||
option apple_cna 1
|
||||
option thread_number 5
|
||||
option wired_passed 0
|
||||
option enable 0
|
||||
```
|
||||
|
||||
> auth_server_hostname was apfree-wifidog auth server, it can be domain or ip; wifidog.kunteng.org.cn is a free auth server we provided, it was also [open source](https://github.com/wificoin-project/wwas)
|
||||
|
||||
> apple_cna 1 apple captive detect deceive; 2 apple captive detect deceive to disallow portal page appear
|
||||
|
||||
> wired_passed means whether LAN access devices need to auth or not, value 1 means no need to auth
|
||||
|
||||
> enable means whether start apfree-wifidog when we executed `/etc/init.d/wifidogx start`, if u wanted to start apfree-wifidog, you must set enable to 1 before executing `/etc/init.d/wifidogx start`
|
||||
|
||||
### How to support https redirect
|
||||
|
||||
In order to support https redirect, apfree-wifidog need x509 pem cert and private key, u can generate yourself like this:
|
||||
|
||||
```
|
||||
PX5G_BIN="/usr/sbin/px5g"
|
||||
OPENSSL_BIN="/usr/bin/openssl"
|
||||
APFREE_CERT="/etc/apfree.crt"
|
||||
APFREE_KEY="/etc/apfree.key"
|
||||
|
||||
generate_keys() {
|
||||
local days bits country state location commonname
|
||||
|
||||
# Prefer px5g for certificate generation (existence evaluated last)
|
||||
local GENKEY_CMD=""
|
||||
local UNIQUEID=$(dd if=/dev/urandom bs=1 count=4 | hexdump -e '1/1 "%02x"')
|
||||
[ -x "$OPENSSL_BIN" ] && GENKEY_CMD="$OPENSSL_BIN req -x509 -sha256 -outform pem -nodes"
|
||||
[ -x "$PX5G_BIN" ] && GENKEY_CMD="$PX5G_BIN selfsigned -pem"
|
||||
[ -n "$GENKEY_CMD" ] && {
|
||||
$GENKEY_CMD \
|
||||
-days ${days:-730} -newkey rsa:${bits:-2048} -keyout "${APFREE_KEY}.new" -out "${APFREE_CERT}.new" \
|
||||
-subj /C="${country:-CN}"/ST="${state:-localhost}"/L="${location:-Unknown}"/O="${commonname:-ApFreeWiFidog}$UNIQUEID"/CN="${commonname:-ApFreeWiFidog}"
|
||||
sync
|
||||
mv "${APFREE_KEY}.new" "${APFREE_KEY}"
|
||||
mv "${APFREE_CERT}.new" "${APFREE_CERT}"
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
or when u start `/etc/init.d/wifidogx start`, it will generate it automatically
|
||||
|
||||
|
||||
For more information, please refer to the upstream [project page](https://github.com/liudf0716/apfree_wifidog)
|
||||
@@ -0,0 +1,39 @@
|
||||
config wifidogx 'common'
|
||||
option external_interface 'wan'
|
||||
option check_interval '60'
|
||||
option client_timeout '60'
|
||||
option wired_passed '1'
|
||||
option enabled '1'
|
||||
option log_level '0'
|
||||
option js_filter '1'
|
||||
option apple_cna '1'
|
||||
option auth_server_mode 'local'
|
||||
option enable_anti_nat '0'
|
||||
option ttl_values '64,128,255'
|
||||
option ap_device_id 'AW85844955CC7D742A339'
|
||||
option ap_mac_address '5C-C7-D7-42-A3-39'
|
||||
option ap_longitude '116.395000'
|
||||
option ap_latitude '039.911000'
|
||||
option disable_portal_auth '0'
|
||||
option enable_privileged_ops '0'
|
||||
option privileged_ops_secret 'chawrt@2026'
|
||||
option device_id 'AW58534605CC7D742A339'
|
||||
option local_portal 'https://qq.com'
|
||||
|
||||
config gateway 'apfree'
|
||||
option gateway_name 'br-lan'
|
||||
option gateway_channel 'apfree'
|
||||
option gateway_auth_enabled '1'
|
||||
option gateway_id '5CC7D742A336'
|
||||
option gateway_subnetv4 '192.168.8.1/24'
|
||||
|
||||
config auth 'local'
|
||||
option auth_server_hostname '192.168.8.109'
|
||||
option auth_server_port '8001'
|
||||
option auth_server_path '/wifidog/'
|
||||
|
||||
config longconn 'openclaw'
|
||||
option long_conn_mode 'ws'
|
||||
option ws_server_hostname '192.168.8.109'
|
||||
option ws_server_port '8001'
|
||||
option ws_server_path '/ws/wifidogx'
|
||||
@@ -0,0 +1,304 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2018 Dengfeng Liu
|
||||
|
||||
# After aw-bpf (START=98) so dns_ringbuf_portal is pinned for wildcard trust.
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
NAME=wifidogx
|
||||
PROG="/usr/bin/${NAME}"
|
||||
CONFIGFILE="/tmp/wifidogx.conf"
|
||||
|
||||
handle_gateway() {
|
||||
local section="$1"
|
||||
local gateway_name gateway_channel gateway_id
|
||||
local gateway_subnetv4
|
||||
local gateway_auth_enabled
|
||||
|
||||
config_get gateway_name "$section" gateway_name
|
||||
config_get gateway_channel "$section" gateway_channel
|
||||
config_get gateway_id "$section" gateway_id
|
||||
config_get gateway_auth_enabled "$section" gateway_auth_enabled 1
|
||||
|
||||
|
||||
if [ -z "$gateway_name" ] || [ -z "$gateway_channel" ] ; then
|
||||
echo "gateway_name and gateway_channel are required for $section" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
# Get gateway_id from gateway_name if not set
|
||||
if [ -z "$gateway_id" ]; then
|
||||
gateway_id=$(ifconfig "$gateway_name" | awk '/HWaddr/ {print toupper($5)}' | tr -d ':')
|
||||
if [ -z "$gateway_id" ]; then
|
||||
echo "Failed to get gateway_id for $gateway_name" >&2
|
||||
return
|
||||
fi
|
||||
uci set wifidogx."$section".gateway_id="$gateway_id"
|
||||
uci commit wifidogx
|
||||
fi
|
||||
# according to the gateway_name to get the subnetv4
|
||||
local gateway_ipv4=$(ifconfig "$gateway_name" | awk '/inet addr:/ {print $2}' | cut -d: -f2)
|
||||
local gateway_maskv4=$(ifconfig "$gateway_name" | awk '/Mask:/ {print $4}' | cut -d: -f2)
|
||||
[ -z "$gateway_ipv4" ] && echo "Failed to get gateway_ipv4 for $gateway_name" >&2 && return
|
||||
[ -z "$gateway_maskv4" ] && echo "Failed to get gateway_maskv4 for $gateway_name" >&2 && return
|
||||
|
||||
# change the gateway_ip4/gateway_maskv4 to CIDR format
|
||||
local mask_bits=0
|
||||
local mask_value=$(printf '%d' "0x$(echo $gateway_maskv4 | tr '.' ' ' | awk '{printf "%02x%02x%02x%02x",$1,$2,$3,$4}')")
|
||||
while [ $mask_value -ne 0 ]; do
|
||||
mask_bits=$((mask_bits + (mask_value & 1)))
|
||||
mask_value=$((mask_value >> 1))
|
||||
done
|
||||
gateway_subnetv4="$gateway_ipv4/$mask_bits"
|
||||
|
||||
uci set wifidogx."$section".gateway_subnetv4="$gateway_subnetv4"
|
||||
uci commit wifidogx
|
||||
|
||||
printf "GatewaySetting {\n\tGatewayAuthEnabled %s\n\tGatewayInterface %s\n\tGatewayChannel %s\n\tGatewayID %s\n\tGatewaySubnetV4 %s\n}\n" \
|
||||
"${gateway_auth_enabled}" "${gateway_name}" "${gateway_channel}" "${gateway_id}" "${gateway_subnetv4}" >> "$CONFIGFILE"
|
||||
}
|
||||
|
||||
add_white_list_entries() {
|
||||
local list_type="$1"
|
||||
local uci_field="$2"
|
||||
local target_variable="$3"
|
||||
|
||||
list_type=$(uci get wifidogx.common."$list_type")
|
||||
for group in $list_type; do
|
||||
group_list=$(uci get wifidogx."$group"."$uci_field")
|
||||
if [ -n "$group_list" ]; then
|
||||
eval "$target_variable=\"\${$target_variable} \$group_list\""
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
prepare_common_settings() {
|
||||
printf "CheckInterval %s\nClientTimeout %s\nJsFilter %s\nWiredPassed %s\nBypassAppleCNA %s\n" \
|
||||
"$check_interval" "$client_timeout" "$js_filter" "$wired_passed" "$apple_cna" >> "$CONFIGFILE"
|
||||
|
||||
printf "EnableAntiNat %s\n" "$enable_anti_nat" >> "$CONFIGFILE"
|
||||
printf "TTLValues %s\n" "$ttl_values" >> "$CONFIGFILE"
|
||||
printf "DisablePortalAuth %s\n" "$disable_portal_auth" >> "$CONFIGFILE"
|
||||
printf "EnablePrivilegedOps %s\n" "$enable_privileged_ops" >> "$CONFIGFILE"
|
||||
[ -n "$privileged_ops_secret" ] && printf "PrivilegedOpsSecret %s\n" "$privileged_ops_secret" >> "$CONFIGFILE"
|
||||
|
||||
[ -n "$anti_nat_permit_macs" ] && printf "AntiNatPermitMACs %s\n" "$anti_nat_permit_macs" >> "$CONFIGFILE"
|
||||
|
||||
process_trusted_list() {
|
||||
local list="$1"
|
||||
local config_name="$2"
|
||||
|
||||
if [ -n "$list" ]; then
|
||||
# Clean up whitespace and remove duplicates
|
||||
list=$(echo "$list" | sed -e 's/^[[:space:]]*//;s/[[:space:]]*$//;s/[[:space:]]\+/ /g' \
|
||||
| tr ' ' '\n' | sort -u | tr '\n' ',' | sed 's/,$//')
|
||||
printf "%s %s\n" "$config_name" "$list" >> "$CONFIGFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
process_trusted_list "$trusted_domains" "TrustedDomains"
|
||||
process_trusted_list "$trusted_macs" "TrustedMACList"
|
||||
process_trusted_list "$trusted_wildcard_domains" "TrustedWildcardDomains"
|
||||
}
|
||||
|
||||
prepare_device_info() {
|
||||
# Check if any device info fields are set
|
||||
if [ -n "$ap_device_id" ] || [ -n "$ap_mac_address" ] || [ -n "$ap_longitude" ] || [ -n "$ap_latitude" ] || [ -n "$location_id" ]; then
|
||||
printf "DeviceInfo {\n" >> "$CONFIGFILE"
|
||||
|
||||
[ -n "$ap_device_id" ] && printf "\tApDeviceId %s\n" "$ap_device_id" >> "$CONFIGFILE"
|
||||
[ -n "$ap_mac_address" ] && printf "\tApMacAddress %s\n" "$ap_mac_address" >> "$CONFIGFILE"
|
||||
[ -n "$ap_longitude" ] && printf "\tApLongitude %s\n" "$ap_longitude" >> "$CONFIGFILE"
|
||||
[ -n "$ap_latitude" ] && printf "\tApLatitude %s\n" "$ap_latitude" >> "$CONFIGFILE"
|
||||
[ -n "$location_id" ] && printf "\tLocationId %s\n" "$location_id" >> "$CONFIGFILE"
|
||||
|
||||
printf "}\n" >> "$CONFIGFILE"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_auth_server_settings() {
|
||||
# 获取选中的认证服务器配置
|
||||
local selected_auth_server
|
||||
selected_auth_server=$(uci get wifidogx.common.selected_auth_server 2>/dev/null)
|
||||
|
||||
# 如果是云认证或旁路模式,需要验证选中的认证服务器
|
||||
if [ "$auth_server_mode" = "cloud" ] || [ "$auth_server_mode" = "bypass" ]; then
|
||||
if [ -z "$selected_auth_server" ]; then
|
||||
echo "Error: selected_auth_server is required for $auth_server_mode mode" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
local auth_server_hostname auth_server_port auth_server_path
|
||||
|
||||
uci_validate_section "$NAME" "auth" "$selected_auth_server" \
|
||||
'auth_server_hostname:string:192.168.1.1' \
|
||||
'auth_server_port:port:80' \
|
||||
'auth_server_path:string:/wifidog/'
|
||||
|
||||
export auth_server_hostname
|
||||
export auth_server_port
|
||||
export auth_server_path
|
||||
fi
|
||||
|
||||
case "$auth_server_mode" in
|
||||
cloud|bypass)
|
||||
printf "AuthServerMode 0\n" >> "$CONFIGFILE"
|
||||
printf "DeviceID %s\nAuthServer {\n\tHostname %s\n\tHTTPPort %s\n\tPath %s\n}\n" \
|
||||
"$device_id" "$auth_server_hostname" "$auth_server_port" "$auth_server_path" >> "$CONFIGFILE"
|
||||
;;
|
||||
local)
|
||||
printf "AuthServerMode 2\n" >> "$CONFIGFILE"
|
||||
[ -n "$device_id" ] && printf "DeviceID %s\n" "$device_id" >> "$CONFIGFILE"
|
||||
[ -n "$auth_server_offline_file" ] && printf "AuthServerOfflineFile %s\n" "$auth_server_offline_file" >> "$CONFIGFILE"
|
||||
[ -n "$local_portal" ] && printf "LocalPortal %s\n" "$local_portal" >> "$CONFIGFILE"
|
||||
;;
|
||||
esac
|
||||
|
||||
[ -n "$internet_offline_file" ] && printf "InternetOfflineFile %s\n" "$internet_offline_file" >> "$CONFIGFILE"
|
||||
}
|
||||
|
||||
prepare_longconn_settings() {
|
||||
local long_conn_mode ws_server_hostname ws_server_port ws_server_path
|
||||
local mqtt_server_hostname mqtt_server_port mqtt_username mqtt_password
|
||||
local selected_long_conn
|
||||
|
||||
selected_long_conn=$(uci get wifidogx.common.selected_long_conn 2>/dev/null)
|
||||
[ -z "$selected_long_conn" ] && return
|
||||
|
||||
if ! uci -q get wifidogx."$selected_long_conn" >/dev/null; then
|
||||
echo "Long connection: selected section $selected_long_conn does not exist" >&2
|
||||
return
|
||||
fi
|
||||
|
||||
uci_validate_section "$NAME" "longconn" "$selected_long_conn" \
|
||||
'long_conn_mode:or("ws","wss","mqtt","mqtts"):ws' \
|
||||
'ws_server_hostname:string' \
|
||||
'ws_server_port:port:443' \
|
||||
'ws_server_path:string:/ws/wifidogx' \
|
||||
'mqtt_server_hostname:string' \
|
||||
'mqtt_server_port:port:1883' \
|
||||
'mqtt_username:string' \
|
||||
'mqtt_password:string'
|
||||
|
||||
case "$long_conn_mode" in
|
||||
ws|wss)
|
||||
if [ -z "$ws_server_hostname" ]; then
|
||||
echo "Long connection: ws_server_hostname is required" >&2
|
||||
return
|
||||
fi
|
||||
local ws_ssl
|
||||
ws_ssl=$([ "$long_conn_mode" = "wss" ] && echo 1 || echo 0)
|
||||
printf "WebSocket {\n\tWSServer %s\n\tWSServerPort %s\n\tWSServerPath %s\n\tWSServerSSL %s\n}\n" \
|
||||
"$ws_server_hostname" "$ws_server_port" "$ws_server_path" "$ws_ssl" >> "$CONFIGFILE"
|
||||
;;
|
||||
mqtt|mqtts)
|
||||
if [ -z "$mqtt_server_hostname" ]; then
|
||||
echo "Long connection: mqtt_server_hostname is required" >&2
|
||||
return
|
||||
fi
|
||||
local mqtt_ssl
|
||||
mqtt_ssl=$([ "$long_conn_mode" = "mqtts" ] && echo 1 || echo 0)
|
||||
printf "mqtt {\n\tserveraddr %s\n\tserverport %s\n\tmqttUseSSL %s\n\tmqttUsername %s\n\tmqttPassword %s\n}\n" \
|
||||
"$mqtt_server_hostname" "$mqtt_server_port" "$mqtt_ssl" "${mqtt_username:-}" "${mqtt_password:-}" >> "$CONFIGFILE"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
prepare_external_interface() {
|
||||
[ -z "$external_interface" ] && echo "No ExternalInterface " >&2 && return
|
||||
local external_interface_name
|
||||
|
||||
if [ "$external_interface" = "wwan" ]; then
|
||||
external_interface_name=$(ubus call network.interface."$external_interface" status | jsonfilter -e '@.device')
|
||||
else
|
||||
external_interface_name=$(uci get network."$external_interface".device)
|
||||
fi
|
||||
|
||||
[ -z "$external_interface_name" ] && echo "Failed to get device name for $external_interface" >&2 && return
|
||||
|
||||
printf "ExternalInterface %s\n" "$external_interface_name" >> "$CONFIGFILE"
|
||||
}
|
||||
|
||||
prepare_wifidog_conf() {
|
||||
[ -f "$CONFIGFILE" ] && rm -f "$CONFIGFILE"
|
||||
local auth_server_mode_value='"cloud", "bypass", "local"'
|
||||
|
||||
# 获取选中的认证服务器
|
||||
local selected_auth_server
|
||||
selected_auth_server=$(uci get wifidogx.common.selected_auth_server 2>/dev/null)
|
||||
|
||||
|
||||
uci_validate_section "$NAME" "$NAME" common \
|
||||
'enabled:bool:0' \
|
||||
"auth_server_mode:or($auth_server_mode_value)" \
|
||||
'selected_auth_server:string' \
|
||||
'selected_long_conn:string' \
|
||||
'log_level:integer:7' \
|
||||
'device_id:string' \
|
||||
'check_interval:integer:60' \
|
||||
'client_timeout:integer:5' \
|
||||
'wired_passed:bool:1' \
|
||||
'apple_cna:bool:0' \
|
||||
'trusted_domains:list(host)' \
|
||||
'trusted_wildcard_domains:list(string)' \
|
||||
'trusted_macs:list(string)' \
|
||||
'app_white_list:list(string)' \
|
||||
'mac_white_list:list(string)' \
|
||||
'wildcard_white_list:list(string)' \
|
||||
'js_filter:bool:1' \
|
||||
'auth_server_offline_file:string' \
|
||||
'internet_offline_file:string' \
|
||||
'local_portal:string' \
|
||||
'external_interface:string' \
|
||||
'enable_anti_nat:bool:0' \
|
||||
'enable_privileged_ops:bool:0' \
|
||||
'privileged_ops_secret:string:chawrt@2026' \
|
||||
'ttl_values:string:64,128' \
|
||||
'anti_nat_permit_macs:string' \
|
||||
'disable_portal_auth:bool:1' \
|
||||
'ap_device_id:string' \
|
||||
'ap_mac_address:string' \
|
||||
'ap_longitude:string' \
|
||||
'ap_latitude:string' \
|
||||
'location_id:string'
|
||||
|
||||
[ -n "$app_white_list" ] && add_white_list_entries "app_white_list" "domain_name" "trusted_domains"
|
||||
[ -n "$mac_white_list" ] && add_white_list_entries "mac_white_list" "mac_address" "trusted_macs"
|
||||
[ -n "$wildcard_white_list" ] && add_white_list_entries "wildcard_white_list" "wildcard_domain" "trusted_wildcard_domains"
|
||||
|
||||
prepare_external_interface
|
||||
prepare_auth_server_settings
|
||||
prepare_longconn_settings
|
||||
prepare_device_info
|
||||
config_foreach handle_gateway gateway
|
||||
prepare_common_settings
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "wifidogx"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$NAME"
|
||||
|
||||
prepare_wifidog_conf
|
||||
|
||||
if [ "$enabled" -eq 0 ]; then
|
||||
echo "wifidogx is disabled, exit..." >&2
|
||||
return
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG" -c "$CONFIGFILE" -s -f -d "$log_level"
|
||||
procd_set_param respawn
|
||||
procd_set_param file /etc/config/wifidogx
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
status_service() {
|
||||
/usr/bin/wdctlx status
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
+2
-2
@@ -10,8 +10,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_BAIDUDRIVE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=baidudrive
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.17
|
||||
PKG_RELEASE:=19
|
||||
PKG_VERSION:=1.0.6
|
||||
PKG_RELEASE:=21
|
||||
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
|
||||
|
||||
+2
-2
@@ -5,8 +5,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=brook
|
||||
PKG_VERSION:=20260101.0
|
||||
PKG_RELEASE:=4
|
||||
PKG_VERSION:=20270101
|
||||
PKG_RELEASE:=5
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/txthinking/brook/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2021 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=cloudflared-next
|
||||
PKG_VERSION:=2026.9.1
|
||||
PKG_RELEASE:=1
|
||||
|
||||
# Architecture mapping for pre-built binaries
|
||||
ifeq ($(ARCH),x86_64)
|
||||
CF_ARCH:=amd64
|
||||
endif
|
||||
ifeq ($(ARCH),aarch64)
|
||||
CF_ARCH:=arm64
|
||||
endif
|
||||
ifeq ($(ARCH),arm64)
|
||||
CF_ARCH:=arm64
|
||||
endif
|
||||
ifeq ($(ARCH),armv8)
|
||||
CF_ARCH:=arm64
|
||||
endif
|
||||
ifeq ($(ARCH),arm)
|
||||
CF_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(ARCH),armv7)
|
||||
CF_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(ARCH),i386)
|
||||
CF_ARCH:=386
|
||||
endif
|
||||
|
||||
PKG_SOURCE:=cloudflared-linux-$(CF_ARCH)-$(PKG_VERSION)
|
||||
PKG_SOURCE_URL:=https://github.com/cloudflare/cloudflared/releases/download/$(PKG_VERSION)/cloudflared-linux-$(CF_ARCH)?
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/cloudflared-next
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=Cloudflare Tunnel client Next - Pre-built Binary
|
||||
URL:=https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/get-started/
|
||||
DEPENDS:=@(x86_64||aarch64||arm||i386) +ca-bundle
|
||||
PROVIDES:=cloudflared
|
||||
CONFLICTS:=cloudflared
|
||||
endef
|
||||
|
||||
define Package/cloudflared-next/description
|
||||
Contains the command-line client for Cloudflare Tunnel, a tunneling
|
||||
daemon that proxies traffic from the Cloudflare network to your origins.
|
||||
This package contains official pre-built static binaries.
|
||||
endef
|
||||
|
||||
define Package/cloudflared-next/conffiles
|
||||
/etc/config/cloudflared
|
||||
/etc/cloudflared/
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) $(DL_DIR)/$(PKG_SOURCE) $(PKG_BUILD_DIR)/cloudflared
|
||||
chmod 0755 $(PKG_BUILD_DIR)/cloudflared
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/cloudflared-next/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/cloudflared $(1)/usr/bin/cloudflared
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/cloudflared/
|
||||
$(INSTALL_CONF) $(CURDIR)/files/sample_config.yml $(1)/etc/cloudflared/config.yml
|
||||
$(INSTALL_DIR) $(1)/etc/config/
|
||||
$(INSTALL_CONF) $(CURDIR)/files/cloudflared.config $(1)/etc/config/cloudflared
|
||||
$(INSTALL_DIR) $(1)/etc/init.d/
|
||||
$(INSTALL_BIN) $(CURDIR)/files/cloudflared.init $(1)/etc/init.d/cloudflared
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,cloudflared-next))
|
||||
@@ -0,0 +1,17 @@
|
||||
|
||||
config cloudflared 'config'
|
||||
option enabled '0'
|
||||
option token ''
|
||||
# option config '/etc/cloudflared/config.yml'
|
||||
# option origincert '/etc/cloudflared/cert.pem'
|
||||
option edge_bind_address ''
|
||||
option edge_ip_version ''
|
||||
option grace_period ''
|
||||
option protocol 'auto'
|
||||
option region ''
|
||||
option retries ''
|
||||
option tag ''
|
||||
option metrics ''
|
||||
option loglevel 'info'
|
||||
option logfile '/var/log/cloudflared.log'
|
||||
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2021 Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
|
||||
CONF="cloudflared"
|
||||
PROG="/usr/bin/cloudflared"
|
||||
|
||||
append_param_arg() {
|
||||
local value
|
||||
|
||||
config_get value "config" "$1" $2
|
||||
[ -n "$value" ] && procd_append_param command "--${1//_/-}" "$value"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
local token
|
||||
|
||||
config_get_bool enabled "config" "enabled"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param command "$PROG" "tunnel"
|
||||
procd_append_param command "--no-autoupdate"
|
||||
|
||||
append_param_arg "edge_bind_address"
|
||||
append_param_arg "edge_ip_version"
|
||||
append_param_arg "grace_period"
|
||||
append_param_arg "protocol"
|
||||
append_param_arg "region"
|
||||
append_param_arg "retries"
|
||||
append_param_arg "tag"
|
||||
append_param_arg "metrics"
|
||||
append_param_arg "loglevel"
|
||||
append_param_arg "logfile"
|
||||
|
||||
config_get token "config" "token"
|
||||
if [ -z "$token" ]; then
|
||||
# Locally-managed tunnels
|
||||
append_param_arg "config" "/etc/cloudflared/config.yml"
|
||||
append_param_arg "origincert" "/etc/cloudflared/cert.pem"
|
||||
fi
|
||||
|
||||
procd_append_param command "run"
|
||||
|
||||
if [ -n "$token" ]; then
|
||||
# Remotely-managed tunnel (recommended by Cloudflare)
|
||||
procd_append_param command "--token" "$token"
|
||||
fi
|
||||
|
||||
procd_set_param respawn
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
procd_add_interface_trigger "interface.*.up" "wan" /etc/init.d/cloudflared restart
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
# https://developers.cloudflare.com/cloudflare-one/networks/connectors/cloudflare-tunnel/do-more-with-tunnels/local-management/configuration-file/
|
||||
|
||||
# NOTICE
|
||||
# Cloudflare recommends setting up a remotely-managed tunnel.
|
||||
# Remotely-managed configurations are stored on Cloudflare,
|
||||
# which allows you to manage the tunnel from any machine
|
||||
# using the dashboard, API, or Terraform.
|
||||
|
||||
#tunnel: <Tunnel-UUID>
|
||||
#credentials-file: /etc/cloudflared/<Tunnel-UUID>.json
|
||||
#
|
||||
#ingress:
|
||||
# - hostname: luci.example.com
|
||||
# service: http://localhost:80
|
||||
# - hostname: ssh.example.com
|
||||
# service: ssh://localhost:22
|
||||
# - service: http_status:404
|
||||
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
cloudflared --version | grep "$PKG_VERSION"
|
||||
Executable
+78
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 自动更新 OpenWrt cloudflared-next 包的版本和 Hash
|
||||
# 依赖: curl, grep, sed, sha256sum (或 shasum)
|
||||
|
||||
set -e
|
||||
|
||||
PKG_DIR=$(cd "$(dirname "$0")"; pwd)
|
||||
MAKEFILE="$PKG_DIR/Makefile"
|
||||
|
||||
if [ ! -f "$MAKEFILE" ]; then
|
||||
echo "错误: 找不到 Makefile 文件 ($MAKEFILE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. 获取当前本地版本
|
||||
CURRENT_VERSION=$(grep 'PKG_VERSION:=' "$MAKEFILE" | cut -d'=' -f2 | tr -d ' ')
|
||||
echo "当前本地版本: $CURRENT_VERSION"
|
||||
|
||||
# 2. 获取 GitHub 上的最新版本号
|
||||
# 注意: cloudflared 的 tag 通常是 2024.x.y 这种格式,无需特殊处理
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/cloudflare/cloudflared/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^v//')
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo "错误: 无法从 GitHub 获取最新版本号。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GitHub 最新版本: $LATEST_VERSION"
|
||||
|
||||
# 3. 比较版本
|
||||
if [ "$CURRENT_VERSION" == "$LATEST_VERSION" ]; then
|
||||
echo "版本已经是最新,无需更新。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "发现新版本! 正在准备更新..."
|
||||
|
||||
# 4. 下载新版本的源码包并计算 Hash
|
||||
SOURCE_URL="https://codeload.github.com/cloudflare/cloudflared/tar.gz/$LATEST_VERSION"
|
||||
echo "正在从 $SOURCE_URL 下载并计算 Hash..."
|
||||
|
||||
# 探测可用的哈希计算工具
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
NEW_HASH=$(curl -sL "$SOURCE_URL" | sha256sum | cut -d' ' -f1)
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
NEW_HASH=$(curl -sL "$SOURCE_URL" | shasum -a 256 | cut -d' ' -f1)
|
||||
else
|
||||
echo "错误: 系统中未找到 sha256sum 或 shasum 命令。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$NEW_HASH" ] || [ ${#NEW_HASH} -ne 64 ]; then
|
||||
echo "错误: 计算得到的 Hash 值无效: $NEW_HASH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "新 Hash 值: $NEW_HASH"
|
||||
|
||||
# 5. 更新 Makefile
|
||||
# 使用特定的正则表达式确保精准替换
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "s/PKG_VERSION:=.*/PKG_VERSION:=$LATEST_VERSION/" "$MAKEFILE"
|
||||
sed -i '' "s/PKG_HASH:=.*/PKG_HASH:=$NEW_HASH/" "$MAKEFILE"
|
||||
else
|
||||
sed -i "s/PKG_VERSION:=.*/PKG_VERSION:=$LATEST_VERSION/" "$MAKEFILE"
|
||||
sed -i "s/PKG_HASH:=.*/PKG_HASH:=$NEW_HASH/" "$MAKEFILE"
|
||||
fi
|
||||
|
||||
echo "Makefile 更新成功: $CURRENT_VERSION -> $LATEST_VERSION"
|
||||
|
||||
# 6. 可选: 提交更改 (如果是在 Git 仓库中且您希望自动提交)
|
||||
# cd "$PKG_DIR/.."
|
||||
# if [ -d ".git" ]; then
|
||||
# git add cloudflared-next/Makefile
|
||||
# git commit -m "cloudflared-next: update to version $LATEST_VERSION"
|
||||
# echo "已在本地 Git 仓库中提交更新。"
|
||||
# fi
|
||||
+2
-2
@@ -6,11 +6,11 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=cloudreve
|
||||
PKG_VERSION:=4.18.0
|
||||
PKG_RELEASE:=16
|
||||
PKG_RELEASE:=17
|
||||
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_URL:=https://github.com/cloudreve/Cloudreve.git
|
||||
PKG_SOURCE_VERSION:=1c3fe4da3c4073e73a11ed93518b545bb8b8d5ce
|
||||
PKG_SOURCE_VERSION:=ade1866be54a3614bc643ab5f513e93a1e0dc82f
|
||||
PKG_MIRROR_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=GPL-3.0-only
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=dae
|
||||
PKG_VERSION:=2026.08.28
|
||||
PKG_RELEASE:=40
|
||||
PKG_VERSION:=2026.09.12
|
||||
PKG_RELEASE:=44
|
||||
|
||||
PKG_SOURCE:=dae-src-2026.08.28-d6aca8a10b35.tar.gz
|
||||
PKG_SOURCE:=dae-src-2026.09.12-187058462a1f.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
|
||||
|
||||
@@ -30,7 +30,6 @@ start_service() {
|
||||
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
|
||||
procd_set_param command "$PROG" run
|
||||
procd_append_param command --config "$config_file"
|
||||
procd_append_param command --disable-timestamp
|
||||
procd_append_param command --logfile "$LOG_DIR/dae.log"
|
||||
procd_append_param command --logfile-maxbackups "$log_maxbackups"
|
||||
procd_append_param command --logfile-maxsize "$log_maxsize"
|
||||
|
||||
+3
-3
@@ -5,10 +5,10 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=daed
|
||||
PKG_VERSION:=2026.08.28
|
||||
PKG_RELEASE:=52
|
||||
PKG_VERSION:=2026.09.12
|
||||
PKG_RELEASE:=55
|
||||
|
||||
PKG_SOURCE:=daed-src-2026.08.28-73f01b995c34.tar.gz
|
||||
PKG_SOURCE:=daed-src-2026.09.12-a0181f729855.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,8 +1,65 @@
|
||||
#!/bin/sh
|
||||
# daed-cleanup.sh — reaper for stale daed kernel state.
|
||||
#
|
||||
# Removes:
|
||||
# 1. Processes inside the `daens` netns (SIGTERM, then SIGKILL after 1s)
|
||||
# 2. The `daens` network namespace itself (ip netns del, with
|
||||
# umount+rm as fallback for zombie nsfs mounts)
|
||||
# 3. The `dae0` veth pair in the host netns
|
||||
#
|
||||
# daed itself owns TC clsact detach. This opkg-side helper only
|
||||
# reaps stale daens/dae0 state and never removes /sys/fs/bpf/daed.
|
||||
# The pin directory is created during normal startup, so it is not
|
||||
# a reliable leak indicator and must not block service lifecycle.
|
||||
#
|
||||
# The function stays sourceable by both init.d/daed and daed-guard.
|
||||
|
||||
daed_process_probe() {
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
pgrep -f '^/usr/bin/daed([[:space:]]|$)' >/dev/null 2>&1
|
||||
case "$?" in
|
||||
0) return 0 ;;
|
||||
1) return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if command -v pidof >/dev/null 2>&1; then
|
||||
pidof daed >/dev/null 2>&1
|
||||
case "$?" in
|
||||
0) return 0 ;;
|
||||
1) return 1 ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
return 2
|
||||
}
|
||||
|
||||
daed_cleanup_runtime() {
|
||||
local pid
|
||||
local pid rc=0 probe_rc
|
||||
|
||||
# If daed userspace is currently running, do not touch the
|
||||
# netns, the veth, or the eBPF dataplane. They are in active
|
||||
# use; removing them would make every connection through dae
|
||||
# hang.
|
||||
daed_process_probe
|
||||
probe_rc=$?
|
||||
case "$probe_rc" in
|
||||
0)
|
||||
if [ "${DAED_GUARD_CLEANUP:-start}" = "start" ]; then
|
||||
logger -t daed-init "cleanup: pre-start skipped because /usr/bin/daed is still running; refusing a second instance"
|
||||
return 1
|
||||
fi
|
||||
logger -t daed-init "cleanup: post-exit skipped because another /usr/bin/daed instance is still running"
|
||||
return 0
|
||||
;;
|
||||
1) ;;
|
||||
*)
|
||||
logger -t daed-init "cleanup: cannot determine whether daed is running; refusing to remove netns/veth"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# 1. Kill processes inside daens.
|
||||
for pid in $(ip netns pids daens 2>/dev/null); do
|
||||
kill "$pid" 2>/dev/null
|
||||
done
|
||||
@@ -14,14 +71,36 @@ daed_cleanup_runtime() {
|
||||
done
|
||||
fi
|
||||
|
||||
# 2. Remove the daens netns. ip netns del can fail if a
|
||||
# process still references it via /proc/<pid>/ns/net or
|
||||
# because the umount has already happened. Try the
|
||||
# umount/rm fallback.
|
||||
if ! ip netns del daens 2>/dev/null; then
|
||||
umount -l /run/netns/daens 2>/dev/null
|
||||
rm -f /run/netns/daens
|
||||
if [ -e /run/netns/daens ] && ! rm -f /run/netns/daens 2>/dev/null; then
|
||||
logger -t daed-init "cleanup: failed to remove /run/netns/daens (resource busy); a reboot may be required"
|
||||
rc=1
|
||||
fi
|
||||
fi
|
||||
|
||||
ip link del dae0 2>/dev/null || true
|
||||
# 3. Remove the dae0 veth pair.
|
||||
if ip link show dae0 >/dev/null 2>&1; then
|
||||
if ! ip link del dae0 2>/dev/null; then
|
||||
logger -t daed-init "cleanup: failed to remove dae0 veth pair"
|
||||
rc=1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. The pin root is normal persistent state. Never remove it or
|
||||
# make its existence change the cleanup result.
|
||||
if [ -e /sys/fs/bpf/daed ]; then
|
||||
logger -t daed-init "cleanup: /sys/fs/bpf/daed exists; leaving normal pin root unchanged"
|
||||
fi
|
||||
|
||||
# Final verification covers only netns and veth state.
|
||||
[ ! -e /run/netns/daens ] || return 1
|
||||
! ip netns list 2>/dev/null | awk '$1 == "daens" { found=1 } END { exit !found }' || return 1
|
||||
! ip netns list 2>/dev/null | grep -Eq '^daens([[:space:]]|$)' || return 1
|
||||
! ip link show dae0 >/dev/null 2>&1 || return 1
|
||||
|
||||
return $rc
|
||||
}
|
||||
|
||||
+118
-16
@@ -1,29 +1,131 @@
|
||||
#!/bin/sh
|
||||
# Keep daed as a child so signals can be forwarded and stale netns/veth
|
||||
# state can be cleaned before start and after exit. daed owns TC detach;
|
||||
# /sys/fs/bpf/daed is normal persistent state and is never removed here.
|
||||
|
||||
. /usr/share/daed/cleanup.sh
|
||||
|
||||
# Pre-start cleanup refuses to remove runtime state while another
|
||||
# daed instance is active, and fails closed if that probe is broken.
|
||||
DAED_GUARD_CLEANUP=start
|
||||
if ! daed_cleanup_runtime; then
|
||||
echo "daed: stale network state could not be removed" >&2
|
||||
echo "daed: stale /usr/bin/daed or netns state could not be verified or removed; refusing to start. Check process and netns state." >&2
|
||||
logger -t daed-init "pre-start cleanup failed: refusing to start daed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Keep daed as a child so a panic or unexpected exit is followed by an
|
||||
# immediate teardown of all kernel/runtime state before procd can respawn us.
|
||||
# Keep daed as a child so post-exit cleanup runs before procd can
|
||||
# respawn it.
|
||||
child_pid=
|
||||
cleanup_child() {
|
||||
[ -n "$child_pid" ] && kill "$child_pid" 2>/dev/null
|
||||
pending_signal=
|
||||
shutdown_signal=
|
||||
shutdown_elapsed=0
|
||||
forced_kill=0
|
||||
child_term_timeout=20
|
||||
|
||||
forward_signal() {
|
||||
local sig="$1"
|
||||
if [ -z "$child_pid" ]; then
|
||||
pending_signal="$sig"
|
||||
logger -t daed-init "signal $sig received before daed child started; launch cancelled"
|
||||
return 0
|
||||
fi
|
||||
case "$sig" in
|
||||
TERM|INT|QUIT)
|
||||
if [ -z "$shutdown_signal" ]; then
|
||||
shutdown_signal="$sig"
|
||||
shutdown_elapsed=0
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
kill -"$sig" "$child_pid" 2>/dev/null
|
||||
}
|
||||
trap cleanup_child TERM INT
|
||||
|
||||
/usr/bin/daed "$@" &
|
||||
child_pid=$!
|
||||
wait "$child_pid"
|
||||
status=$?
|
||||
child_pid=
|
||||
trap - TERM INT
|
||||
exit_with_signal() {
|
||||
local sig="$1"
|
||||
trap - TERM INT HUP QUIT
|
||||
kill -"$sig" "$$" 2>/dev/null
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ! daed_cleanup_runtime; then
|
||||
echo "daed: runtime cleanup after exit failed" >&2
|
||||
status=1
|
||||
child_is_running() {
|
||||
local state
|
||||
kill -0 "$child_pid" 2>/dev/null || return 1
|
||||
state=$(awk '{ print $3 }' "/proc/$child_pid/stat" 2>/dev/null)
|
||||
[ "$state" != "Z" ]
|
||||
}
|
||||
trap 'forward_signal TERM' TERM
|
||||
trap 'forward_signal INT' INT
|
||||
trap 'forward_signal HUP' HUP
|
||||
trap 'forward_signal QUIT' QUIT
|
||||
|
||||
start_child() {
|
||||
local sig
|
||||
# Keep this check inside the function as well as at the call site:
|
||||
# it closes the ordinary pre-start window, while the pending-signal
|
||||
# path below handles a signal arriving during the background fork.
|
||||
[ -z "$pending_signal" ] || return 125
|
||||
/usr/bin/daed "$@" &
|
||||
child_pid=$!
|
||||
if [ -n "$pending_signal" ]; then
|
||||
sig="$pending_signal"
|
||||
pending_signal=
|
||||
forward_signal "$sig"
|
||||
fi
|
||||
}
|
||||
|
||||
if [ -n "$pending_signal" ]; then
|
||||
logger -t daed-init "refusing to start daed after pending signal $pending_signal"
|
||||
exit_with_signal "$pending_signal"
|
||||
fi
|
||||
exit "$status"
|
||||
|
||||
# Best-effort OOM preference; failure must not block startup.
|
||||
if ! echo -16 > /proc/self/oom_score_adj 2>/dev/null; then
|
||||
logger -t daed-init "warn: failed to set /proc/self/oom_score_adj; continuing without OOM preference"
|
||||
fi
|
||||
if ! start_child "$@"; then
|
||||
logger -t daed-init "refusing to start daed after pending signal $pending_signal"
|
||||
if [ -n "$pending_signal" ]; then
|
||||
exit_with_signal "$pending_signal"
|
||||
fi
|
||||
exit 1
|
||||
fi
|
||||
status=0
|
||||
reaped=0
|
||||
while [ "$reaped" -eq 0 ]; do
|
||||
if [ -n "$shutdown_signal" ] && child_is_running; then
|
||||
if [ "$shutdown_elapsed" -ge "$child_term_timeout" ]; then
|
||||
if [ "$forced_kill" -eq 0 ]; then
|
||||
logger -t daed-init "daed did not exit within ${child_term_timeout}s after $shutdown_signal; sending KILL"
|
||||
kill -KILL "$child_pid" 2>/dev/null
|
||||
forced_kill=1
|
||||
fi
|
||||
else
|
||||
sleep 1
|
||||
shutdown_elapsed=$((shutdown_elapsed + 1))
|
||||
continue
|
||||
fi
|
||||
sleep 1
|
||||
continue
|
||||
fi
|
||||
wait "$child_pid" 2>/dev/null
|
||||
status=$?
|
||||
if ! child_is_running; then
|
||||
reaped=1
|
||||
fi
|
||||
done
|
||||
child_pid=
|
||||
trap - TERM INT HUP QUIT
|
||||
|
||||
# Post-exit cleanup runs after the child is reaped.
|
||||
DAED_GUARD_CLEANUP=post-exit
|
||||
cleanup_status=0
|
||||
daed_cleanup_runtime || cleanup_status=$?
|
||||
if [ "$cleanup_status" -ne 0 ]; then
|
||||
echo "daed: runtime cleanup after exit failed" >&2
|
||||
logger -t daed-init "post-exit cleanup failed; check ip netns / ip link show"
|
||||
fi
|
||||
if [ "$status" -ne 0 ]; then
|
||||
exit "$status"
|
||||
fi
|
||||
exit "$cleanup_status"
|
||||
|
||||
+37
-13
@@ -1,5 +1,7 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# Copyright (C) 2023 Tianling Shen <cnsztl@immortalwrt.org>
|
||||
# daed-guard handles bounded child shutdown and post-exit netns/veth cleanup.
|
||||
# Keep the log file and a pre-stop state snapshot for diagnostics.
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
@@ -10,18 +12,28 @@ LOG="/var/log/daed/daed.log"
|
||||
|
||||
. /usr/share/daed/cleanup.sh
|
||||
|
||||
log() {
|
||||
logger -t daed-init "$@"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
log "start: begin"
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
if [ "$enabled" -ne "1" ]; then
|
||||
log "start: config disabled, exit"
|
||||
return 1
|
||||
fi
|
||||
|
||||
local listen_addr log_maxbackups log_maxsize
|
||||
config_get listen_addr "config" "listen_addr" "0.0.0.0:2023"
|
||||
config_get log_maxbackups "config" "log_maxbackups" "1"
|
||||
config_get log_maxsize "config" "log_maxsize" "5"
|
||||
|
||||
log "start: listen=$listen_addr log_maxbackups=$log_maxbackups log_maxsize=$log_maxsize"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param env DAE_LOCATION_ASSET="/usr/share/v2ray" TZ="$(uci -q get system.@system[0].zonename)"
|
||||
procd_set_param command "$PROG" run
|
||||
@@ -33,25 +45,37 @@ start_service() {
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
# Avoid an endless crash/respawn loop which can repeatedly reattach dae's
|
||||
# data-plane hooks and make the router management plane unreachable.
|
||||
procd_set_param respawn 3600 5 5
|
||||
# daed-guard escalates its child after 20 seconds. Leave time for
|
||||
# reap and post-exit cleanup before procd kills the wrapper.
|
||||
procd_set_param term_timeout 30
|
||||
# procd_set_param respawn: arguments are (threshold, timeout, retry).
|
||||
# threshold = runtime that resets the short-lived exit counter
|
||||
# timeout = seconds to wait between retries
|
||||
# retry = maximum short-lived exits before procd gives up
|
||||
# Reset the counter after one hour of stable runtime; otherwise retry
|
||||
# after 5 seconds and stop after 10 failures.
|
||||
procd_set_param respawn 3600 5 10
|
||||
# daed-guard sets oom_score_adj before forking; procd has no
|
||||
# oom_adj/oom_score_adj parameter.
|
||||
# procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
log "start: procd_open_instance done"
|
||||
}
|
||||
|
||||
stop_service() {
|
||||
rm -f "$LOG"
|
||||
daed_cleanup_runtime
|
||||
}
|
||||
|
||||
restart() {
|
||||
stop
|
||||
sleep 1
|
||||
daed_cleanup_runtime
|
||||
start
|
||||
log "stop: begin"
|
||||
# Cleanup runs in daed-guard after its child exits. Record only a
|
||||
# pre-stop snapshot here; pin entries do not prove TC attachment.
|
||||
local pinned="" ns_left=""
|
||||
if [ -d /sys/fs/bpf/daed ]; then
|
||||
pinned=$(ls /sys/fs/bpf/daed 2>/dev/null | tr '\n' ' ')
|
||||
fi
|
||||
if ip netns list 2>/dev/null | grep -q '^daens'; then
|
||||
ns_left="daens"
|
||||
fi
|
||||
log "stop: pre-stop state — bpf_pin_entries=[${pinned:-none}] netns_left=[${ns_left:-none}]"
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
|
||||
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_DOCKERMANAGER:=$(ARCH)
|
||||
|
||||
PKG_NAME:=dockermanager
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.17
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=0.1.1
|
||||
PKG_RELEASE:=7
|
||||
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
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
-include $(dir $(lastword $(MAKEFILE_LIST)))../version.mk
|
||||
|
||||
PKG_NAME:=easytier-next
|
||||
PKG_VERSION:=2.6.4
|
||||
PKG_RELEASE:=1
|
||||
|
||||
# Architecture mapping for pre-built binaries
|
||||
ifeq ($(ARCH),mipsel)
|
||||
APP_ARCH:=mipsel
|
||||
endif
|
||||
ifeq ($(ARCH),mips)
|
||||
APP_ARCH:=mips
|
||||
endif
|
||||
ifeq ($(ARCH),arm)
|
||||
APP_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(BOARD),kirkwood)
|
||||
APP_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(ARCH),armv7)
|
||||
APP_ARCH:=armv7
|
||||
endif
|
||||
ifeq ($(ARCH),aarch64)
|
||||
APP_ARCH:=aarch64
|
||||
endif
|
||||
ifeq ($(ARCH),arm64)
|
||||
APP_ARCH:=aarch64
|
||||
endif
|
||||
ifeq ($(ARCH),armv8)
|
||||
APP_ARCH:=aarch64
|
||||
endif
|
||||
ifeq ($(ARCH),x86_64)
|
||||
APP_ARCH:=x86_64
|
||||
endif
|
||||
|
||||
PKG_SOURCE:=easytier-linux-$(APP_ARCH)-v$(PKG_VERSION).zip
|
||||
PKG_SOURCE_URL:=https://github.com/EasyTier/EasyTier/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=Dante <dante@seckv.com>
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=VPN
|
||||
TITLE:=A simple, decentralized mesh VPN with WireGuard support (Next)
|
||||
URL:=https://github.com/EasyTier/EasyTier
|
||||
DEPENDS:=@(x86_64||arm||aarch64||mipsel||mips) +kmod-tun
|
||||
PROVIDES:=easytier
|
||||
CONFLICTS:=easytier
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
A simple, decentralized mesh VPN with WireGuard support.
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/config
|
||||
config EASYTIER_INCLUDE_WEBCONSOLE
|
||||
bool "Include Web Console (easytier-web)"
|
||||
depends on PACKAGE_$(PKG_NAME)
|
||||
depends on !(mips || mipsel)
|
||||
default y
|
||||
help
|
||||
Install the easytier-web web console.
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
unzip -q -o $(DL_DIR)/$(PKG_SOURCE) -d $(PKG_BUILD_DIR)
|
||||
find $(PKG_BUILD_DIR) -mindepth 2 -type f -exec mv -f {} $(PKG_BUILD_DIR)/ \;
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-core $(1)/usr/bin/
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-cli $(1)/usr/bin/
|
||||
ifdef CONFIG_EASYTIER_INCLUDE_WEBCONSOLE
|
||||
if [ -f $(PKG_BUILD_DIR)/easytier-web-embed ]; then \
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-web-embed $(1)/usr/bin/easytier-web; \
|
||||
elif [ -f $(PKG_BUILD_DIR)/easytier-web ]; then \
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/easytier-web $(1)/usr/bin/easytier-web; \
|
||||
fi
|
||||
endif
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
+6
-8
@@ -9,15 +9,14 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_FASTNET:=$(ARCH)
|
||||
|
||||
PKG_NAME:=fastnet
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compatible
|
||||
PKG_SOURCE_DATE:=0.7.2
|
||||
PKG_RELEASE:=2
|
||||
PKG_VERSION:=0.7.7
|
||||
PKG_RELEASE:=4
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-binary-$(PKG_SOURCE_DATE).tar.gz
|
||||
PKG_SOURCE_URL:=http://dl.istoreos.com/binary/fastnet/
|
||||
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)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_SOURCE_DATE)
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-binary-$(PKG_VERSION)
|
||||
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_USE_MIPS16:=0
|
||||
@@ -30,7 +29,6 @@ define Package/$(PKG_NAME)
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=FastNet - network test Web UI
|
||||
DEPENDS:=@(arm||x86_64||aarch64)
|
||||
PKGARCH:=all
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
@@ -57,7 +55,7 @@ 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)/fastnet.$(PKG_ARCH_FASTNET) $(1)/usr/sbin/fastnet
|
||||
$(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
|
||||
|
||||
@@ -20,7 +20,7 @@ start_service() {
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param limits nofile="65535 65535"
|
||||
procd_set_param command /usr/sbin/fastnet web --addr "$addr" --no-open
|
||||
procd_set_param command /usr/sbin/FastNet web --addr "$addr" --no-open
|
||||
[ -n "$token" ] && procd_append_param command --token "$token"
|
||||
[ "$logger" = 1 ] && procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=filebrowser-q
|
||||
PKG_VERSION:=1.5.6-stable
|
||||
PKG_RELEASE:=6
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/gtsteffaniak/filebrowser/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/filebrowser-$(PKG_VERSION)
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=kiddin9
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host node/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
GO_PKG:=github.com/gtsteffaniak/filebrowser/backend
|
||||
GO_PKG_BUILD_DIR:=$(PKG_BUILD_DIR)/backend
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
$(GO_PKG)/internal/version.Version=v$(PKG_VERSION) \
|
||||
$(GO_PKG)/internal/version.CommitSHA=$(PKG_VERSION)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
define Package/filebrowser-q
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
SUBMENU:=Filesystem
|
||||
TITLE:=FileBrowser Quantum - Modern Web File Manager
|
||||
URL:=https://github.com/gtsteffaniak/filebrowser
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS)
|
||||
endef
|
||||
|
||||
define Package/filebrowser-q/description
|
||||
FileBrowser Quantum provides a modern, responsive web-based file management
|
||||
interface with multi-source, real-time search, and enhanced preview features.
|
||||
endef
|
||||
|
||||
define Package/filebrowser-q/conffiles
|
||||
/etc/filebrowser-q/
|
||||
/etc/config/filebrowser-q
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
( \
|
||||
pushd $(PKG_BUILD_DIR)/frontend && \
|
||||
npm install && \
|
||||
npm run build ; \
|
||||
)
|
||||
( \
|
||||
cd $(PKG_BUILD_DIR)/backend && \
|
||||
$(GO_PKG_VARS) \
|
||||
go build \
|
||||
-trimpath \
|
||||
-ldflags="-w -s" \
|
||||
-o $(PKG_BUILD_DIR)/filebrowser . ; \
|
||||
)
|
||||
endef
|
||||
|
||||
define Package/filebrowser-q/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/filebrowser $(1)/usr/bin/filebrowser-q
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser-q
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser-q
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,filebrowser-q))
|
||||
@@ -0,0 +1,5 @@
|
||||
config filebrowser 'config'
|
||||
option enabled '0'
|
||||
option listen_port '8787'
|
||||
option root_path '/'
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
|
||||
CONF="filebrowser-q"
|
||||
PROG="/usr/bin/filebrowser-q"
|
||||
CONF_PATH="/etc/filebrowser-q/config.yaml"
|
||||
DB_PATH="/etc/filebrowser-q/database.db"
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
local listen_port root_path root_name
|
||||
config_get listen_port "config" "listen_port" "8787"
|
||||
config_get root_path "config" "root_path" "/"
|
||||
|
||||
root_name=""
|
||||
[ "$root_path" = "/" ] && root_name="root"
|
||||
|
||||
if [ ! -f "$CONF_PATH" ]; then
|
||||
mkdir -p "$(dirname "$CONF_PATH")"
|
||||
cat <<EOF > "$CONF_PATH"
|
||||
server:
|
||||
port: $listen_port
|
||||
database: "$DB_PATH"
|
||||
sources:
|
||||
- path: "$root_path"
|
||||
name: "$root_name"
|
||||
config:
|
||||
defaultEnabled: true
|
||||
auth:
|
||||
adminUsername: "admin"
|
||||
adminPassword: "admin"
|
||||
userDefaults:
|
||||
ui:
|
||||
locale: "zhCN"
|
||||
EOF
|
||||
else
|
||||
grep -q " name:" "$CONF_PATH" || sed -i "s,.*- path:.*,&\n name: \"\"," "$CONF_PATH"
|
||||
sed -e "s/ port:.*/ port: $listen_port/" \
|
||||
-e "s, - path:.*, - path: \"$root_path\"," \
|
||||
-e "s/ name: \".*\"/ name: \"$root_name\"/" \
|
||||
-i "$CONF_PATH"
|
||||
fi
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command -c "$CONF_PATH"
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
}
|
||||
+49
-40
@@ -1,47 +1,46 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2017-2024
|
||||
#
|
||||
# This is free software, licensed under the GNU General Public License v2.
|
||||
#
|
||||
# Copyright (C) 2021 ImmortalWrt.org
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=filebrowser
|
||||
PKG_VERSION:=1.5.6-stable
|
||||
PKG_RELEASE=1
|
||||
PKG_VERSION:=2.63.23
|
||||
PKG_RELEASE:=23
|
||||
|
||||
ifeq ($(ARCH),aarch64)
|
||||
PKG_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
else ifeq ($(ARCH),arm)
|
||||
PKG_ARCH:=armv7
|
||||
PKG_HASH:=skip
|
||||
else ifeq ($(ARCH),x86_64)
|
||||
PKG_ARCH:=amd64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/filebrowser/filebrowser/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=Tianling Shen <cnsztl@immortalwrt.org>
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host node/host node-pnpm/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
GO_PKG:=github.com/filebrowser/filebrowser/v2
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
$(GO_PKG)/version.CommitSHA=$(PKG_VERSION) \
|
||||
$(GO_PKG)/version.Version=v$(PKG_VERSION)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
define Package/filebrowser
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
DEPENDS:=@(arm||aarch64||x86_64)
|
||||
TITLE:=FileBrowser Quantum
|
||||
SECTION:=utils
|
||||
CATEGORY:=Utilities
|
||||
TITLE:=Web File Browser
|
||||
URL:=https://github.com/filebrowser/filebrowser
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS)
|
||||
endef
|
||||
|
||||
define Package/filebrowser/description
|
||||
The best free self-hosted web-based file manager.
|
||||
endef
|
||||
|
||||
PKG_SOURCE:=linux-$(PKG_ARCH)-filebrowser
|
||||
PKG_SOURCE_URL:=https://github.com/gtsteffaniak/filebrowser/releases/download/v$(PKG_VERSION)
|
||||
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
filebrowser provides a file managing interface within a specified directory
|
||||
and it can be used to upload, delete, preview, rename and edit your files.
|
||||
It allows the creation of multiple users and each user can have its own directory.
|
||||
It can be used as a standalone app or as a middleware.
|
||||
endef
|
||||
|
||||
define Package/filebrowser/conffiles
|
||||
@@ -49,15 +48,25 @@ define Package/filebrowser/conffiles
|
||||
/etc/config/filebrowser
|
||||
endef
|
||||
|
||||
define Package/filebrowser/install
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser
|
||||
$(INSTALL_DIR) $(1)/etc/filebrowser
|
||||
$(INSTALL_CONF) $(CURDIR)/files/config.yaml $(1)/etc/filebrowser/config.yaml
|
||||
$(INSTALL_DIR) $(1)/usr/bin/
|
||||
$(INSTALL_BIN) $(DL_DIR)/linux-$(PKG_ARCH)-filebrowser $(1)/usr/bin/filebrowser
|
||||
define Build/Compile
|
||||
( \
|
||||
export PNPM_HOME="$(PKG_BUILD_DIR)/frontend" ; \
|
||||
pushd "$(PKG_BUILD_DIR)/frontend" ; \
|
||||
pnpm install --frozen-lockfile ; \
|
||||
pnpm run build ; \
|
||||
popd ; \
|
||||
$(call GoPackage/Build/Compile) ; \
|
||||
)
|
||||
endef
|
||||
|
||||
define Package/filebrowser/install
|
||||
$(call GoPackage/Package/Install/Bin,$(1))
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/filebrowser.config $(1)/etc/config/filebrowser
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/filebrowser.init $(1)/etc/init.d/filebrowser
|
||||
endef
|
||||
|
||||
$(eval $(call GoBinPackage,filebrowser))
|
||||
$(eval $(call BuildPackage,filebrowser))
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
server:
|
||||
port: 8989
|
||||
database: "/etc/filebrowser/database.db"
|
||||
sources:
|
||||
- path: "/" # Do not use a root "/" directory or include the "/var" folder
|
||||
config:
|
||||
defaultEnabled: true
|
||||
auth:
|
||||
adminUsername: admin
|
||||
adminPassword: "admin"
|
||||
userDefaults:
|
||||
ui:
|
||||
locale: "cn"
|
||||
@@ -2,4 +2,6 @@ config filebrowser 'config'
|
||||
option enabled '0'
|
||||
option listen_port '8989'
|
||||
option root_path '/'
|
||||
option base_url ''
|
||||
option disable_exec '1'
|
||||
|
||||
|
||||
@@ -1,33 +1,50 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
CONF="filebrowser"
|
||||
PROG="/usr/bin/filebrowser"
|
||||
CONF_PATH="/etc/filebrowser/config.yaml"
|
||||
PID_FILE="/var/run/filebrowser.pid"
|
||||
DB_PATH="/etc/filebrowser/database.db"
|
||||
|
||||
start() {
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
local enabled
|
||||
config_get_bool enabled "config" "enabled" "0"
|
||||
[ "$enabled" -eq "1" ] || return 1
|
||||
|
||||
local listen_port root_path base_url
|
||||
mkdir -p "${DB_PATH%/*}"
|
||||
|
||||
local listen_port root_path base_url disable_exec
|
||||
config_get listen_port "config" "listen_port" "8989"
|
||||
config_get root_path "config" "root_path" "/"
|
||||
|
||||
echo "Starting filebrowser..."
|
||||
sed -e "s/ port:.*/ port: $listen_port/" \
|
||||
-e "s, - path:.*, - path: \"$root_path\"," \
|
||||
-i "$CONF_PATH"
|
||||
start-stop-daemon -S -q -b -m -p "$PID_FILE" -x "$PROG" -- -c "$CONF_PATH"
|
||||
config_get root_path "config" "root_path" "/mnt/"
|
||||
config_get base_url "config" "base_url"
|
||||
config_get_bool disable_exec "config" "disable_exec" "1"
|
||||
|
||||
procd_open_instance
|
||||
procd_set_param command "$PROG"
|
||||
procd_append_param command --database "$DB_PATH"
|
||||
procd_append_param command --address "[::]"
|
||||
procd_append_param command --port "$listen_port"
|
||||
procd_append_param command --root "$root_path"
|
||||
procd_append_param command --disable-exec="$disable_exec"
|
||||
[ -z "$base_url" ] || procd_append_param command --baseurl "$base_url"
|
||||
if [ ! -e "$DB_PATH" ]; then
|
||||
procd_append_param command --username "admin"
|
||||
procd_append_param command --password "$("$PROG" hash "admin")"
|
||||
fi
|
||||
|
||||
procd_set_param limits core="unlimited"
|
||||
procd_set_param limits nofile="1000000 1000000"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
procd_close_instance
|
||||
|
||||
}
|
||||
|
||||
stop() {
|
||||
kill -9 `pidof filebrowser | sed "s/$$//g"` 2>/dev/null
|
||||
rm -f "$PID_FILE"
|
||||
echo "filebrowser stopped"
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
}
|
||||
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=frp
|
||||
PKG_VERSION:=0.70.1
|
||||
PKG_RELEASE:=5
|
||||
PKG_VERSION:=0.71.0
|
||||
PKG_RELEASE:=10
|
||||
|
||||
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/fatedier/frp/tar.gz/v$(PKG_VERSION)?
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
config init
|
||||
option enabled '0'
|
||||
option stdout '1'
|
||||
option stderr '1'
|
||||
# Uncomment to run frpc as an existing user/group. Keep disabled by
|
||||
|
||||
+11
-4
@@ -912,6 +912,7 @@ service_triggers() {
|
||||
|
||||
start_service() {
|
||||
local init_cfg=
|
||||
local enabled=0
|
||||
local stdout=1
|
||||
local stderr=1
|
||||
local respawn=1
|
||||
@@ -919,14 +920,19 @@ start_service() {
|
||||
local run_group=
|
||||
local old_umask
|
||||
|
||||
mkdir -p /var/etc
|
||||
_TOML_ERR=0
|
||||
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
|
||||
|
||||
config_load "$NAME"
|
||||
|
||||
config_foreach _find_init_section init
|
||||
|
||||
if [ -n "$init_cfg" ]; then
|
||||
config_get_bool enabled "$init_cfg" enabled 0
|
||||
fi
|
||||
[ "$enabled" -eq 1 ] || return 0
|
||||
|
||||
mkdir -p /var/etc
|
||||
_TOML_ERR=0
|
||||
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
|
||||
|
||||
old_umask="$(umask)"
|
||||
umask 077
|
||||
: > "$CONF_FILE" || {
|
||||
@@ -950,6 +956,7 @@ start_service() {
|
||||
|
||||
if [ -n "$init_cfg" ]; then
|
||||
config_list_foreach "$init_cfg" conf_inc _append_conf_file
|
||||
[ "$_TOML_ERR" = "0" ] || return 1
|
||||
|
||||
config_get_bool stdout "$init_cfg" stdout 1
|
||||
config_get_bool stderr "$init_cfg" stderr 1
|
||||
|
||||
@@ -79,6 +79,7 @@ upgrade_init() {
|
||||
changed=1
|
||||
fi
|
||||
|
||||
set_if_empty "$init_section" enabled 1
|
||||
set_if_empty "$init_section" stdout 1
|
||||
set_if_empty "$init_section" stderr 1
|
||||
set_if_empty "$init_section" respawn 1
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
config init
|
||||
option enabled '0'
|
||||
option stdout '1'
|
||||
option stderr '1'
|
||||
# Uncomment to run frps as an existing user/group. Keep disabled by
|
||||
@@ -18,7 +19,6 @@ config init
|
||||
config conf 'common'
|
||||
option bind_addr '0.0.0.0'
|
||||
option bind_port '7000'
|
||||
|
||||
option authentication_method 'token'
|
||||
# option token 'your_token'
|
||||
# Alternatively, load a token from a file or command. Exec token sources
|
||||
@@ -31,12 +31,10 @@ config conf 'common'
|
||||
# list token_source_exec_args '--format'
|
||||
# list token_source_exec_args 'raw'
|
||||
# list token_source_exec_env 'TOKEN_SERVICE=production'
|
||||
|
||||
option max_pool_count '5'
|
||||
option tcp_mux 'true'
|
||||
option tls_force 'false'
|
||||
option detailed_errors_to_client 'true'
|
||||
|
||||
# Web server is disabled when admin_port is empty or 0.
|
||||
# option admin_addr '127.0.0.1'
|
||||
# option admin_port '7500'
|
||||
@@ -47,15 +45,12 @@ config conf 'common'
|
||||
option enable_prometheus 'false'
|
||||
# option admin_tls_cert_file '/etc/ssl/acme/example.com.fullchain.crt'
|
||||
# option admin_tls_key_file '/etc/ssl/acme/example.com.key'
|
||||
|
||||
# Allow ports can be single ports or ranges.
|
||||
# list allow_ports '2000-3000'
|
||||
# list allow_ports '3001'
|
||||
|
||||
option log_file 'console'
|
||||
option log_level 'info'
|
||||
option log_max_days '3'
|
||||
|
||||
# List options with name "_" will be directly appended as raw TOML lines.
|
||||
# Use this only for options not covered by UCI options above.
|
||||
# Do not duplicate keys generated by UCI options above.
|
||||
@@ -68,7 +63,6 @@ config conf 'common'
|
||||
# option path '/handler'
|
||||
# list ops 'Login'
|
||||
# option tls_verify 'false'
|
||||
|
||||
#config http_plugin 'port_manager'
|
||||
# option name 'port-manager'
|
||||
# option addr '127.0.0.1:9001'
|
||||
|
||||
+11
-4
@@ -689,6 +689,7 @@ service_triggers() {
|
||||
|
||||
start_service() {
|
||||
local init_cfg=
|
||||
local enabled=0
|
||||
local stdout=1
|
||||
local stderr=1
|
||||
local respawn=1
|
||||
@@ -696,14 +697,19 @@ start_service() {
|
||||
local run_group=
|
||||
local old_umask
|
||||
|
||||
mkdir -p /var/etc
|
||||
_TOML_ERR=0
|
||||
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
|
||||
|
||||
config_load "$NAME"
|
||||
|
||||
config_foreach _find_init_section init
|
||||
|
||||
if [ -n "$init_cfg" ]; then
|
||||
config_get_bool enabled "$init_cfg" enabled 0
|
||||
fi
|
||||
[ "$enabled" -eq 1 ] || return 0
|
||||
|
||||
mkdir -p /var/etc
|
||||
_TOML_ERR=0
|
||||
_ALLOW_UNSAFE_TOKEN_SOURCE_EXEC=0
|
||||
|
||||
old_umask="$(umask)"
|
||||
umask 077
|
||||
: > "$CONF_FILE" || {
|
||||
@@ -727,6 +733,7 @@ start_service() {
|
||||
|
||||
if [ -n "$init_cfg" ]; then
|
||||
config_list_foreach "$init_cfg" conf_inc _append_conf_file
|
||||
[ "$_TOML_ERR" = "0" ] || return 1
|
||||
|
||||
config_get_bool stdout "$init_cfg" stdout 1
|
||||
config_get_bool stderr "$init_cfg" stderr 1
|
||||
|
||||
@@ -90,6 +90,7 @@ upgrade_init() {
|
||||
changed=1
|
||||
fi
|
||||
|
||||
set_if_empty "$init_section" enabled 1
|
||||
set_if_empty "$init_section" stdout 1
|
||||
set_if_empty "$init_section" stderr 1
|
||||
set_if_empty "$init_section" respawn 1
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=frpc-next
|
||||
PKG_VERSION:=0.71.0
|
||||
PKG_RELEASE:=1
|
||||
|
||||
# Architecture mapping for pre-built binaries
|
||||
ifeq ($(ARCH),mipsel)
|
||||
FRP_ARCH:=mipsle
|
||||
endif
|
||||
ifeq ($(ARCH),mips)
|
||||
FRP_ARCH:=mips
|
||||
endif
|
||||
ifeq ($(ARCH),i386)
|
||||
FRP_ARCH:=386
|
||||
endif
|
||||
ifeq ($(ARCH),x86_64)
|
||||
FRP_ARCH:=amd64
|
||||
endif
|
||||
ifeq ($(ARCH),arm)
|
||||
FRP_ARCH:=arm
|
||||
endif
|
||||
ifeq ($(ARCH),aarch64)
|
||||
FRP_ARCH:=arm64
|
||||
endif
|
||||
|
||||
PKG_SOURCE:=frp_$(PKG_VERSION)_linux_$(FRP_ARCH).tar.gz
|
||||
PKG_SOURCE_URL:=https://github.com/fatedier/frp/releases/download/v$(PKG_VERSION)/
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_LICENSE_FILES:=LICENSE
|
||||
PKG_MAINTAINER:=Dante <dante@seckv.com>
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/frp_$(PKG_VERSION)_linux_$(FRP_ARCH)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/frpc-next
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
TITLE:=A fast reverse proxy (Client) - Binary
|
||||
URL:=https://github.com/fatedier/frp
|
||||
DEPENDS:=@(i386||x86_64||arm||aarch64||mipsel||mips)
|
||||
PROVIDES:=frpc
|
||||
CONFLICTS:=frpc
|
||||
endef
|
||||
|
||||
define Package/frpc-next/description
|
||||
frp is a fast reverse proxy to help you expose a local server behind a
|
||||
NAT or firewall to the internet. This package contains the pre-built frpc client.
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
endef
|
||||
|
||||
define Package/frpc-next/install
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/frpc $(1)/usr/bin/
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,frpc-next))
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
|
||||
frpc --version | grep "$PKG_VERSION"
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 自动更新 OpenWrt frpc-next 包的版本和 Hash
|
||||
# 依赖: curl, grep, sed, sha256sum (或 shasum)
|
||||
|
||||
set -e
|
||||
|
||||
PKG_DIR=$(cd "$(dirname "$0")"; pwd)
|
||||
MAKEFILE="$PKG_DIR/Makefile"
|
||||
|
||||
if [ ! -f "$MAKEFILE" ]; then
|
||||
echo "错误: 找不到 Makefile 文件 ($MAKEFILE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 1. 获取当前本地版本
|
||||
CURRENT_VERSION=$(grep 'PKG_VERSION:=' "$MAKEFILE" | cut -d'=' -f2 | tr -d ' ')
|
||||
echo "当前本地版本: $CURRENT_VERSION"
|
||||
|
||||
# 2. 获取 GitHub 上的最新版本号
|
||||
LATEST_VERSION=$(curl -s https://api.github.com/repos/fatedier/frp/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/' | sed 's/^v//')
|
||||
|
||||
if [ -z "$LATEST_VERSION" ]; then
|
||||
echo "错误: 无法从 GitHub 获取最新版本号。"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "GitHub 最新版本: $LATEST_VERSION"
|
||||
|
||||
# 3. 比较版本
|
||||
if [ "$CURRENT_VERSION" == "$LATEST_VERSION" ]; then
|
||||
echo "版本已经是最新,无需更新。"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "发现新版本! 正在准备更新..."
|
||||
|
||||
# 4. 更新 Makefile
|
||||
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||
sed -i '' "s/PKG_VERSION:=.*/PKG_VERSION:=$LATEST_VERSION/" "$MAKEFILE"
|
||||
else
|
||||
sed -i "s/PKG_VERSION:=.*/PKG_VERSION:=$LATEST_VERSION/" "$MAKEFILE"
|
||||
fi
|
||||
|
||||
echo "Makefile 更新成功: $CURRENT_VERSION -> $LATEST_VERSION (Binary Mode)"
|
||||
+5
-3
@@ -2,7 +2,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=gecoosac
|
||||
PKG_VERSION:=2.2.20251015
|
||||
PKG_RELEASE:=20
|
||||
PKG_RELEASE:=21
|
||||
|
||||
PKG_MAINTAINER:=Roc Lai <laipeng668@qq.com>
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
@@ -43,8 +43,8 @@ define Package/$(PKG_NAME)
|
||||
CATEGORY:=Network
|
||||
TITLE:=gecoosac server (version $(PKG_VERSION))
|
||||
URL:=http://www.cnrouter.com/
|
||||
DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util
|
||||
PROVIDES:=gecoosac-files
|
||||
DEPENDS:=@(aarch64||arm||i386||mips||mipsel||x86_64) +openssl-util +coreutils-timeout
|
||||
PROVIDES:=gecoosac-files gecoosac-common
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
@@ -66,11 +66,13 @@ define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/etc/gecoosac/tls
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_DIR) $(1)/etc/uci-defaults
|
||||
$(INSTALL_DIR) $(1)/usr/share/gecoosac
|
||||
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/$(PKG_NAME) $(1)/usr/bin
|
||||
$(INSTALL_BIN) ./files/etc/init.d/gecoosac $(1)/etc/init.d/gecoosac
|
||||
$(INSTALL_BIN) ./files/etc/uci-defaults/gecoosac $(1)/etc/uci-defaults/gecoosac
|
||||
$(INSTALL_CONF) ./files/etc/config/gecoosac $(1)/etc/config/gecoosac
|
||||
$(INSTALL_DATA) ./files/usr/share/gecoosac/common.sh $(1)/usr/share/gecoosac/common.sh
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
. /lib/functions.sh
|
||||
. /lib/functions/procd.sh
|
||||
. /usr/share/gecoosac/common.sh
|
||||
|
||||
USE_PROCD=1
|
||||
|
||||
@@ -10,13 +11,9 @@ START=90
|
||||
STOP=10
|
||||
|
||||
PROG=/usr/bin/gecoosac
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
DEFAULT_LANG=zh
|
||||
CERT_TIMEOUT=60
|
||||
CERT_RENEW_BEFORE=2592000
|
||||
|
||||
init_conf() {
|
||||
local section_type
|
||||
@@ -47,193 +44,6 @@ init_conf() {
|
||||
config_get "log" "config" "log" "0"
|
||||
}
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
is_secure_dir() {
|
||||
local allow_sticky="$2" owner permissions metadata
|
||||
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
||||
metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1
|
||||
set -- $metadata
|
||||
permissions="$1"
|
||||
owner="$3"
|
||||
[ "$owner" = "0" ] || return 1
|
||||
case "$permissions" in
|
||||
d?????????) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \
|
||||
[ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then
|
||||
[ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_secure_upload_dir() {
|
||||
local path current part rest
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
[ -d "$path" ] && [ ! -L "$path" ] || return 1
|
||||
current="/"
|
||||
rest="${path#/}"
|
||||
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
current="${current%/}/$part"
|
||||
case "$current" in
|
||||
/tmp) is_secure_dir "$current" 1 || return 1 ;;
|
||||
*) is_secure_dir "$current" || return 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_port() {
|
||||
case "$1" in
|
||||
""|*[!0-9]*) return 1 ;;
|
||||
@@ -242,66 +52,6 @@ is_port() {
|
||||
[ "$1" -ge 1 ] 2>/dev/null && [ "$1" -le 65535 ]
|
||||
}
|
||||
|
||||
run_with_timeout() {
|
||||
local timeout pid i
|
||||
|
||||
timeout="$1"
|
||||
shift
|
||||
|
||||
"$@" >/dev/null 2>&1 &
|
||||
pid="$!"
|
||||
i=0
|
||||
|
||||
while kill -0 "$pid" >/dev/null 2>&1; do
|
||||
if [ "$i" -ge "$timeout" ]; then
|
||||
kill "$pid" >/dev/null 2>&1
|
||||
sleep 1
|
||||
kill -9 "$pid" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
run_with_timeout_output() {
|
||||
local timeout output pid i
|
||||
|
||||
timeout="$1"
|
||||
output="$2"
|
||||
shift 2
|
||||
|
||||
"$@" >"$output" 2>/dev/null &
|
||||
pid="$!"
|
||||
i=0
|
||||
|
||||
while kill -0 "$pid" >/dev/null 2>&1; do
|
||||
if [ "$i" -ge "$timeout" ]; then
|
||||
kill "$pid" >/dev/null 2>&1
|
||||
kill -9 "$pid" >/dev/null 2>&1
|
||||
return 1
|
||||
fi
|
||||
sleep 1
|
||||
i=$((i + 1))
|
||||
done
|
||||
|
||||
wait "$pid"
|
||||
}
|
||||
|
||||
is_readable_regular_file() {
|
||||
local path="$1" tmp_dir status
|
||||
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-file.XXXXXX)" || return 1
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/check" \
|
||||
/bin/sh -c '[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ]' \
|
||||
gecoosac-file-check "$path" </dev/null
|
||||
status="$?"
|
||||
rm -rf "$tmp_dir"
|
||||
return "$status"
|
||||
}
|
||||
|
||||
is_ipv4() {
|
||||
local value="$1"
|
||||
local part count
|
||||
@@ -328,48 +78,55 @@ san_has_entry() {
|
||||
local san="$1"
|
||||
local entry="$2"
|
||||
|
||||
echo "$san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -F -x -q "$entry"
|
||||
printf '%s\n' "$san" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -F -x -q "$entry"
|
||||
}
|
||||
|
||||
cert_matches_san() {
|
||||
local cert_file="$1" cert_host="$2" cert_ip="$3"
|
||||
local san tmp_dir
|
||||
run_with_timeout_output() {
|
||||
local limit="$1" output="$2"
|
||||
shift 2
|
||||
|
||||
is_readable_regular_file "$cert_file" || return 1
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-cert.XXXXXX)" || return 1
|
||||
if ! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/san" \
|
||||
openssl x509 -in "$cert_file" -noout -ext subjectAltName </dev/null; then
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
fi
|
||||
san="$(cat "$tmp_dir/san" 2>/dev/null)"
|
||||
rm -rf "$tmp_dir"
|
||||
[ -n "$san" ] || return 1
|
||||
san_has_entry "$san" "DNS:${cert_host}" || return 1
|
||||
[ -z "$cert_ip" ] || san_has_entry "$san" "IP Address:${cert_ip}" || return 1
|
||||
|
||||
return 0
|
||||
# coreutils-timeout waits for completion directly and also bounds commands
|
||||
# which ignore SIGTERM; no polling delay or orphaned sleep process is needed.
|
||||
timeout -k 1 "$limit" "$@" >"$output" 2>/dev/null
|
||||
}
|
||||
|
||||
cert_matches_key() {
|
||||
local cert_file="$1" key_file="$2"
|
||||
local cert_pub key_pub tmp_dir
|
||||
run_with_timeout() {
|
||||
local limit="$1"
|
||||
shift
|
||||
run_with_timeout_output "$limit" /dev/null "$@"
|
||||
}
|
||||
|
||||
cert_pair_valid() {
|
||||
local cert_file="$1" key_file="$2" cert_host="$3" cert_ip="$4"
|
||||
local min_lifetime="${5:-0}" tmp_dir cert_pub key_pub san status
|
||||
|
||||
is_readable_regular_file "$cert_file" || return 1
|
||||
is_readable_regular_file "$key_file" || return 1
|
||||
tmp_dir="$(mktemp -d /tmp/gecoosac-cert.XXXXXX)" || return 1
|
||||
if ! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/cert.pub" \
|
||||
openssl x509 -in "$cert_file" -noout -pubkey </dev/null || \
|
||||
! run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/key.pub" \
|
||||
openssl pkey -in "$key_file" -pubout </dev/null; then
|
||||
rm -rf "$tmp_dir"
|
||||
return 1
|
||||
status=1
|
||||
if run_with_timeout "$CERT_TIMEOUT" /bin/sh -c '
|
||||
[ -f "$1" ] && [ -r "$1" ] && [ -s "$1" ] &&
|
||||
[ -f "$2" ] && [ -r "$2" ] && [ -s "$2" ]
|
||||
' gecoosac-file-check "$cert_file" "$key_file" </dev/null && \
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/cert" \
|
||||
openssl x509 -in "$cert_file" -noout -pubkey -ext subjectAltName -checkend "$min_lifetime" </dev/null && \
|
||||
run_with_timeout_output "$CERT_TIMEOUT" "$tmp_dir/key.pub" \
|
||||
openssl pkey -in "$key_file" -pubout </dev/null && \
|
||||
run_with_timeout "$CERT_TIMEOUT" \
|
||||
openssl verify -partial_chain -trusted "$cert_file" "$cert_file" </dev/null; then
|
||||
# Trust the configured leaf only for the local validity-period check;
|
||||
# browser trust still depends on the certificate installed by the user.
|
||||
cert_pub="$(sed -n '/^-----BEGIN PUBLIC KEY-----$/,/^-----END PUBLIC KEY-----$/p' "$tmp_dir/cert")"
|
||||
key_pub="$(cat "$tmp_dir/key.pub")"
|
||||
if [ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ]; then
|
||||
status=0
|
||||
if [ -n "$cert_host" ]; then
|
||||
san="$(cat "$tmp_dir/cert")"
|
||||
san_has_entry "$san" "DNS:${cert_host}" || status=1
|
||||
[ -z "$cert_ip" ] || san_has_entry "$san" "IP Address:${cert_ip}" || status=1
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
cert_pub="$(cat "$tmp_dir/cert.pub" 2>/dev/null)"
|
||||
key_pub="$(cat "$tmp_dir/key.pub" 2>/dev/null)"
|
||||
rm -rf "$tmp_dir"
|
||||
|
||||
[ -n "$cert_pub" ] && [ "$cert_pub" = "$key_pub" ]
|
||||
return "$status"
|
||||
}
|
||||
|
||||
generate_default_cert() {
|
||||
@@ -389,7 +146,7 @@ generate_default_cert() {
|
||||
cert_ip="$(uci -q get network.lan.ipaddr)"
|
||||
is_ipv4 "$cert_ip" || cert_ip=""
|
||||
|
||||
if is_readable_regular_file "$DEFAULT_KEY_FILE" && cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then
|
||||
if cert_pair_valid "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE" "$cert_host" "$cert_ip" "$CERT_RENEW_BEFORE"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -423,9 +180,7 @@ generate_default_cert() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ ! -s "$tmp_crt" ] || [ ! -s "$tmp_key" ] || \
|
||||
! cert_matches_san "$tmp_crt" "$cert_host" "$cert_ip" || \
|
||||
! cert_matches_key "$tmp_crt" "$tmp_key" || \
|
||||
if ! cert_pair_valid "$tmp_crt" "$tmp_key" "$cert_host" "$cert_ip" "$CERT_RENEW_BEFORE" || \
|
||||
! chmod 600 "$tmp_key" || ! chmod 644 "$tmp_crt"; then
|
||||
rm -f "$tmp_crt" "$tmp_key"
|
||||
logger -t gecoosac "failed to validate default HTTPS certificate"
|
||||
@@ -468,10 +223,8 @@ generate_default_cert() {
|
||||
old_key=1
|
||||
fi
|
||||
|
||||
if mv "$tmp_key" "$DEFAULT_KEY_FILE" && \
|
||||
mv "$tmp_crt" "$DEFAULT_CRT_FILE" && \
|
||||
cert_matches_san "$DEFAULT_CRT_FILE" "$cert_host" "$cert_ip" && \
|
||||
cert_matches_key "$DEFAULT_CRT_FILE" "$DEFAULT_KEY_FILE"; then
|
||||
# Both temporary files were validated above; rename preserves their contents.
|
||||
if mv "$tmp_key" "$DEFAULT_KEY_FILE" && mv "$tmp_crt" "$DEFAULT_CRT_FILE"; then
|
||||
rm -rf "$backup_dir"
|
||||
return 0
|
||||
fi
|
||||
@@ -601,31 +354,6 @@ ensure_secure_dir_tree() {
|
||||
done
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file|tls) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
ensure_upload_storage_root() {
|
||||
local path="$1" root
|
||||
|
||||
@@ -677,10 +405,8 @@ prepare_service() {
|
||||
if [ "$isonlyoneprot" = "0" ] && [ "$https" = "1" ]; then
|
||||
if [ "$crt_file" = "$DEFAULT_CRT_FILE" ] && [ "$key_file" = "$DEFAULT_KEY_FILE" ]; then
|
||||
generate_default_cert || return 1
|
||||
fi
|
||||
|
||||
if ! cert_matches_key "$crt_file" "$key_file"; then
|
||||
logger -t gecoosac "HTTPS certificate and key do not match or cannot be read"
|
||||
elif ! cert_pair_valid "$crt_file" "$key_file"; then
|
||||
logger -t gecoosac "HTTPS certificate is not currently valid, or its private key cannot be read or does not match"
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
#!/bin/sh
|
||||
|
||||
. /usr/share/gecoosac/common.sh
|
||||
|
||||
changed=0
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
LEGACY_UPLOAD_DIR=/etc/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
OLD_CRT_FILE=/etc/gecoosac/tls/1.crt
|
||||
OLD_KEY_FILE=/etc/gecoosac/tls/1.key
|
||||
CONFIG_COMPAT=2
|
||||
@@ -26,192 +23,6 @@ set_default() {
|
||||
changed=1
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
path_has_mount() {
|
||||
local root line mount_path
|
||||
|
||||
root="$(normalize_path "$1")" || return 2
|
||||
[ -r /proc/self/mountinfo ] || return 2
|
||||
|
||||
while IFS= read -r line; do
|
||||
mount_path="$(printf '%s\n' "$line" | cut -d ' ' -f 5)" || return 2
|
||||
[ -n "$mount_path" ] || return 2
|
||||
mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2
|
||||
mount_path="$(normalize_path "$mount_path")" || return 2
|
||||
is_path_in_dir "$mount_path" "$root" && return 0
|
||||
done < /proc/self/mountinfo
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
normalize_upload_dir() {
|
||||
local upload_dir normalized
|
||||
|
||||
@@ -229,38 +40,45 @@ normalize_upload_dir() {
|
||||
}
|
||||
|
||||
configured_path_in_legacy_upload() {
|
||||
local option path resolved
|
||||
local physical="$1" option role path
|
||||
|
||||
for option in db_dir piddir crt_file key_file; do
|
||||
path="$(uci -q get "gecoosac.config.${option}")"
|
||||
[ -n "$path" ] || continue
|
||||
path="$(normalize_path "$path")" || return 0
|
||||
is_path_in_dir "$path" "$LEGACY_UPLOAD_DIR" && return 0
|
||||
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 0
|
||||
resolved="$(normalize_path "$resolved")" || return 0
|
||||
is_path_in_dir "$resolved" "$LEGACY_UPLOAD_DIR" && return 0
|
||||
fi
|
||||
case "$option" in
|
||||
db_dir) role=db ;;
|
||||
piddir) role=pid ;;
|
||||
*) role="file" ;;
|
||||
esac
|
||||
protected_path_in_dir "$path" "$role" "$physical" "$LEGACY_UPLOAD_DIR" "$physical"
|
||||
case "$?" in
|
||||
1) ;;
|
||||
*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup_legacy_upload_dir() {
|
||||
local mount_state
|
||||
local physical mount_state
|
||||
|
||||
[ -e "$LEGACY_UPLOAD_DIR" ] || [ -L "$LEGACY_UPLOAD_DIR" ] || return 0
|
||||
if [ ! -d "$LEGACY_UPLOAD_DIR" ] || [ -L "$LEGACY_UPLOAD_DIR" ]; then
|
||||
logger -t gecoosac "refusing to remove unsafe legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
if configured_path_in_legacy_upload; then
|
||||
physical="$(readlink -f "$LEGACY_UPLOAD_DIR" 2>/dev/null)" || {
|
||||
logger -t gecoosac "unable to resolve legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
}
|
||||
physical="$(normalize_path "$physical")" || return 0
|
||||
if configured_path_in_legacy_upload "$physical"; then
|
||||
logger -t gecoosac "preserving legacy upload path referenced by configuration: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
|
||||
path_has_mount "$LEGACY_UPLOAD_DIR"
|
||||
path_has_mount "$physical"
|
||||
mount_state="$?"
|
||||
case "$mount_state" in
|
||||
0)
|
||||
@@ -273,7 +91,13 @@ cleanup_legacy_upload_dir() {
|
||||
;;
|
||||
esac
|
||||
|
||||
rm -rf "$LEGACY_UPLOAD_DIR" || {
|
||||
# Apply the same ownership and ancestor-symlink policy as RPC cleanup.
|
||||
# Relocated legacy directories are preserved for manual review.
|
||||
if ! is_secure_upload_dir "$LEGACY_UPLOAD_DIR" || [ "$physical" != "$LEGACY_UPLOAD_DIR" ]; then
|
||||
logger -t gecoosac "refusing to remove unsafe legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 0
|
||||
fi
|
||||
rm -rf "$physical" || {
|
||||
logger -t gecoosac "failed to remove legacy upload path: $LEGACY_UPLOAD_DIR"
|
||||
return 1
|
||||
}
|
||||
@@ -310,7 +134,7 @@ is_managed_cert_path() {
|
||||
|
||||
is_regular_file_or_absent() {
|
||||
[ ! -e "$1" ] && [ ! -L "$1" ] && return 0
|
||||
[ -f "$1" ] && [ ! -L "$1" ]
|
||||
[ -f "$1" ] && [ ! -L "$1" ] && is_secure_upload_dir "${1%/*}"
|
||||
}
|
||||
|
||||
migrate_default_certificates() {
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
# Shared path policy for the service, configuration migration and LuCI RPC.
|
||||
|
||||
DEFAULT_DB_DIR=/etc/gecoosac
|
||||
DEFAULT_UPLOAD_DIR=/tmp/gecoosac/upload
|
||||
DEFAULT_CRT_FILE=/etc/gecoosac/tls/gecoosac.crt
|
||||
DEFAULT_KEY_FILE=/etc/gecoosac/tls/gecoosac.key
|
||||
DEFAULT_PID_DIR=/var/run
|
||||
|
||||
is_abs_path() {
|
||||
case "$1" in
|
||||
/*) return 0 ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
normalize_path() {
|
||||
local path="$1"
|
||||
local part normalized parent
|
||||
|
||||
is_abs_path "$path" || return 1
|
||||
normalized="/"
|
||||
path="${path#/}"
|
||||
|
||||
while [ -n "$path" ]; do
|
||||
part="${path%%/*}"
|
||||
if [ "$part" = "$path" ]; then
|
||||
path=""
|
||||
else
|
||||
path="${path#*/}"
|
||||
fi
|
||||
|
||||
case "$part" in
|
||||
""|.) ;;
|
||||
..)
|
||||
if [ "$normalized" != "/" ]; then
|
||||
parent="${normalized%/*}"
|
||||
[ -n "$parent" ] || parent="/"
|
||||
normalized="$parent"
|
||||
fi
|
||||
;;
|
||||
*) normalized="${normalized%/}/$part" ;;
|
||||
esac
|
||||
done
|
||||
|
||||
printf '%s\n' "$normalized"
|
||||
}
|
||||
|
||||
managed_dir_path() {
|
||||
local role="$1" path anchor
|
||||
|
||||
case "$role" in
|
||||
upload|db|pid|file|tls) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
path="$(normalize_path "$2")" || return 1
|
||||
case "$path" in
|
||||
/var/run|/var/run/*)
|
||||
anchor="$(readlink -f /var/run 2>/dev/null)" || return 1
|
||||
[ "$anchor" = "/tmp/run" ] || return 1
|
||||
printf '%s%s\n' "$anchor" "${path#/var/run}"
|
||||
;;
|
||||
/var|/var/*)
|
||||
anchor="$(readlink -f /var 2>/dev/null)" || return 1
|
||||
case "$anchor" in
|
||||
/var|/tmp) printf '%s%s\n' "$anchor" "${path#/var}" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
;;
|
||||
*) printf '%s\n' "$path" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
path_has_clear_stage_component() {
|
||||
local path="$1" part rest
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
rest="${path#/}"
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
case "$part" in
|
||||
.gecoosac-clear.*) return 0 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
path_uses_clear_stage() {
|
||||
local path="$1" resolved
|
||||
|
||||
path_has_clear_stage_component "$path" && return 0
|
||||
if [ -e "$path" ] || [ -L "$path" ]; then
|
||||
resolved="$(readlink -f "$path" 2>/dev/null)" || return 1
|
||||
path_has_clear_stage_component "$resolved" && return 0
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
is_supported_upload_path() {
|
||||
local path="$1" storage
|
||||
|
||||
path="$(normalize_path "$path")" || return 1
|
||||
path_uses_clear_stage "$path" && return 1
|
||||
[ "$path" = "$DEFAULT_UPLOAD_DIR" ] && return 0
|
||||
|
||||
case "$path" in
|
||||
/mnt/*/gecoosac/upload)
|
||||
storage="${path#/mnt/}"
|
||||
storage="${storage%/gecoosac/upload}"
|
||||
[ -n "$storage" ] && [ "${storage#*/}" = "$storage" ]
|
||||
;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
is_safe_upload_dir() {
|
||||
local path physical
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
physical="$(managed_dir_path upload "$path")" || return 1
|
||||
is_supported_upload_path "$path" || return 1
|
||||
is_supported_upload_path "$physical"
|
||||
}
|
||||
|
||||
is_path_in_dir() {
|
||||
local path root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
root="$(normalize_path "$2")" || return 1
|
||||
|
||||
[ "$root" != "/" ] || return 1
|
||||
[ "$path" = "$root" ] && return 0
|
||||
[ "${path#"$root"/}" != "$path" ]
|
||||
}
|
||||
|
||||
is_secure_dir() {
|
||||
local allow_sticky="$2" owner permissions metadata
|
||||
|
||||
[ -d "$1" ] && [ ! -L "$1" ] || return 1
|
||||
metadata="$(ls -ldn "$1" 2>/dev/null)" || return 1
|
||||
set -- $metadata
|
||||
permissions="$1"
|
||||
owner="$3"
|
||||
[ "$owner" = "0" ] || return 1
|
||||
case "$permissions" in
|
||||
d?????????) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
|
||||
if [ "$(printf '%s' "$permissions" | cut -c6)" = "w" ] || \
|
||||
[ "$(printf '%s' "$permissions" | cut -c9)" = "w" ]; then
|
||||
[ "$allow_sticky" = "1" ] && [ "$(printf '%s' "$permissions" | cut -c10)" = "t" ] || return 1
|
||||
fi
|
||||
}
|
||||
|
||||
is_secure_upload_dir() {
|
||||
local path current part rest
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
[ -d "$path" ] && [ ! -L "$path" ] || return 1
|
||||
current="/"
|
||||
rest="${path#/}"
|
||||
|
||||
while [ -n "$rest" ]; do
|
||||
part="${rest%%/*}"
|
||||
if [ "$part" = "$rest" ]; then
|
||||
rest=""
|
||||
else
|
||||
rest="${rest#*/}"
|
||||
fi
|
||||
current="${current%/}/$part"
|
||||
case "$current" in
|
||||
/tmp) is_secure_dir "$current" 1 || return 1 ;;
|
||||
*) is_secure_dir "$current" || return 1 ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
is_safe_db_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/etc/gecoosac|/etc/gecoosac/*|/tmp/gecoosac|/tmp/gecoosac/*|/var/lib/gecoosac|/var/lib/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path db "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
is_safe_pid_dir() {
|
||||
local path upload_root physical physical_upload_root
|
||||
|
||||
path="$(normalize_path "$1")" || return 1
|
||||
upload_root="$(normalize_path "${2:-$DEFAULT_UPLOAD_DIR}")" || return 1
|
||||
|
||||
case "$path" in
|
||||
/var/run|/var/run/*|/tmp/gecoosac|/tmp/gecoosac/*) ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
is_path_in_dir "$path" "$upload_root" && return 1
|
||||
physical="$(managed_dir_path pid "$path")" || return 1
|
||||
physical_upload_root="$(managed_dir_path upload "$upload_root")" || return 1
|
||||
is_path_in_dir "$physical" "$physical_upload_root" && return 1
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
path_has_mount() {
|
||||
local root mount_id parent_id device mount_root mount_path rest
|
||||
|
||||
root="$(normalize_path "$1")" || return 2
|
||||
[ -r /proc/self/mountinfo ] || return 2
|
||||
|
||||
while IFS=' ' read -r mount_id parent_id device mount_root mount_path rest; do
|
||||
[ -n "$mount_path" ] && [ -n "$rest" ] || return 2
|
||||
mount_path="$(printf '%b\n' "$mount_path" 2>/dev/null)" || return 2
|
||||
mount_path="$(normalize_path "$mount_path")" || return 2
|
||||
is_path_in_dir "$mount_path" "$root" && return 0
|
||||
done < /proc/self/mountinfo
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
# The checked root must be a resolved, validated directory. Return 0 for a
|
||||
# protected path, 1 for an unrelated path, or 2 when validation is impossible.
|
||||
# Keep the original spelling when resolving symlinks: link/../file and its
|
||||
# lexical normalization can refer to different files.
|
||||
protected_path_in_dir() {
|
||||
local raw_path="$1" role="$2" checked_root="$3"
|
||||
local live_logical="${4:-$3}" live_physical="${5:-$3}"
|
||||
local path="$1" physical real_path suffix mapped
|
||||
|
||||
[ -n "$path" ] || return 1
|
||||
|
||||
path="$(normalize_path "$path")" || return 2
|
||||
physical="$(managed_dir_path "$role" "$path")" || return 2
|
||||
checked_root="$(normalize_path "$checked_root")" || return 2
|
||||
live_logical="$(normalize_path "$live_logical")" || return 2
|
||||
live_physical="$(normalize_path "$live_physical")" || return 2
|
||||
|
||||
is_path_in_dir "$path" "$checked_root" && return 0
|
||||
is_path_in_dir "$physical" "$checked_root" && return 0
|
||||
real_path="$(readlink -f "$raw_path" 2>/dev/null)" || return 2
|
||||
[ -n "$real_path" ] || return 2
|
||||
real_path="$(normalize_path "$real_path")" || return 2
|
||||
is_path_in_dir "$real_path" "$checked_root" && return 0
|
||||
|
||||
[ "$checked_root" != "$live_physical" ] || {
|
||||
is_path_in_dir "$path" "$live_logical" && return 0
|
||||
is_path_in_dir "$physical" "$live_physical" && return 0
|
||||
if [ -n "$real_path" ] && is_path_in_dir "$real_path" "$live_physical"; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
suffix=
|
||||
if is_path_in_dir "$path" "$live_logical"; then
|
||||
suffix="${path#"$live_logical"}"
|
||||
elif is_path_in_dir "$physical" "$live_physical"; then
|
||||
suffix="${physical#"$live_physical"}"
|
||||
elif [ -n "$real_path" ] && is_path_in_dir "$real_path" "$live_physical"; then
|
||||
suffix="${real_path#"$live_physical"}"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
mapped="${checked_root%/}${suffix}"
|
||||
[ -e "$mapped" ] || [ -L "$mapped" ]
|
||||
}
|
||||
+1
-4
@@ -9,7 +9,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=glorytun
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=4
|
||||
PKG_SOURCE_PROTO:=git
|
||||
PKG_SOURCE_VERSION:=95fa5ead2154adb546bcdb454c56c876e9f60bb5
|
||||
PKG_SOURCE:=glorytun-$(PKG_VERSION).tar.gz
|
||||
@@ -50,13 +50,10 @@ endef
|
||||
define Package/$(PKG_NAME)/install
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/glorytun $(1)/usr/sbin/$(PKG_NAME)
|
||||
$(INSTALL_BIN) files/glorytun-irq-affinity.sh $(1)/usr/sbin/glorytun-irq-affinity.sh
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) init $(1)/etc/init.d/$(PKG_NAME)
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_DATA) glorytun.config $(1)/etc/config/glorytun
|
||||
$(INSTALL_DIR) $(1)/etc/glorytun
|
||||
$(INSTALL_DATA) files/glorytun-sysctl.conf $(1)/etc/glorytun/glorytun-sysctl.conf
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Set IRQ affinity for glorytun interfaces
|
||||
set_irq_affinity() {
|
||||
local irq=$1
|
||||
local cpu=$2
|
||||
local mask=$(printf "%x" $((1 << $cpu)))
|
||||
|
||||
echo "Setting IRQ $irq to CPU $cpu (mask: $mask)"
|
||||
echo $mask > /proc/irq/$irq/smp_affinity
|
||||
}
|
||||
|
||||
# Get interface IRQs
|
||||
get_interface_irqs() {
|
||||
local interface=$1
|
||||
grep -E "$interface" /proc/interrupts | awk '{print $1}' | tr -d ':'
|
||||
}
|
||||
|
||||
# Main function
|
||||
setup_irq_affinity() {
|
||||
# Get available CPU cores
|
||||
local cpu_count=$(grep -c processor /proc/cpuinfo)
|
||||
echo "Available CPU cores: $cpu_count"
|
||||
|
||||
# Determine tun interface
|
||||
local tun_interface=$(ip link | grep tun | awk -F': ' '{print $2}' | head -n1)
|
||||
if [ -z "$tun_interface" ]; then
|
||||
echo "No tun interface found"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Found tun interface: $tun_interface"
|
||||
|
||||
# Get IRQs for tun interface
|
||||
local irqs=$(get_interface_irqs $tun_interface)
|
||||
if [ -z "$irqs" ]; then
|
||||
echo "No IRQs found for $tun_interface"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Distribute IRQs across available cores (except CPU0)
|
||||
local cpu=1
|
||||
for irq in $irqs; do
|
||||
set_irq_affinity $irq $cpu
|
||||
cpu=$(( (cpu + 1) % cpu_count ))
|
||||
if [ $cpu -eq 0 ]; then
|
||||
cpu=1
|
||||
fi
|
||||
done
|
||||
|
||||
# Set RPS (Receive Packet Steering) for better packet distribution
|
||||
if [ -e /sys/class/net/$tun_interface/queues/rx-0/rps_cpus ]; then
|
||||
# Calculate mask for all CPUs except CPU0
|
||||
local rps_mask=$(printf "%x" $(( (1 << $cpu_count) - 2 )))
|
||||
echo "Setting RPS mask to $rps_mask for $tun_interface"
|
||||
echo $rps_mask > /sys/class/net/$tun_interface/queues/rx-0/rps_cpus
|
||||
fi
|
||||
|
||||
# Set RFS (Receive Flow Steering) limits if available
|
||||
if [ -e /proc/sys/net/core/rps_sock_flow_entries ]; then
|
||||
echo 32768 > /proc/sys/net/core/rps_sock_flow_entries
|
||||
if [ -e /sys/class/net/$tun_interface/queues/rx-0/rps_flow_cnt ]; then
|
||||
echo 32768 > /sys/class/net/$tun_interface/queues/rx-0/rps_flow_cnt
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set XPS (Transmit Packet Steering) if available
|
||||
if [ -e /sys/class/net/$tun_interface/queues/tx-0/xps_cpus ]; then
|
||||
# Use all CPUs except CPU0 for transmit
|
||||
local xps_mask=$(printf "%x" $(( (1 << $cpu_count) - 2 )))
|
||||
echo "Setting XPS mask to $xps_mask for $tun_interface"
|
||||
echo $xps_mask > /sys/class/net/$tun_interface/queues/tx-0/xps_cpus
|
||||
fi
|
||||
|
||||
# Increase network queue length for better performance
|
||||
if [ -e /sys/class/net/$tun_interface/tx_queue_len ]; then
|
||||
echo 1000 > /sys/class/net/$tun_interface/tx_queue_len
|
||||
fi
|
||||
|
||||
echo "IRQ affinity setup completed for $tun_interface"
|
||||
}
|
||||
|
||||
# Run the main function
|
||||
setup_irq_affinity
|
||||
|
||||
exit 0
|
||||
@@ -1,27 +0,0 @@
|
||||
# Glorytun-specific optimizations
|
||||
# Note: These settings complement the system-wide sysctl parameters
|
||||
|
||||
# Buffer size optimization for glorytun tunnels
|
||||
# Increase only if not already set in system configuration
|
||||
net.core.rmem_max = 16777216
|
||||
net.core.wmem_max = 16777216
|
||||
net.core.optmem_max = 16777216
|
||||
|
||||
# Enable BBR congestion control for better tunnel performance
|
||||
net.core.default_qdisc = fq
|
||||
|
||||
# RPS/RFS settings for tunnel interfaces
|
||||
# These are critical for multicore performance with glorytun
|
||||
net.core.rps_sock_flow_entries = 32768
|
||||
|
||||
# Network device tuning for tunnel interfaces
|
||||
net.core.netdev_max_backlog = 10000
|
||||
net.core.netdev_budget = 600
|
||||
net.core.netdev_budget_usecs = 10000
|
||||
|
||||
# Lower TCP timeout for faster recovery of tunnel connections
|
||||
net.ipv4.tcp_keepalive_intvl = 10
|
||||
net.ipv4.tcp_keepalive_probes = 6
|
||||
|
||||
# Disable slow start after idle for more consistent tunnel performance
|
||||
net.ipv4.tcp_slow_start_after_idle = 0
|
||||
@@ -11,6 +11,5 @@ config glorytun 'vpn'
|
||||
option localip '10.255.255.2'
|
||||
option remoteip '10.255.255.1'
|
||||
option multiqueue '1'
|
||||
option cpu_affinity '1'
|
||||
option label 'Default VPN'
|
||||
option timeout '10000'
|
||||
+17
-67
@@ -18,14 +18,6 @@ _err() {
|
||||
logger -p daemon.err -t ${PROG_NAME} "$@"
|
||||
}
|
||||
|
||||
# Apply system optimizations
|
||||
apply_sysctl_optimizations() {
|
||||
if [ -f /etc/glorytun/glorytun-sysctl.conf ]; then
|
||||
_log "Applying system optimizations"
|
||||
sysctl -q -p /etc/glorytun/glorytun-sysctl.conf
|
||||
fi
|
||||
}
|
||||
|
||||
validate_section() {
|
||||
uci_validate_section glorytun glorytun "${1}" \
|
||||
'enable:bool:0' \
|
||||
@@ -38,50 +30,13 @@ validate_section() {
|
||||
'timeout:uinteger:10000' \
|
||||
'chacha20:bool:0' \
|
||||
'multiqueue:bool:1' \
|
||||
'cpu_affinity:bool:1' \
|
||||
'proto:string' \
|
||||
'localip:string' \
|
||||
'remoteip:string'
|
||||
}
|
||||
|
||||
# New function for CPU core optimization
|
||||
optimize_cpu_cores() {
|
||||
local dev="$1"
|
||||
local multiqueue="$2"
|
||||
local cpu_affinity="$3"
|
||||
|
||||
if [ "$cpu_affinity" = "1" ]; then
|
||||
_log "Setting up CPU affinity for $dev"
|
||||
if [ -x /usr/sbin/glorytun-irq-affinity.sh ]; then
|
||||
/usr/sbin/glorytun-irq-affinity.sh
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "$multiqueue" = "1" ]; then
|
||||
_log "Enabling multiqueue for $dev"
|
||||
|
||||
# Get CPU count
|
||||
local cpu_count=$(grep -c processor /proc/cpuinfo)
|
||||
if [ "$cpu_count" -gt 1 ]; then
|
||||
# Use half of available CPUs but at least 2
|
||||
local queue_count=$(( cpu_count / 2 ))
|
||||
[ "$queue_count" -lt 2 ] && queue_count=2
|
||||
|
||||
# Set multiqueue parameters
|
||||
ip link set dev "$dev" multiqueue on
|
||||
|
||||
# Increase buffer sizes for better performance
|
||||
if [ -e /proc/sys/net/core/rmem_max ]; then
|
||||
echo 16777216 > /proc/sys/net/core/rmem_max
|
||||
echo 16777216 > /proc/sys/net/core/wmem_max
|
||||
echo 16777216 > /proc/sys/net/core/optmem_max
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
start_instance() {
|
||||
local enable key host port dev mptcp proto chacha20 mode multiqueue cpu_affinity timeout localip remoteip
|
||||
local enable key host port dev mptcp proto chacha20 mode multiqueue timeout localip remoteip
|
||||
local key_dir="/var/run/${PROG_NAME}"
|
||||
local key_file="${key_dir}/${1}.key"
|
||||
|
||||
@@ -91,9 +46,6 @@ start_instance() {
|
||||
}
|
||||
|
||||
[ "${enable}" = "1" ] || return 1
|
||||
|
||||
apply_sysctl_optimizations
|
||||
|
||||
[ "${proto}" = "tcp" ] || return 1
|
||||
[ -n "${key}" ] || {
|
||||
_err "Key empty"
|
||||
@@ -147,25 +99,23 @@ start_instance() {
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
|
||||
# Apply CPU optimizations after the instance is started
|
||||
optimize_cpu_cores "$dev" "$multiqueue" "$cpu_affinity"
|
||||
|
||||
# Configure interface IP addresses
|
||||
|
||||
# Configure the tunnel addresses once glorytun has created the device.
|
||||
# RPS/XPS steering for it is set by /etc/hotplug.d/net/20-rps-xps
|
||||
# (openmptcprouter package), which handles every interface.
|
||||
if [ -n "$localip" ] && [ -n "$remoteip" ]; then
|
||||
(
|
||||
# Wait for device to be created
|
||||
local count=0
|
||||
while [ $count -lt 10 ]; do
|
||||
if ip link show "$dev" >/dev/null 2>&1; then
|
||||
ifconfig "$dev" "$localip" pointopoint "$remoteip" up && \
|
||||
_log "Configured $dev with IP $localip peer $remoteip"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
count=$((count + 1))
|
||||
done
|
||||
) &
|
||||
(
|
||||
count=0
|
||||
while [ $count -lt 10 ]; do
|
||||
if [ -d "/sys/class/net/$dev" ]; then
|
||||
ifconfig "$dev" "$localip" pointopoint "$remoteip" up && \
|
||||
_log "Configured $dev with IP $localip peer $remoteip"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
count=$((count + 1))
|
||||
done
|
||||
) &
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ PKG_ARCH_ISTOREENHANCE:=$(ARCH)
|
||||
|
||||
PKG_NAME:=istoreenhance
|
||||
# use PKG_SOURCE_DATE instead of PKG_VERSION for compitable
|
||||
PKG_SOURCE_DATE:=0.7.17
|
||||
PKG_RELEASE:=7
|
||||
PKG_SOURCE_DATE:=0.8.0
|
||||
PKG_RELEASE:=8
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
ARCH_HEXCODE=8664
|
||||
|
||||
+2
-2
@@ -9,8 +9,8 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_ARCH_kaiplus:=$(ARCH)
|
||||
|
||||
PKG_NAME:=kaiplus
|
||||
PKG_VERSION:=linkeasefull-runtime-v3.0.17
|
||||
PKG_RELEASE:=9
|
||||
PKG_VERSION:=1.0.9
|
||||
PKG_RELEASE:=13
|
||||
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
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
},
|
||||
"standalone": {
|
||||
"entry": "index.html",
|
||||
"basePath": "/apps/kaiplus/"
|
||||
"basePath": "/apps/kaiplus/",
|
||||
"url": "/apps/kaiplus/"
|
||||
},
|
||||
"config": {
|
||||
"providerOrder": ["uci"],
|
||||
|
||||
@@ -8,3 +8,5 @@ config kaiplus
|
||||
option 'bind_addr' '0.0.0.0'
|
||||
option 'base_path' '/apps/kaiplus/'
|
||||
option 'system_role' 'istoreos'
|
||||
option 'release_channel' 'stable'
|
||||
option 'auth_mode' 'disabled'
|
||||
|
||||
@@ -13,6 +13,8 @@ get_config() {
|
||||
config_get bind_addr "$1" bind_addr "0.0.0.0"
|
||||
config_get base_path "$1" base_path "/apps/kaiplus/"
|
||||
config_get system_role "$1" system_role "istoreos"
|
||||
config_get release_channel "$1" release_channel "stable"
|
||||
config_get auth_mode "$1" auth_mode "disabled"
|
||||
}
|
||||
|
||||
mkdir_p() {
|
||||
@@ -32,10 +34,12 @@ start_service() {
|
||||
fi
|
||||
|
||||
mkdir_p "$data_dir"
|
||||
umask 077
|
||||
mkdir_p "$data_dir/workspace"
|
||||
mkdir_p "$data_dir/cache"
|
||||
mkdir_p "$data_dir/config"
|
||||
mkdir_p "$data_dir/state"
|
||||
chmod 0700 "$data_dir/config" "$data_dir/state"
|
||||
|
||||
helper_token_file="$data_dir/state/ssh-helper.token"
|
||||
if [ ! -s "$helper_token_file" ]; then
|
||||
@@ -58,6 +62,9 @@ start_service() {
|
||||
procd_set_param env \
|
||||
KAIPLUS_HOME="$data_dir" \
|
||||
KAIPLUS_SYSTEM_ROLE="$system_role" \
|
||||
KAIPLUS_RELEASE_CHANNEL="$release_channel" \
|
||||
KAIPLUS_AUTH_MODE="$auth_mode" \
|
||||
LINKEASE_AUTH_PROVIDER="$auth_mode" \
|
||||
KAIPLUS_LISTEN_MODE="tcp" \
|
||||
KAIPLUS_WORKSPACE_HELPER_DIR="/usr/share/kaiplus/helpers" \
|
||||
KAIPLUS_WORKSPACE_TOOL_INSTALL_DIR="/tmp/kaiplus_workspace_tool" \
|
||||
@@ -68,6 +75,9 @@ start_service() {
|
||||
procd_set_param env \
|
||||
KAIPLUS_HOME="$data_dir" \
|
||||
KAIPLUS_SYSTEM_ROLE="$system_role" \
|
||||
KAIPLUS_RELEASE_CHANNEL="$release_channel" \
|
||||
KAIPLUS_AUTH_MODE="$auth_mode" \
|
||||
LINKEASE_AUTH_PROVIDER="$auth_mode" \
|
||||
KAIPLUS_LISTEN_MODE="unix" \
|
||||
KAIPLUS_SOCKET_PATH="$socket_path" \
|
||||
KAIPLUS_WORKSPACE_HELPER_DIR="/usr/share/kaiplus/helpers" \
|
||||
@@ -88,6 +98,7 @@ start_service() {
|
||||
procd_append_param command --defaults-dir /usr/share/kaiplus/defaults
|
||||
procd_append_param command --base-path "$base_path"
|
||||
procd_append_param command --system-role "$system_role"
|
||||
procd_append_param command --release-channel "$release_channel"
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_set_param respawn
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
#
|
||||
# Copyright (C) 2026 Dante <dante@seckv.com>
|
||||
#
|
||||
# This is free software, licensed under the Apache License, Version 2.0 .
|
||||
#
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=linkback
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Dante
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
|
||||
define Package/$(PKG_NAME)
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Routing and Redirection
|
||||
TITLE:=LinkBack gateway switch daemon
|
||||
DEPENDS:=+libuci +libubus +libubox +libblobmsg-json +ip-full
|
||||
MAINTAINER:=Dante
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/description
|
||||
LinkBack: A highly efficient and lightweight C-based Multi-WAN link health monitoring
|
||||
and dynamic route metric failover/failback daemon. Supports Ping, DNS, and TCP probes.
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/conffiles
|
||||
/etc/config/linkback
|
||||
endef
|
||||
|
||||
define Build/Prepare
|
||||
mkdir -p $(PKG_BUILD_DIR)
|
||||
$(CP) ./src/* $(PKG_BUILD_DIR)/
|
||||
endef
|
||||
|
||||
define Build/Compile
|
||||
$(MAKE) -C $(PKG_BUILD_DIR) \
|
||||
CC="$(TARGET_CC)" \
|
||||
CFLAGS="$(TARGET_CFLAGS)" \
|
||||
LDFLAGS="$(TARGET_LDFLAGS)"
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/install
|
||||
# Install C daemon binary
|
||||
$(INSTALL_DIR) $(1)/usr/sbin
|
||||
$(INSTALL_BIN) $(PKG_BUILD_DIR)/linkbackd $(1)/usr/sbin/linkbackd
|
||||
|
||||
# Install config and init files
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) ./files/linkback.config $(1)/etc/config/linkback
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) ./files/linkback.init $(1)/etc/init.d/linkback
|
||||
endef
|
||||
|
||||
define Package/$(PKG_NAME)/postinst
|
||||
#!/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
/etc/init.d/linkback enable
|
||||
/etc/init.d/linkback restart
|
||||
}
|
||||
exit 0
|
||||
endef
|
||||
|
||||
$(eval $(call BuildPackage,$(PKG_NAME)))
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
config global
|
||||
option enabled '0'
|
||||
option check_interval '5'
|
||||
option check_timeout '3'
|
||||
option recovery_delay '3'
|
||||
option failover_delay '2'
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
|
||||
START=99
|
||||
USE_PROCD=1
|
||||
|
||||
_get_global_enabled() {
|
||||
config_get_bool _GLOBAL_ENABLED "$1" "enabled" 0
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "linkback"
|
||||
|
||||
local _GLOBAL_ENABLED=0
|
||||
config_foreach _get_global_enabled "global"
|
||||
[ "$_GLOBAL_ENABLED" -eq 1 ] || return 0
|
||||
|
||||
procd_open_instance "main"
|
||||
procd_set_param command /usr/sbin/linkbackd
|
||||
procd_set_param respawn
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
stop
|
||||
start
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "linkback"
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
TARGET := linkbackd
|
||||
OBJS := linkbackd.o
|
||||
|
||||
CFLAGS += -Wall -O2
|
||||
LIBS := -luci
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
$(TARGET): $(OBJS)
|
||||
$(CC) $(CFLAGS) $(OBJS) -o $(TARGET) $(LDFLAGS) $(LIBS)
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(CFLAGS) -c $< -o $@
|
||||
|
||||
clean:
|
||||
rm -f *.o $(TARGET)
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -0,0 +1,849 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <signal.h>
|
||||
#include <syslog.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <sys/types.h>
|
||||
#include <sys/select.h>
|
||||
#include <netinet/in.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <fcntl.h>
|
||||
#include <errno.h>
|
||||
#include <uci.h>
|
||||
#include "linkbackd.h"
|
||||
|
||||
// DNS Header struct for raw check
|
||||
struct dns_header {
|
||||
unsigned short id;
|
||||
unsigned short flags;
|
||||
unsigned short qdcount;
|
||||
unsigned short ancount;
|
||||
unsigned short nscount;
|
||||
unsigned short arcount;
|
||||
};
|
||||
|
||||
// Global daemon state
|
||||
static global_config_t global_cfg;
|
||||
static link_t links[MAX_LINKS];
|
||||
static int link_count = 0;
|
||||
static volatile bool keep_running = true;
|
||||
|
||||
// Prototypes
|
||||
static void restore_all_metrics(void);
|
||||
static bool validate_loaded_config(void);
|
||||
static void handle_signal(int sig) {
|
||||
syslog(LOG_INFO, "Received signal %d, exiting...", sig);
|
||||
keep_running = false;
|
||||
}
|
||||
|
||||
// Ubus popen JSON parser
|
||||
static bool get_interface_ubus_status(const char *ifname, char *device, int dev_len, char *gateway, int gw_len, bool *is_up) {
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof(cmd), "ubus call network.interface.%s status 2>/dev/null", ifname);
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if (!fp) return false;
|
||||
|
||||
char buf[4096] = {0};
|
||||
int bytes_read = fread(buf, 1, sizeof(buf) - 1, fp);
|
||||
pclose(fp);
|
||||
|
||||
if (bytes_read <= 0) return false;
|
||||
|
||||
// Check if interface is up
|
||||
*is_up = false;
|
||||
char *p_up = strstr(buf, "\"up\":");
|
||||
if (p_up) {
|
||||
char *p_true = strstr(p_up, "true");
|
||||
// Ensure 'true' belongs to this property and isn't far away
|
||||
if (p_true && p_true - p_up < 10) {
|
||||
*is_up = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Extract physical device
|
||||
device[0] = '\0';
|
||||
char *p_dev = strstr(buf, "\"l3_device\":");
|
||||
int dev_key_len = 12;
|
||||
if (!p_dev) {
|
||||
p_dev = strstr(buf, "\"device\":");
|
||||
dev_key_len = 9;
|
||||
}
|
||||
if (p_dev) {
|
||||
char *p_start = strchr(p_dev + dev_key_len, '"');
|
||||
if (p_start) {
|
||||
char *p_end = strchr(p_start + 1, '"');
|
||||
if (p_end) {
|
||||
int len = p_end - p_start - 1;
|
||||
if (len >= dev_len) len = dev_len - 1;
|
||||
strncpy(device, p_start + 1, len);
|
||||
device[len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extract gateway/nexthop from route list
|
||||
gateway[0] = '\0';
|
||||
char *p_route = strstr(buf, "\"route\":");
|
||||
if (p_route) {
|
||||
char *p_nexthop = strstr(p_route, "\"nexthop\":");
|
||||
if (p_nexthop) {
|
||||
char *p_start = strchr(p_nexthop + 10, '"');
|
||||
if (p_start) {
|
||||
char *p_end = strchr(p_start + 1, '"');
|
||||
if (p_end) {
|
||||
int len = p_end - p_start - 1;
|
||||
if (len >= gw_len) len = gw_len - 1;
|
||||
strncpy(gateway, p_start + 1, len);
|
||||
gateway[len] = '\0';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Ping check
|
||||
static bool run_ping_check(const char *device, const char *target, int timeout, int *rtt_ms) {
|
||||
if (device[0] == '\0' || target[0] == '\0') return false;
|
||||
|
||||
char cmd[256];
|
||||
snprintf(cmd, sizeof(cmd), "ping -I %s -c 1 -W %d %s 2>/dev/null", device, timeout, target);
|
||||
FILE *fp = popen(cmd, "r");
|
||||
if (!fp) return false;
|
||||
|
||||
char line[128];
|
||||
bool ok = false;
|
||||
*rtt_ms = -1;
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
char *p = strstr(line, "time=");
|
||||
if (p) {
|
||||
ok = true;
|
||||
double rtt = atof(p + 5);
|
||||
*rtt_ms = (int)rtt;
|
||||
}
|
||||
}
|
||||
int status = pclose(fp);
|
||||
return ok && (WIFEXITED(status) && WEXITSTATUS(status) == 0);
|
||||
}
|
||||
|
||||
// DNS check
|
||||
static bool run_dns_check(const char *device, const char *dns_server, const char *domain, int timeout, int *rtt_ms) {
|
||||
if (device[0] == '\0' || dns_server[0] == '\0' || domain[0] == '\0') return false;
|
||||
|
||||
struct timeval start, end;
|
||||
gettimeofday(&start, NULL);
|
||||
|
||||
int sockfd = socket(AF_INET, SOCK_DGRAM, 0);
|
||||
if (sockfd < 0) return false;
|
||||
|
||||
// Set non-blocking
|
||||
int flags = fcntl(sockfd, F_GETFL, 0);
|
||||
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Bind to device
|
||||
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device)) < 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in servaddr;
|
||||
memset(&servaddr, 0, sizeof(servaddr));
|
||||
servaddr.sin_family = AF_INET;
|
||||
servaddr.sin_port = htons(53);
|
||||
if (inet_pton(AF_INET, dns_server, &servaddr.sin_addr) <= 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Format DNS packet
|
||||
unsigned char packet[512];
|
||||
memset(packet, 0, sizeof(packet));
|
||||
|
||||
struct dns_header *dns = (struct dns_header *)packet;
|
||||
dns->id = (unsigned short)htons(getpid());
|
||||
dns->flags = htons(0x0100);
|
||||
dns->qdcount = htons(1);
|
||||
|
||||
unsigned char *qname = packet + sizeof(struct dns_header);
|
||||
const char *src = domain;
|
||||
unsigned char *dst = qname;
|
||||
while (*src) {
|
||||
const char *next = strchr(src, '.');
|
||||
int len = next ? (next - src) : strlen(src);
|
||||
*dst++ = len;
|
||||
memcpy(dst, src, len);
|
||||
dst += len;
|
||||
src = next ? (next + 1) : (src + len);
|
||||
}
|
||||
*dst++ = 0;
|
||||
|
||||
unsigned short *qtype = (unsigned short *)dst;
|
||||
*qtype = htons(1); // A Record
|
||||
dst += 2;
|
||||
unsigned short *qclass = (unsigned short *)dst;
|
||||
*qclass = htons(1); // IN
|
||||
dst += 2;
|
||||
|
||||
int packet_len = dst - packet;
|
||||
|
||||
if (sendto(sockfd, packet, packet_len, 0, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
|
||||
if (errno != EAGAIN && errno != EWOULDBLOCK) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
fd_set readfds;
|
||||
FD_ZERO(&readfds);
|
||||
FD_SET(sockfd, &readfds);
|
||||
struct timeval tv;
|
||||
tv.tv_sec = timeout;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
int sel = select(sockfd + 1, &readfds, NULL, NULL, &tv);
|
||||
if (sel <= 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
unsigned char response[512];
|
||||
struct sockaddr_in from;
|
||||
socklen_t from_len = sizeof(from);
|
||||
int resp_len = recvfrom(sockfd, response, sizeof(response), 0, (struct sockaddr *)&from, &from_len);
|
||||
close(sockfd);
|
||||
|
||||
if (resp_len < 12) return false;
|
||||
|
||||
struct dns_header *resp_dns = (struct dns_header *)response;
|
||||
if (ntohs(resp_dns->id) != getpid()) return false;
|
||||
|
||||
gettimeofday(&end, NULL);
|
||||
*rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
// TCP check
|
||||
static bool run_tcp_check(const char *device, const char *tcp_target, int port, int timeout, int *rtt_ms) {
|
||||
if (device[0] == '\0' || tcp_target[0] == '\0' || port <= 0) return false;
|
||||
|
||||
struct timeval start, end;
|
||||
gettimeofday(&start, NULL);
|
||||
|
||||
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
|
||||
if (sockfd < 0) return false;
|
||||
|
||||
// Set non-blocking
|
||||
int flags = fcntl(sockfd, F_GETFL, 0);
|
||||
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
// Bind to device
|
||||
if (setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE, device, strlen(device)) < 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct sockaddr_in addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(port);
|
||||
if (inet_pton(AF_INET, tcp_target, &addr.sin_addr) <= 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
int rc = connect(sockfd, (struct sockaddr *)&addr, sizeof(addr));
|
||||
if (rc < 0) {
|
||||
if (errno != EINPROGRESS) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
gettimeofday(&end, NULL);
|
||||
*rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000);
|
||||
close(sockfd);
|
||||
return true;
|
||||
}
|
||||
|
||||
fd_set writefds;
|
||||
FD_ZERO(&writefds);
|
||||
FD_SET(sockfd, &writefds);
|
||||
struct timeval tv;
|
||||
tv.tv_sec = timeout;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
int sel = select(sockfd + 1, NULL, &writefds, NULL, &tv);
|
||||
if (sel <= 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
int optval;
|
||||
socklen_t optlen = sizeof(optval);
|
||||
if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen) < 0 || optval != 0) {
|
||||
close(sockfd);
|
||||
return false;
|
||||
}
|
||||
|
||||
gettimeofday(&end, NULL);
|
||||
*rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000);
|
||||
close(sockfd);
|
||||
return true;
|
||||
}
|
||||
|
||||
// UCI parser helper
|
||||
static bool load_config(void) {
|
||||
struct uci_context *ctx = uci_alloc_context();
|
||||
if (!ctx) return false;
|
||||
|
||||
struct uci_package *pkg = NULL;
|
||||
if (uci_load(ctx, "linkback", &pkg) != UCI_OK) {
|
||||
uci_free_context(ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Parse global config
|
||||
global_cfg.enabled = false;
|
||||
|
||||
struct uci_section *global_sec = uci_lookup_section(ctx, pkg, "global");
|
||||
if (!global_sec) {
|
||||
struct uci_element *ge;
|
||||
uci_foreach_element(&pkg->sections, ge) {
|
||||
struct uci_section *s = uci_to_section(ge);
|
||||
if (strcmp(s->type, "global") == 0) {
|
||||
global_sec = s;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (global_sec) {
|
||||
const char *enabled = uci_lookup_option_string(ctx, global_sec, "enabled");
|
||||
global_cfg.enabled = (enabled && strcmp(enabled, "1") == 0);
|
||||
}
|
||||
|
||||
// Parse links
|
||||
link_count = 0;
|
||||
struct uci_element *e;
|
||||
uci_foreach_element(&pkg->sections, e) {
|
||||
struct uci_section *s = uci_to_section(e);
|
||||
if (strcmp(s->type, "link") != 0) continue;
|
||||
|
||||
const char *enabled = uci_lookup_option_string(ctx, s, "enabled");
|
||||
if (enabled && strcmp(enabled, "0") == 0) continue;
|
||||
|
||||
link_t *link = &links[link_count];
|
||||
memset(link, 0, sizeof(link_t));
|
||||
|
||||
const char *name = uci_lookup_option_string(ctx, s, "name");
|
||||
if (!name) continue;
|
||||
strncpy(link->name, name, MAX_NAME_LEN - 1);
|
||||
|
||||
link->enabled = true;
|
||||
|
||||
const char *priority = uci_lookup_option_string(ctx, s, "priority");
|
||||
int prio_val = priority ? atoi(priority) : 1;
|
||||
link->priority = (prio_val > 0) ? prio_val : 1;
|
||||
|
||||
link->metric = link->priority * 10;
|
||||
link->current_metric = link->metric;
|
||||
|
||||
// Parse ping targets (split by comma or space)
|
||||
const char *pings = uci_lookup_option_string(ctx, s, "ping_targets");
|
||||
if (pings) {
|
||||
char tmp[256];
|
||||
strncpy(tmp, pings, sizeof(tmp) - 1);
|
||||
tmp[sizeof(tmp) - 1] = '\0';
|
||||
char *token = strtok(tmp, ", \t");
|
||||
while (token && link->ping_target_count < MAX_TARGETS) {
|
||||
// Trim leading whitespace
|
||||
while (*token == ' ' || *token == '\t' || *token == '\r' || *token == '\n') {
|
||||
token++;
|
||||
}
|
||||
// Trim trailing whitespace
|
||||
char *end = token + strlen(token) - 1;
|
||||
while (end > token && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
|
||||
*end = '\0';
|
||||
end--;
|
||||
}
|
||||
if (*token != '\0') {
|
||||
strncpy(link->ping_targets[link->ping_target_count], token, MAX_IP_LEN - 1);
|
||||
link->ping_target_count++;
|
||||
}
|
||||
token = strtok(NULL, ",");
|
||||
}
|
||||
}
|
||||
|
||||
// Parse DNS targets
|
||||
const char *dns_srv = uci_lookup_option_string(ctx, s, "dns_server");
|
||||
if (dns_srv) strncpy(link->dns_server, dns_srv, MAX_IP_LEN - 1);
|
||||
|
||||
const char *dns_dom = uci_lookup_option_string(ctx, s, "dns_domain");
|
||||
if (dns_dom) strncpy(link->dns_domain, dns_dom, MAX_DOMAIN_LEN - 1);
|
||||
|
||||
// Parse TCP targets
|
||||
const char *tcp_tgt = uci_lookup_option_string(ctx, s, "tcp_target");
|
||||
if (tcp_tgt) strncpy(link->tcp_target, tcp_tgt, MAX_IP_LEN - 1);
|
||||
|
||||
const char *tcp_p = uci_lookup_option_string(ctx, s, "tcp_port");
|
||||
if (tcp_p) link->tcp_port = atoi(tcp_p);
|
||||
|
||||
const char *interval = uci_lookup_option_string(ctx, s, "check_interval");
|
||||
link->check_interval = interval ? atoi(interval) : 5;
|
||||
|
||||
const char *timeout = uci_lookup_option_string(ctx, s, "check_timeout");
|
||||
link->check_timeout = timeout ? atoi(timeout) : 3;
|
||||
|
||||
const char *recovery = uci_lookup_option_string(ctx, s, "recovery_delay");
|
||||
link->recovery_delay = recovery ? atoi(recovery) : 3;
|
||||
|
||||
const char *failover = uci_lookup_option_string(ctx, s, "failover_delay");
|
||||
link->failover_delay = failover ? atoi(failover) : 2;
|
||||
|
||||
// Default runtime states
|
||||
link->healthy = true;
|
||||
link->is_up = false;
|
||||
link->last_checked = 0; // Force immediate check on startup
|
||||
|
||||
link_count++;
|
||||
if (link_count >= MAX_LINKS) break;
|
||||
}
|
||||
|
||||
uci_unload(ctx, pkg);
|
||||
uci_free_context(ctx);
|
||||
|
||||
if (!validate_loaded_config()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Startup-time defensive validation. LuCI-side checks can be bypassed by
|
||||
// direct UCI edits, so daemon must refuse unsafe/incomplete configs.
|
||||
static bool validate_loaded_config(void) {
|
||||
if (!global_cfg.enabled) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (link_count < 2) {
|
||||
syslog(LOG_ERR, "Invalid config: at least 2 enabled monitored links are required, got %d.", link_count);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
link_t *a = &links[i];
|
||||
|
||||
if (a->name[0] == '\0') {
|
||||
syslog(LOG_ERR, "Invalid config: link[%d] has empty interface name.", i);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a->priority <= 0) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s has invalid priority %d (must be > 0).", a->name, a->priority);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a->metric <= 0) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s has invalid metric %d (must be > 0).", a->name, a->metric);
|
||||
return false;
|
||||
}
|
||||
|
||||
// No duplicate priorities
|
||||
for (int j = i + 1; j < link_count; j++) {
|
||||
link_t *b = &links[j];
|
||||
if (a->priority == b->priority) {
|
||||
syslog(LOG_ERR, "Invalid config: duplicate priority %d on links %s and %s.", a->priority, a->name, b->name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool has_ping = (a->ping_target_count > 0);
|
||||
bool has_dns = (a->dns_server[0] != '\0' && a->dns_domain[0] != '\0');
|
||||
bool has_tcp = (a->tcp_target[0] != '\0' && a->tcp_port > 0);
|
||||
|
||||
int check_count = 0;
|
||||
if (has_ping) check_count++;
|
||||
if (has_dns) check_count++;
|
||||
if (has_tcp) check_count++;
|
||||
|
||||
if (check_count == 0) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s has no complete health-check probe configured.", a->name);
|
||||
return false;
|
||||
}
|
||||
if (check_count > 1) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s has multiple health-check probes configured. Only one check type is allowed.", a->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((a->dns_server[0] != '\0') != (a->dns_domain[0] != '\0')) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s DNS probe is incomplete (dns_server + dns_domain required).", a->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((a->tcp_target[0] != '\0') != (a->tcp_port > 0)) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s TCP probe is incomplete (tcp_target + tcp_port required).", a->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (a->check_interval <= 0 || a->check_timeout <= 0 ||
|
||||
a->recovery_delay <= 0 || a->failover_delay <= 0) {
|
||||
syslog(LOG_ERR, "Invalid config: link %s timing values must be > 0.", a->name);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Retrieve the real default route metric for a device from /proc/net/route
|
||||
static int get_system_route_metric(const char *device, int expected_metric) {
|
||||
if (device[0] == '\0') return -1;
|
||||
FILE *fp = fopen("/proc/net/route", "r");
|
||||
if (!fp) return -1;
|
||||
|
||||
char line[256];
|
||||
char iface[32];
|
||||
unsigned long dest;
|
||||
int metric = -1;
|
||||
int first_found_metric = -1;
|
||||
bool found_expected = false;
|
||||
|
||||
// Skip header line
|
||||
if (fgets(line, sizeof(line), fp)) {
|
||||
while (fgets(line, sizeof(line), fp)) {
|
||||
// Destination is 2nd column, Metric is 7th column, dest is in hex.
|
||||
// Use %*s for Gateway and Flags to safely skip non-numeric characters.
|
||||
if (sscanf(line, "%31s %lx %*s %*s %*d %*d %d", iface, &dest, &metric) == 3) {
|
||||
if (strcmp(iface, device) == 0 && dest == 0) {
|
||||
if (first_found_metric == -1) {
|
||||
first_found_metric = metric;
|
||||
}
|
||||
if (metric == expected_metric) {
|
||||
found_expected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
fclose(fp);
|
||||
|
||||
if (found_expected) {
|
||||
return expected_metric;
|
||||
}
|
||||
return first_found_metric;
|
||||
}
|
||||
|
||||
// Compare priority for sorting (lowest priority number is highest precedence)
|
||||
static int compare_links(const void *a, const void *b) {
|
||||
link_t *la = (link_t *)a;
|
||||
link_t *lb = (link_t *)b;
|
||||
return la->priority - lb->priority;
|
||||
}
|
||||
|
||||
// Dynamic route update using ip route command
|
||||
static void update_route_metric(link_t *link, int new_metric, int old_metric_to_delete) {
|
||||
if (link->device[0] == '\0') return;
|
||||
|
||||
char cmd[512];
|
||||
|
||||
// 1. Delete the specified old metric to prevent duplicate routes
|
||||
if (old_metric_to_delete != -1 && old_metric_to_delete != new_metric) {
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, old_metric_to_delete);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default dev %s metric %d 2>/dev/null",
|
||||
link->device, old_metric_to_delete);
|
||||
}
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
// 2. Delete the current_metric if it is different from new_metric and old_metric_to_delete
|
||||
if (link->current_metric != new_metric && link->current_metric != old_metric_to_delete) {
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, link->current_metric);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default dev %s metric %d 2>/dev/null",
|
||||
link->device, link->current_metric);
|
||||
}
|
||||
system(cmd);
|
||||
}
|
||||
|
||||
// 3. Add/replace with new_metric
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route replace default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, new_metric);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route replace default dev %s metric %d 2>/dev/null",
|
||||
link->device, new_metric);
|
||||
}
|
||||
|
||||
syslog(LOG_INFO, "Applying route metric update on link %s (%s, priority %d): %d -> %d",
|
||||
link->name, link->device, link->priority, link->current_metric, new_metric);
|
||||
|
||||
int rc = system(cmd);
|
||||
if (rc == 0) {
|
||||
link->current_metric = new_metric;
|
||||
} else {
|
||||
syslog(LOG_ERR, "Failed to apply route update for %s (priority %d) using cmd: %s", link->name, link->priority, cmd);
|
||||
}
|
||||
}
|
||||
|
||||
// Restore default metrics on exit
|
||||
static void restore_all_metrics(void) {
|
||||
syslog(LOG_INFO, "Restoring all interface metrics on exit...");
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
link_t *link = &links[i];
|
||||
if (link->enabled && link->is_up && link->device[0] != '\0') {
|
||||
char cmd[512];
|
||||
// Delete floated metric if it was in fault state
|
||||
if (link->current_metric != link->metric) {
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, link->current_metric);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route del default dev %s metric %d 2>/dev/null",
|
||||
link->device, link->current_metric);
|
||||
}
|
||||
system(cmd);
|
||||
}
|
||||
if (link->gateway[0] != '\0') {
|
||||
snprintf(cmd, sizeof(cmd), "ip route replace default via %s dev %s metric %d 2>/dev/null",
|
||||
link->gateway, link->device, link->metric);
|
||||
} else {
|
||||
snprintf(cmd, sizeof(cmd), "ip route replace default dev %s metric %d 2>/dev/null",
|
||||
link->device, link->metric);
|
||||
}
|
||||
int rc = system(cmd);
|
||||
if (rc == 0) {
|
||||
syslog(LOG_INFO, "Successfully restored default metric %d for interface %s (priority %d)", link->metric, link->name, link->priority);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Write status file to /var/run/linkback.json
|
||||
static void write_status_json(void) {
|
||||
FILE *fp = fopen(STATUS_FILE, "w");
|
||||
if (!fp) return;
|
||||
|
||||
fprintf(fp, "{\n");
|
||||
fprintf(fp, " \"enabled\": %s,\n", global_cfg.enabled ? "true" : "false");
|
||||
fprintf(fp, " \"check_interval\": 5,\n");
|
||||
|
||||
// Find current active gateway link (first healthy link ordered by priority)
|
||||
char active_link[MAX_NAME_LEN] = "none";
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
if (links[i].is_up && links[i].healthy) {
|
||||
strncpy(active_link, links[i].name, MAX_NAME_LEN - 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
fprintf(fp, " \"active_link\": \"%s\",\n", active_link);
|
||||
fprintf(fp, " \"links\": [\n");
|
||||
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
link_t *link = &links[i];
|
||||
fprintf(fp, " {\n");
|
||||
fprintf(fp, " \"name\": \"%s\",\n", link->name);
|
||||
fprintf(fp, " \"priority\": %d,\n", link->priority);
|
||||
fprintf(fp, " \"metric\": %d,\n", link->metric);
|
||||
fprintf(fp, " \"current_metric\": %d,\n", link->current_metric);
|
||||
fprintf(fp, " \"healthy\": %s,\n", link->healthy ? "true" : "false");
|
||||
fprintf(fp, " \"is_up\": %s,\n", link->is_up ? "true" : "false");
|
||||
fprintf(fp, " \"device\": \"%s\",\n", link->device);
|
||||
fprintf(fp, " \"gateway\": \"%s\",\n", link->gateway);
|
||||
fprintf(fp, " \"score\": %d,\n", link->current_score);
|
||||
fprintf(fp, " \"threshold\": %d,\n", link->weight_threshold);
|
||||
fprintf(fp, " \"check_interval\": %d,\n", link->check_interval);
|
||||
fprintf(fp, " \"check_timeout\": %d,\n", link->check_timeout);
|
||||
fprintf(fp, " \"recovery_delay\": %d,\n", link->recovery_delay);
|
||||
fprintf(fp, " \"failover_delay\": %d,\n", link->failover_delay);
|
||||
const char *type = "none";
|
||||
if (link->ping_target_count > 0) type = "ping";
|
||||
else if (link->dns_server[0] != '\0') type = "dns";
|
||||
else if (link->tcp_target[0] != '\0') type = "tcp";
|
||||
|
||||
fprintf(fp, " \"check_type\": \"%s\",\n", type);
|
||||
fprintf(fp, " \"ping\": {\"ok\": %s, \"rtt\": %d},\n", link->ping_ok ? "true" : "false", link->ping_rtt_ms);
|
||||
fprintf(fp, " \"dns\": {\"ok\": %s, \"rtt\": %d},\n", link->dns_ok ? "true" : "false", link->dns_rtt_ms);
|
||||
fprintf(fp, " \"tcp\": {\"ok\": %s, \"rtt\": %d}\n", link->tcp_ok ? "true" : "false", link->tcp_rtt_ms);
|
||||
fprintf(fp, " }%s\n", (i == link_count - 1) ? "" : ",");
|
||||
}
|
||||
fprintf(fp, " ]\n");
|
||||
fprintf(fp, "}\n");
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
// Setup syslog
|
||||
openlog("linkbackd", LOG_PID | LOG_NDELAY, LOG_DAEMON);
|
||||
syslog(LOG_INFO, "Starting LinkBack daemon...");
|
||||
|
||||
// Register signal handlers for clean exits and metric restoration
|
||||
signal(SIGTERM, handle_signal);
|
||||
signal(SIGINT, handle_signal);
|
||||
|
||||
// Load configuration
|
||||
if (!load_config()) {
|
||||
syslog(LOG_ERR, "Failed to load linkback config. Exiting.");
|
||||
closelog();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!global_cfg.enabled) {
|
||||
syslog(LOG_WARNING, "LinkBack is disabled globally in configuration. Exiting.");
|
||||
closelog();
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Sort links by priority (lowest priority number first)
|
||||
qsort(links, link_count, sizeof(link_t), compare_links);
|
||||
|
||||
syslog(LOG_INFO, "Loaded %d monitored interfaces. Starting health check scheduler.", link_count);
|
||||
|
||||
// Core check loop
|
||||
while (keep_running) {
|
||||
time_t now = time(NULL);
|
||||
bool any_checked = false;
|
||||
|
||||
for (int i = 0; i < link_count; i++) {
|
||||
link_t *link = &links[i];
|
||||
|
||||
// Check if this interface is due for checking
|
||||
if (now - link->last_checked < link->check_interval) {
|
||||
continue;
|
||||
}
|
||||
link->last_checked = now;
|
||||
any_checked = true;
|
||||
|
||||
// 1. Fetch real-time netifd status
|
||||
char dev[MAX_NAME_LEN] = {0};
|
||||
char gw[MAX_IP_LEN] = {0};
|
||||
bool is_up = false;
|
||||
|
||||
get_interface_ubus_status(link->name, dev, sizeof(dev), gw, sizeof(gw), &is_up);
|
||||
link->is_up = is_up;
|
||||
strncpy(link->device, dev, MAX_NAME_LEN - 1);
|
||||
strncpy(link->gateway, gw, MAX_IP_LEN - 1);
|
||||
|
||||
if (!is_up || dev[0] == '\0') {
|
||||
// Interface is down in netifd, mark unhealthy immediately
|
||||
link->healthy = false;
|
||||
link->current_score = 0;
|
||||
link->ping_ok = false;
|
||||
link->dns_ok = false;
|
||||
link->tcp_ok = false;
|
||||
link->consecutive_success = 0;
|
||||
link->consecutive_failure = 0;
|
||||
|
||||
// If it still has low metric, float it
|
||||
if (link->current_metric == link->metric) {
|
||||
update_route_metric(link, 1000 + link->metric, -1);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Perform health checks
|
||||
bool check_success = false;
|
||||
|
||||
if (link->ping_target_count > 0) {
|
||||
link->ping_ok = false;
|
||||
link->ping_rtt_ms = -1;
|
||||
for (int p = 0; p < link->ping_target_count; p++) {
|
||||
int rtt = -1;
|
||||
if (run_ping_check(link->device, link->ping_targets[p], link->check_timeout, &rtt)) {
|
||||
link->ping_ok = true;
|
||||
link->ping_rtt_ms = rtt;
|
||||
break;
|
||||
}
|
||||
}
|
||||
check_success = link->ping_ok;
|
||||
}
|
||||
else if (link->dns_server[0] != '\0' && link->dns_domain[0] != '\0') {
|
||||
link->dns_ok = false;
|
||||
link->dns_rtt_ms = -1;
|
||||
int rtt = -1;
|
||||
if (run_dns_check(link->device, link->dns_server, link->dns_domain, link->check_timeout, &rtt)) {
|
||||
link->dns_ok = true;
|
||||
link->dns_rtt_ms = rtt;
|
||||
}
|
||||
check_success = link->dns_ok;
|
||||
}
|
||||
else if (link->tcp_target[0] != '\0' && link->tcp_port > 0) {
|
||||
link->tcp_ok = false;
|
||||
link->tcp_rtt_ms = -1;
|
||||
int rtt = -1;
|
||||
if (run_tcp_check(link->device, link->tcp_target, link->tcp_port, link->check_timeout, &rtt)) {
|
||||
link->tcp_ok = true;
|
||||
link->tcp_rtt_ms = rtt;
|
||||
}
|
||||
check_success = link->tcp_ok;
|
||||
}
|
||||
|
||||
link->current_score = check_success ? 1 : 0;
|
||||
|
||||
// 3. Evaluate health state changes (filtering and delay)
|
||||
|
||||
if (check_success) {
|
||||
link->consecutive_success++;
|
||||
link->consecutive_failure = 0;
|
||||
|
||||
if (!link->healthy && link->consecutive_success >= link->recovery_delay) {
|
||||
// Recovered! Failback!
|
||||
link->healthy = true;
|
||||
syslog(LOG_NOTICE, "Link %s (%s, priority %d) recovered to healthy after %d successes.",
|
||||
link->name, link->device, link->priority, link->consecutive_success);
|
||||
|
||||
// Restore original metric
|
||||
update_route_metric(link, link->metric, -1);
|
||||
}
|
||||
} else {
|
||||
link->consecutive_failure++;
|
||||
link->consecutive_success = 0;
|
||||
|
||||
if (link->healthy && link->consecutive_failure >= link->failover_delay) {
|
||||
// Failed! Failover!
|
||||
link->healthy = false;
|
||||
syslog(LOG_WARNING, "Link %s (%s, priority %d) went down after %d failures.",
|
||||
link->name, link->device, link->priority, link->consecutive_failure);
|
||||
|
||||
// Push metric out of choice range
|
||||
update_route_metric(link, 1000 + link->metric, -1);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Active routing metric self-healing to prevent external/netifd interference
|
||||
if (link->device[0] != '\0') {
|
||||
int expected_metric = (link->is_up && link->healthy) ? link->metric : (1000 + link->metric);
|
||||
int real_metric = get_system_route_metric(link->device, expected_metric);
|
||||
if (real_metric != -1 && real_metric != expected_metric) {
|
||||
syslog(LOG_WARNING, "Route metric mismatch detected on %s (%s, priority %d): expected %d, got %d. Correcting...",
|
||||
link->name, link->device, link->priority, expected_metric, real_metric);
|
||||
update_route_metric(link, expected_metric, real_metric);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (any_checked) {
|
||||
// Write states to shared JSON file
|
||||
write_status_json();
|
||||
}
|
||||
|
||||
// High-responsiveness scheduler ticks every second
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
// Terminating: clean up routes before exit
|
||||
restore_all_metrics();
|
||||
unlink(STATUS_FILE);
|
||||
syslog(LOG_INFO, "LinkBack daemon terminated successfully.");
|
||||
closelog();
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#ifndef LINKBACKD_H
|
||||
#define LINKBACKD_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <time.h>
|
||||
|
||||
#define MAX_LINKS 16
|
||||
#define MAX_NAME_LEN 32
|
||||
#define MAX_TARGETS 8
|
||||
#define MAX_IP_LEN 64
|
||||
#define MAX_DOMAIN_LEN 128
|
||||
#define STATUS_FILE "/var/run/linkback.json"
|
||||
|
||||
typedef struct {
|
||||
char name[MAX_NAME_LEN];
|
||||
bool enabled;
|
||||
int priority;
|
||||
int metric;
|
||||
|
||||
// Ping config
|
||||
char ping_targets[MAX_TARGETS][MAX_IP_LEN];
|
||||
int ping_target_count;
|
||||
int ping_weight;
|
||||
|
||||
// DNS config
|
||||
char dns_server[MAX_IP_LEN];
|
||||
char dns_domain[MAX_DOMAIN_LEN];
|
||||
int dns_weight;
|
||||
|
||||
// TCP config
|
||||
char tcp_target[MAX_IP_LEN];
|
||||
int tcp_port;
|
||||
int tcp_weight;
|
||||
|
||||
// Thresholds
|
||||
int weight_threshold;
|
||||
int check_interval;
|
||||
int check_timeout;
|
||||
int recovery_delay;
|
||||
int failover_delay;
|
||||
|
||||
// Runtime state
|
||||
char device[MAX_NAME_LEN]; // physical interface name e.g., pppoe-wan, eth1
|
||||
char gateway[MAX_IP_LEN]; // gateway IP address
|
||||
bool is_up; // whether interface is reported up by netifd
|
||||
bool healthy; // daemon link health state
|
||||
int consecutive_success;
|
||||
int consecutive_failure;
|
||||
time_t last_checked; // Last health check timestamp
|
||||
|
||||
// Detailed health checks status
|
||||
bool ping_ok;
|
||||
int ping_rtt_ms;
|
||||
bool dns_ok;
|
||||
int dns_rtt_ms;
|
||||
bool tcp_ok;
|
||||
int tcp_rtt_ms;
|
||||
|
||||
int current_score;
|
||||
int current_metric; // actual system metric applied
|
||||
} link_t;
|
||||
|
||||
typedef struct {
|
||||
bool enabled;
|
||||
} global_config_t;
|
||||
|
||||
#endif // LINKBACKD_H
|
||||
@@ -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:=24
|
||||
PKG_RELEASE:=32
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
|
||||
PKG_SOURCE_VERSION:=4bb94bc5fe4da6d98dfcd730f40fe15f0d71ec4a
|
||||
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:=27
|
||||
PKG_RELEASE:=35
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -28,7 +28,7 @@ else ifeq ($(ARCH),mipsel)
|
||||
ARCH_HEXCODE=1b0c
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
|
||||
PKG_SOURCE_VERSION:=4bb94bc5fe4da6d98dfcd730f40fe15f0d71ec4a
|
||||
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.17
|
||||
PKG_RELEASE:=26
|
||||
PKG_SOURCE_DATE:=3.0.20
|
||||
PKG_RELEASE:=34
|
||||
|
||||
ARCH_HEXCODE:=
|
||||
ifeq ($(ARCH),x86_64)
|
||||
@@ -24,7 +24,7 @@ LINKEASE_RUNTIME_ARCH:=arm64
|
||||
PKG_HASH:=skip
|
||||
endif
|
||||
|
||||
PKG_SOURCE_VERSION:=469e9a582af646e9d3df7aec4b31fcfda9e4a325
|
||||
PKG_SOURCE_VERSION:=4bb94bc5fe4da6d98dfcd730f40fe15f0d71ec4a
|
||||
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)
|
||||
|
||||
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
LUCI_TITLE:=AgentFlow
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=3
|
||||
PKG_RELEASE:=5
|
||||
LUCI_DEPENDS:=+agentflow +luci-compat
|
||||
LUCI_MINIFY_CSS:=0
|
||||
LUCI_MINIFY_JS:=0
|
||||
|
||||
@@ -2,28 +2,128 @@ 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
|
||||
|
||||
function index()
|
||||
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
|
||||
local open = entry({"admin", "services", "agentflow", "open"}, call("agentflow_open"))
|
||||
open.leaf = true
|
||||
open.dependent = false
|
||||
open.sysauth = false
|
||||
|
||||
if not nixio.fs.access("/etc/config/agentflow") then
|
||||
return
|
||||
end
|
||||
|
||||
local page = entry({"admin", "services", "agentflow"}, cbi("agentflow"), _("AgentFlow"), 100)
|
||||
page.dependent = true
|
||||
entry({"admin", "services", "agentflow_status"}, call("agentflow_status"))
|
||||
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
|
||||
|
||||
function agentflow_status()
|
||||
local sys = require "luci.sys"
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local port = tonumber(uci:get_first("agentflow", "agentflow", "port")) or 9000
|
||||
if port < 1 or port > 65535 then
|
||||
port = 9000
|
||||
end
|
||||
local port, base_path = agentflow_config()
|
||||
local entry_url = agentflow_entry_url()
|
||||
|
||||
local status = {
|
||||
running = (sys.call("pidof agentflow >/dev/null") == 0),
|
||||
port = port
|
||||
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()
|
||||
}
|
||||
http.prepare_content("application/json")
|
||||
http.write_json(status)
|
||||
end
|
||||
|
||||
function agentflow_open()
|
||||
local entry_url = agentflow_entry_url()
|
||||
http.redirect(entry_url)
|
||||
end
|
||||
|
||||
@@ -45,37 +45,14 @@ agentflow.find_paths = function(blocks, home_dirs)
|
||||
return paths, default_path
|
||||
end
|
||||
|
||||
local dirname = function(path)
|
||||
path = (path or ""):match("^%s*(.-)%s*$")
|
||||
path = path:gsub("/+$", "")
|
||||
return path:match("^(.*)/[^/]+$") or ""
|
||||
end
|
||||
|
||||
agentflow.runtime_dir = function(data_dir, home_dirs)
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local configured = uci:get_first("mise", "mise", "runtime_dir", "")
|
||||
if configured ~= nil and configured ~= "" then
|
||||
return configured
|
||||
end
|
||||
|
||||
local conf_dir = dirname(data_dir)
|
||||
if conf_dir == "" then
|
||||
conf_dir = home_dirs["Configs"]
|
||||
end
|
||||
agentflow.runtime_dir = function(data_dir)
|
||||
data_dir = (data_dir or ""):match("^%s*(.-)%s*$"):gsub("/+$", "")
|
||||
local conf_dir = data_dir:match("^(.*)/[^/]+$")
|
||||
if conf_dir == nil or conf_dir == "" then
|
||||
return ""
|
||||
return nil
|
||||
end
|
||||
|
||||
return conf_dir .. "/Runtime"
|
||||
end
|
||||
|
||||
agentflow.runtime_home = function(data_dir, home_dirs)
|
||||
local runtime_dir = agentflow.runtime_dir(data_dir, home_dirs)
|
||||
if runtime_dir == nil or runtime_dir == "" then
|
||||
return ""
|
||||
end
|
||||
|
||||
return runtime_dir .. "/home"
|
||||
return conf_dir .. "/Runtime/home"
|
||||
end
|
||||
|
||||
return agentflow
|
||||
|
||||
@@ -34,15 +34,22 @@ for _, val in pairs(paths) do
|
||||
end
|
||||
data_dir.default = default_path
|
||||
|
||||
local runtime_home = s:option(DummyValue, "_runtime_home", translate("Shared runtime home"))
|
||||
runtime_home.description = translate("AgentFlow and other runtime-aware applications share this HOME. It is derived from the selected Configs directory unless mise has an explicit runtime directory.")
|
||||
function runtime_home.cfgvalue(self, section)
|
||||
local selected_data_dir = m.uci:get("agentflow", section, "data_dir") or default_path
|
||||
local path = agentflow_model.runtime_home(selected_data_dir, home)
|
||||
if path == "" then
|
||||
return translate("Not configured")
|
||||
function m.on_after_commit(self)
|
||||
local uci = require "luci.model.uci".cursor()
|
||||
local runtime_dir = uci:get("mise", "main", "runtime_dir")
|
||||
if runtime_dir ~= nil and runtime_dir ~= "" then
|
||||
return
|
||||
end
|
||||
|
||||
local saved_data_dir = uci:get_first("agentflow", "agentflow", "data_dir", "")
|
||||
runtime_dir = agentflow_model.runtime_dir(saved_data_dir)
|
||||
if runtime_dir == nil then
|
||||
return
|
||||
end
|
||||
|
||||
if uci:set("mise", "main", "runtime_dir", runtime_dir) then
|
||||
uci:commit("mise")
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
local port = s:option(Value, "port", translate("Listen port"))
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
<script type="text/javascript">//<![CDATA[
|
||||
var agentflowOpenUrl = '<%=url("admin/services/agentflow/open")%>';
|
||||
XHR.poll(5, '<%=url("admin/services/agentflow_status")%>', null, function(x, st) {
|
||||
var el = document.getElementById('agentflow_status');
|
||||
if (st && el) {
|
||||
if (!st.running) {
|
||||
el.innerHTML = '<br/><em style="color:red"><%:The AgentFlow service is not running.%></em>';
|
||||
} else {
|
||||
var accessMode = '';
|
||||
if (st.proxy_prefix_enabled && st.linkeasefull_running) {
|
||||
accessMode = '<br/><em><%:Access mode: LinkEase Desktop /apps proxy.%></em>';
|
||||
} else {
|
||||
accessMode = '<br/><em><%:Access mode: external port.%></em>';
|
||||
}
|
||||
el.innerHTML = '<br/><em style="color:green"><%:The AgentFlow service is running.%></em>'
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('http://" + window.location.hostname + ":" + st.port + "/')\"/>";
|
||||
+ accessMode
|
||||
+ "<br/><br/><input class=\"btn cbi-button cbi-button-apply\" type=\"button\" value=\" <%:Click to open AgentFlow%> \" onclick=\"window.open('" + agentflowOpenUrl + "')\"/>";
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -10,6 +10,12 @@ msgstr "AgentFlow 提供用于编排编码智能体和工作流的 Web 管理界
|
||||
msgid "Click to open AgentFlow"
|
||||
msgstr "点击打开 AgentFlow"
|
||||
|
||||
msgid "Access mode: LinkEase Desktop /apps proxy."
|
||||
msgstr "访问模式:LinkEase Desktop /apps 代理。"
|
||||
|
||||
msgid "Access mode: external port."
|
||||
msgstr "访问模式:外部端口。"
|
||||
|
||||
msgid "Collecting data..."
|
||||
msgstr "正在获取数据……"
|
||||
|
||||
@@ -37,15 +43,6 @@ msgstr "请选择硬盘作为数据目录"
|
||||
msgid "Required. AgentFlow stores its configuration, database and workspace data under this directory."
|
||||
msgstr "必需。AgentFlow 在此目录中保存配置、数据库和工作区数据。"
|
||||
|
||||
msgid "Shared runtime home"
|
||||
msgstr "共享运行时 HOME"
|
||||
|
||||
msgid "AgentFlow and other runtime-aware applications share this HOME. It is derived from the selected Configs directory unless mise has an explicit runtime directory."
|
||||
msgstr "AgentFlow 和其他运行时感知应用共用此 HOME。除非 mise 显式设置了运行时目录,否则它会从所选 Configs 目录推导。"
|
||||
|
||||
msgid "Not configured"
|
||||
msgstr "未配置"
|
||||
|
||||
msgid "Status"
|
||||
msgstr "状态"
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
from pathlib import Path
|
||||
import unittest
|
||||
|
||||
|
||||
APP_DIR = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
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):
|
||||
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('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('url("admin/services/agentflow/open")', status)
|
||||
self.assertNotIn('window.location.hostname + ":" + st.port', status)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,15 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-ap-switch
|
||||
PKG_VERSION:=1.0.0
|
||||
PKG_RELEASE:=1
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=Dante
|
||||
|
||||
LUCI_TITLE:=LuCI support for one-click AP mode switching
|
||||
LUCI_DEPENDS:=+luci-base +ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
include $(TOPDIR)/feeds/luci/luci.mk
|
||||
|
||||
# call BuildPackage - OpenWrt buildroot signature
|
||||
@@ -0,0 +1,201 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require rpc';
|
||||
'require ui';
|
||||
'require uci';
|
||||
|
||||
var callGetStatus = rpc.declare({
|
||||
object: 'luci.ap-switch',
|
||||
method: 'get_status'
|
||||
});
|
||||
|
||||
var callSetMode = rpc.declare({
|
||||
object: 'luci.ap-switch',
|
||||
method: 'set_mode',
|
||||
params: ['mode']
|
||||
});
|
||||
|
||||
var callProbeIP = rpc.declare({
|
||||
object: 'luci.ap-switch',
|
||||
method: 'probe_ip'
|
||||
});
|
||||
|
||||
return L.view.extend({
|
||||
i18n: 'ap-switch',
|
||||
handleSaveApply: null,
|
||||
handleSave: null,
|
||||
handleReset: null,
|
||||
|
||||
load: function() {
|
||||
return Promise.all([
|
||||
callGetStatus(),
|
||||
uci.load('ap-switch')
|
||||
]);
|
||||
},
|
||||
|
||||
render: function(data) {
|
||||
var self = this;
|
||||
var status = data[0];
|
||||
var mode = status.mode || 'router';
|
||||
var lan_ip = status.lan_ip || 'N/A';
|
||||
var lan_proto = status.lan_proto || 'static';
|
||||
var lan_mac = status.lan_mac || 'N/A';
|
||||
|
||||
var changeModeRows = [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, _('Switch to %s').format(mode === 'ap' ? _('Router Mode') : _('AP Mode'))),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-apply',
|
||||
'click': ui.createHandlerFn(self, function() {
|
||||
return self.handleSwitch(mode === 'ap' ? 'router' : 'ap', status);
|
||||
})
|
||||
}, [ (mode === 'ap' ? _('Switch to Router Mode') : _('Switch to AP Mode')) ])
|
||||
])
|
||||
])
|
||||
];
|
||||
|
||||
var body = E('div', { 'class': 'cbi-map' }, [
|
||||
E('h2', {}, _('AP Switch Title') + ' - ' + _('Switch Mode')),
|
||||
E('div', { 'class': 'cbi-map-descr' }, _('Switch your system runtime configuration smoothly between standard Router and Access Point (AP) modes. Network interfaces and bridge rules will be adjusted automatically.')),
|
||||
E('fieldset', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, _('Current Status')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, [
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left', 'width': '33%' }, _('Operation Mode')),
|
||||
E('td', { 'class': 'td left' }, E('strong', {}, (mode === 'ap' ? _('Access Point (AP)') : _('Router'))))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('Login URL')),
|
||||
E('td', { 'class': 'td left' }, lan_ip ? E('a', {
|
||||
'href': 'http://' + lan_ip,
|
||||
'target': '_blank',
|
||||
'style': 'text-decoration: underline; font-weight: bold; color: blue;'
|
||||
}, 'http://' + lan_ip) : _('N/A'))
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('LAN IP Address')),
|
||||
E('td', { 'class': 'td left' }, [
|
||||
E('strong', { 'style': 'color: #2196F3;' }, lan_ip || _('Pending...')),
|
||||
E('span', { 'style': 'margin-left: 10px; color: #666;' }, '(' + lan_proto + ')')
|
||||
])
|
||||
]),
|
||||
E('tr', { 'class': 'tr' }, [
|
||||
E('td', { 'class': 'td left' }, _('LAN MAC Address')),
|
||||
E('td', { 'class': 'td left', 'id': 'lan-mac' }, lan_mac)
|
||||
])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('fieldset', { 'class': 'cbi-section' }, [
|
||||
E('legend', {}, _('Change Mode')),
|
||||
E('div', { 'class': 'cbi-section-node' }, [
|
||||
E('table', { 'class': 'table cbi-section-table' }, changeModeRows)
|
||||
])
|
||||
])
|
||||
]);
|
||||
|
||||
return body;
|
||||
},
|
||||
|
||||
handleSwitch: function(target_mode, status) {
|
||||
var self = this;
|
||||
|
||||
var executeSwitch = function() {
|
||||
ui.showModal(null, [
|
||||
E('p', { 'class': 'spinning' }, _('Applying changes and restarting network...')),
|
||||
E('p', {}, _('The page will redirect or you may need to manually reconnect to the new IP address in a few moments.'))
|
||||
]);
|
||||
|
||||
return callSetMode(target_mode).then(function(res) {
|
||||
if (res && res.result === 'success') {
|
||||
setTimeout(function() {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Mode switched successfully. Please check your network connection.')), 'info');
|
||||
}, 5000);
|
||||
} else {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Failed to switch mode: %s').format(res.error || 'Unknown error')), 'error');
|
||||
}
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
ui.addNotification(null, E('p', _('Error calling RPC: %s').format(err.message)), 'error');
|
||||
});
|
||||
};
|
||||
|
||||
var showConfirmModal = function(title, messages, onConfirm) {
|
||||
var content = [];
|
||||
messages.forEach(function(msg) {
|
||||
content.push(E('p', { 'style': 'margin-bottom: 10px; line-height: 1.5;' }, msg));
|
||||
});
|
||||
content.push(E('div', { 'class': 'right', 'style': 'margin-top: 20px;' }, [
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-reset',
|
||||
'click': ui.hideModal
|
||||
}, _('Cancel')),
|
||||
E('button', {
|
||||
'class': 'cbi-button cbi-button-action cbi-button-apply',
|
||||
'style': 'margin-left: 10px;',
|
||||
'click': function() {
|
||||
ui.hideModal();
|
||||
onConfirm();
|
||||
}
|
||||
}, _('Confirm'))
|
||||
]));
|
||||
|
||||
ui.showModal(title, content);
|
||||
};
|
||||
|
||||
if (target_mode === 'ap') {
|
||||
ui.showModal(null, [
|
||||
E('p', { 'class': 'spinning' }, _('Auto-probing for future AP IP via DHCP on br-lan...')),
|
||||
E('p', {}, _('This ensures you can find your way back to the management panel after the switch.'))
|
||||
]);
|
||||
|
||||
return callProbeIP().then(function(res) {
|
||||
ui.hideModal();
|
||||
|
||||
var title = _('Confirm AP Mode Switch');
|
||||
var messages = [];
|
||||
|
||||
if (res && res.ip) {
|
||||
messages.push(E('span', {}, [
|
||||
_('Successfully pre-fetched future management IP: '),
|
||||
E('a', {
|
||||
'href': 'http://' + res.ip,
|
||||
'target': '_blank',
|
||||
'style': 'color: #2196F3; font-weight: bold; text-decoration: underline; word-break: break-all;'
|
||||
}, 'http://' + res.ip)
|
||||
]));
|
||||
messages.push(_('You can use this URL to access this dashboard after the switch.'));
|
||||
messages.push(_('Are you sure you want to switch to AP mode and restart network?'));
|
||||
} else {
|
||||
messages.push(_('WARNING: Failed to pre-fetch IP address via DHCP probe!'));
|
||||
messages.push(_('This may be because your LAN port is not connected to the main router.'));
|
||||
messages.push(_('If you proceed, you must find the new IP from your main router client list using MAC: %s').format(status.lan_mac || 'N/A'));
|
||||
messages.push(_('Do you still want to force the switch anyway?'));
|
||||
}
|
||||
|
||||
showConfirmModal(title, messages, executeSwitch);
|
||||
}).catch(function(err) {
|
||||
ui.hideModal();
|
||||
|
||||
var title = _('Confirm AP Mode Switch');
|
||||
var messages = [
|
||||
_('WARNING: DHCP probe RPC error: %s').format(err.message),
|
||||
_('If you proceed, you must find the new IP from your main router client list using MAC: %s').format(status.lan_mac || 'N/A'),
|
||||
_('Do you still want to force the switch anyway?')
|
||||
];
|
||||
|
||||
showConfirmModal(title, messages, executeSwitch);
|
||||
});
|
||||
} else {
|
||||
var title = _('Confirm Router Mode Switch');
|
||||
var messages = [
|
||||
_('Switching to Router mode will restore the WAN port and local DHCP server. Your device will use the previous static IP address. Are you sure?')
|
||||
];
|
||||
showConfirmModal(title, messages, executeSwitch);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
msgid "AP Switch"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch between Router and Access Point (AP) mode."
|
||||
msgstr ""
|
||||
|
||||
msgid "Current Status"
|
||||
msgstr ""
|
||||
|
||||
msgid "Operation Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Access Point (AP)"
|
||||
msgstr ""
|
||||
|
||||
msgid "Router"
|
||||
msgstr ""
|
||||
|
||||
msgid "LAN IP Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Change Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Router Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "AP Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to Router Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switch to AP Mode"
|
||||
msgstr ""
|
||||
|
||||
msgid "LAN MAC Address"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switching to AP mode will bridge the WAN port into \"br-lan\". The \"br-lan\" interface will become a DHCP client to your main router."
|
||||
msgstr ""
|
||||
|
||||
msgid "Pre-fetch AP IP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Probing for DHCP IP on br-lan..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Successfully fetched IP: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to fetch IP. Ensure a LAN port is connected to your main router."
|
||||
msgstr ""
|
||||
|
||||
msgid "Probe IP"
|
||||
msgstr ""
|
||||
|
||||
msgid "Not probed"
|
||||
msgstr ""
|
||||
|
||||
msgid "A valid IP was pre-fetched: %s. You can use this address to log in after the switch."
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure you want to proceed?"
|
||||
msgstr ""
|
||||
|
||||
msgid "WARNING: No IP address was pre-fetched via DHCP probe!"
|
||||
msgstr ""
|
||||
|
||||
msgid "We strongly recommend connecting your LAN port to the main router and using \"Probe IP\" first."
|
||||
msgstr ""
|
||||
|
||||
msgid "If you proceed now, you must find the new IP from your main router using MAC: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Do you want to proceed anyway or cancel to plug in the cable and try again?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Pending..."
|
||||
msgstr ""
|
||||
|
||||
msgid "Login URL"
|
||||
msgstr ""
|
||||
|
||||
msgid "N/A"
|
||||
msgstr ""
|
||||
|
||||
msgid "None"
|
||||
msgstr ""
|
||||
|
||||
msgid "Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Switching to Router mode will restore the WAN port and local DHCP server. Your device will use the previous static IP address. Are you sure?"
|
||||
msgstr ""
|
||||
|
||||
msgid "Applying changes and restarting network..."
|
||||
msgstr ""
|
||||
|
||||
msgid "The page will redirect or you may need to manually reconnect to the new IP address in a few moments."
|
||||
msgstr ""
|
||||
|
||||
msgid "Mode switched successfully. Please check your network connection."
|
||||
msgstr ""
|
||||
|
||||
msgid "Failed to switch mode: %s"
|
||||
msgstr ""
|
||||
|
||||
msgid "Error calling RPC: %s"
|
||||
msgstr ""
|
||||
@@ -0,0 +1,169 @@
|
||||
msgid ""
|
||||
msgstr "Content-Type: text/plain; charset=UTF-8"
|
||||
|
||||
msgid "AP Switch"
|
||||
msgstr "AP模式切换"
|
||||
|
||||
msgid "Switch between Router and Access Point (AP) mode."
|
||||
msgstr "在路由器模式和访问点 (AP) 模式之间切换。"
|
||||
|
||||
msgid "Current Status"
|
||||
msgstr "当前状态"
|
||||
|
||||
msgid "Operation Mode"
|
||||
msgstr "运行模式"
|
||||
|
||||
msgid "Access Point (AP)"
|
||||
msgstr "访问点 (AP)"
|
||||
|
||||
msgid "Router"
|
||||
msgstr "路由器"
|
||||
|
||||
msgid "LAN IP Address"
|
||||
msgstr "LAN IP 地址"
|
||||
|
||||
msgid "Change Mode"
|
||||
msgstr "更改模式"
|
||||
|
||||
msgid "Switch to %s"
|
||||
msgstr "切换到 %s"
|
||||
|
||||
msgid "Router Mode"
|
||||
msgstr "路由器模式"
|
||||
|
||||
msgid "AP Mode"
|
||||
msgstr "AP 模式"
|
||||
|
||||
msgid "Switch to Router Mode"
|
||||
msgstr "切换到路由器模式"
|
||||
|
||||
msgid "Switch to AP Mode"
|
||||
msgstr "切换到 AP 模式"
|
||||
|
||||
msgid "LAN MAC Address"
|
||||
msgstr "LAN MAC 地址"
|
||||
|
||||
msgid "Switching to AP mode will bridge the WAN port into \"br-lan\". The \"br-lan\" interface will become a DHCP client to your main router."
|
||||
msgstr "切换到 AP 模式会将 WAN 口桥接到 \"br-lan\"。\"br-lan\" 接口将成为主路由器的 DHCP 客户端。"
|
||||
|
||||
msgid "Pre-fetch AP IP"
|
||||
msgstr "预获取 AP IP"
|
||||
|
||||
msgid "Probing for DHCP IP on br-lan..."
|
||||
msgstr "正在 br-lan 接口探测 DHCP IP..."
|
||||
|
||||
msgid "Successfully fetched IP: %s"
|
||||
msgstr "成功获取到 IP:%s"
|
||||
|
||||
msgid "Failed to fetch IP. Ensure a LAN port is connected to your main router."
|
||||
msgstr "获取 IP 失败。请确保 LAN 口已连接到主路由器。"
|
||||
|
||||
msgid "Probe IP"
|
||||
msgstr "开始探测"
|
||||
|
||||
msgid "Not probed"
|
||||
msgstr "未探测"
|
||||
|
||||
msgid "A valid IP was pre-fetched: %s. You can use this address to log in after the switch."
|
||||
msgstr "已预获取到有效的 IP:%s。切换后,您可以使用此地址登录。"
|
||||
|
||||
msgid "Are you sure you want to proceed?"
|
||||
msgstr "确定要继续吗?"
|
||||
|
||||
msgid "WARNING: No IP address was pre-fetched via DHCP probe!"
|
||||
msgstr "警告:未通过 DHCP 探测预获取到 IP 地址!"
|
||||
|
||||
msgid "We strongly recommend connecting your LAN port to the main router and using \"Probe IP\" first."
|
||||
msgstr "强烈建议先将 LAN 口连接到主路由器,并使用“预获取 AP IP”功能。"
|
||||
|
||||
msgid "If you proceed now, you must find the new IP from your main router using MAC: %s"
|
||||
msgstr "如果您现在继续,切换后必须通过 MAC 地址在主路由中查找新 IP:%s"
|
||||
|
||||
msgid "Do you want to proceed anyway or cancel to plug in the cable and try again?"
|
||||
msgstr "您是坚持继续,还是取消操作并在插上网线后重新尝试?"
|
||||
|
||||
msgid "Pending..."
|
||||
msgstr "等待中..."
|
||||
|
||||
msgid "Login URL"
|
||||
msgstr "登录地址"
|
||||
|
||||
msgid "N/A"
|
||||
msgstr "不适用"
|
||||
|
||||
msgid "None"
|
||||
msgstr "无"
|
||||
|
||||
msgid "Are you sure?"
|
||||
msgstr "确定要继续吗?"
|
||||
|
||||
msgid "Switching to Router mode will restore the WAN port and local DHCP server. Your device will use the previous static IP address. Are you sure?"
|
||||
msgstr "切换到路由器模式将恢复 WAN 口和本地 DHCP 服务。您的设备将使用之前的静态 IP 地址。确定要继续吗?"
|
||||
|
||||
msgid "Applying changes and restarting network..."
|
||||
msgstr "正在应用更改并重启网络..."
|
||||
|
||||
msgid "The page will redirect or you may need to manually reconnect to the new IP address in a few moments."
|
||||
msgstr "页面将会重定向,或者您可能需要在片刻后手动连接到新的 IP 地址。"
|
||||
|
||||
msgid "Mode switched successfully. Please check your network connection."
|
||||
msgstr "模式切换成功。请检查您的网络连接。"
|
||||
|
||||
msgid "Failed to switch mode: %s"
|
||||
msgstr "切换模式失败:%s"
|
||||
|
||||
msgid "Error calling RPC: %s"
|
||||
msgstr "调用 RPC 错误:%s"
|
||||
|
||||
msgid "Switch Mode"
|
||||
msgstr "模式切换"
|
||||
|
||||
msgid "Switch your system runtime configuration smoothly between standard Router and Access Point (AP) modes. Network interfaces and bridge rules will be adjusted automatically."
|
||||
msgstr "在标准路由器模式与 AP 桥接访问点模式之间平滑切换系统运行配置。切换后,网口物理桥接规则以及接口 DHCP 客户端行为将自动智能调整。"
|
||||
|
||||
msgid "AP Switch Title"
|
||||
msgstr "AP 模式切换"
|
||||
|
||||
msgid "Auto-probing for future AP IP via DHCP on br-lan..."
|
||||
msgstr "正在通过 br-lan 接口的 DHCP 自动探测未来的 AP 管理 IP 地址..."
|
||||
|
||||
msgid "This ensures you can find your way back to the management panel after the switch."
|
||||
msgstr "这可以确保您在切换模式后,能够顺利找到并登录新的设备管理后台。"
|
||||
|
||||
msgid "Successfully pre-fetched future management IP: "
|
||||
msgstr "成功预先获取到未来的管理 IP:"
|
||||
|
||||
msgid "You can use this URL to access this dashboard after the switch."
|
||||
msgstr "您可以使用此 URL 地址在切换后访问本控制台。"
|
||||
|
||||
msgid "Are you sure you want to switch to AP mode and restart network?"
|
||||
msgstr "您确定要切换到 AP 模式并重启网络吗?"
|
||||
|
||||
msgid "WARNING: Failed to pre-fetch IP address via DHCP probe!"
|
||||
msgstr "警告:通过 DHCP 探测预获取 IP 地址失败!"
|
||||
|
||||
msgid "This may be because your LAN port is not connected to the main router."
|
||||
msgstr "这可能是由于您设备的 LAN 物理端口尚未连接到上层主路由器。"
|
||||
|
||||
msgid "If you proceed, you must find the new IP from your main router client list using MAC: %s"
|
||||
msgstr "如果您继续,切换后必须通过 MAC 地址在上级主路由的客户端列表中查找新 IP:%s"
|
||||
|
||||
msgid "Do you still want to force the switch anyway?"
|
||||
msgstr "您是否仍要强制执行切换?"
|
||||
|
||||
msgid "WARNING: DHCP probe RPC error: %s"
|
||||
msgstr "警告:DHCP 探测 RPC 请求发生错误:%s"
|
||||
|
||||
msgid "Confirm AP Mode Switch"
|
||||
msgstr "确认切换至 AP 模式"
|
||||
|
||||
msgid "Confirm Router Mode Switch"
|
||||
msgstr "确认切换至路由器模式"
|
||||
|
||||
msgid "Cancel"
|
||||
msgstr "取消"
|
||||
|
||||
msgid "Confirm"
|
||||
msgstr "确认"
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
|
||||
config global 'settings'
|
||||
option mode 'router'
|
||||
|
||||
config backup 'router_state'
|
||||
option lan_proto 'static'
|
||||
option lan_ipaddr '192.168.1.1'
|
||||
option lan_netmask '255.255.255.0'
|
||||
option lan_gateway ''
|
||||
list lan_dns ''
|
||||
option dhcp_ignore '0'
|
||||
option wan_device ''
|
||||
option wan_disabled '0'
|
||||
option wan6_disabled '0'
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
|
||||
[ "$1" = "bound" ] || exit 0
|
||||
|
||||
echo "$ip" > /tmp/ap_switch_probed_ip
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"admin/services/ap_switch": {
|
||||
"title": "AP Switch",
|
||||
"action": {
|
||||
"type": "view",
|
||||
"path": "ap-switch/ap-switch"
|
||||
},
|
||||
"order": 90,
|
||||
"depends": {
|
||||
"acl": [ "luci-app-ap-switch" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"luci-app-ap-switch": {
|
||||
"description": "ACL for AP Switch",
|
||||
"read": {
|
||||
"uci": [ "ap-switch", "network", "dhcp" ],
|
||||
"ubus": {
|
||||
"luci.ap-switch": [ "get_status", "probe_ip" ],
|
||||
"network.interface": [ "dump" ],
|
||||
"network.device": [ "status" ]
|
||||
}
|
||||
},
|
||||
"write": {
|
||||
"uci": [ "ap-switch", "network", "dhcp" ],
|
||||
"ubus": {
|
||||
"luci.ap-switch": [ "set_mode" ]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
'use strict';
|
||||
|
||||
import { cursor } from 'uci';
|
||||
import { connect } from 'ubus';
|
||||
import { readfile, unlink } from 'fs';
|
||||
|
||||
const UCI_CONFIG = 'ap-switch';
|
||||
const PROBE_FILE = '/tmp/ap_switch_probed_ip';
|
||||
|
||||
const methods = {
|
||||
get_status: {
|
||||
call: function() {
|
||||
let uci_cursor = cursor();
|
||||
let mode = uci_cursor.get(UCI_CONFIG, 'settings', 'mode') || 'router';
|
||||
|
||||
let lan_proto = uci_cursor.get('network', 'lan', 'proto') || 'static';
|
||||
let lan_ip = ''; // We will get the real-time IP
|
||||
|
||||
let ubus_conn = connect();
|
||||
let lan_mac = '';
|
||||
let wan_mac = '';
|
||||
let wan_ip = '';
|
||||
|
||||
if (ubus_conn) {
|
||||
// 1. Get real-time LAN IP and MAC
|
||||
let lan_status = ubus_conn.call('network.interface.lan', 'status');
|
||||
if (lan_status && type(lan_status['ipv4-address']) === 'array' && length(lan_status['ipv4-address']) > 0) {
|
||||
lan_ip = lan_status['ipv4-address'][0].address;
|
||||
}
|
||||
|
||||
let lan_dev = ubus_conn.call('network.device', 'status', { name: 'br-lan' });
|
||||
lan_mac = lan_dev?.macaddr || '';
|
||||
|
||||
// 2. Get current WAN interface status and its physical device MAC
|
||||
let wan_status = ubus_conn.call('network.interface.wan', 'status');
|
||||
let wan_device_name = wan_status?.device || '';
|
||||
if (wan_device_name) {
|
||||
let wan_dev = ubus_conn.call('network.device', 'status', { name: wan_device_name });
|
||||
wan_mac = wan_dev?.macaddr || '';
|
||||
}
|
||||
|
||||
// 3. Get current WAN IP
|
||||
if (wan_status && type(wan_status['ipv4-address']) === 'array' && length(wan_status['ipv4-address']) > 0) {
|
||||
wan_ip = wan_status['ipv4-address'][0].address;
|
||||
}
|
||||
ubus_conn.disconnect();
|
||||
}
|
||||
|
||||
let probed_ip = rtrim(readfile(PROBE_FILE) || '');
|
||||
|
||||
return {
|
||||
mode: mode,
|
||||
lan_proto: lan_proto,
|
||||
lan_ip: lan_ip,
|
||||
lan_mac: lan_mac,
|
||||
wan_mac: wan_mac,
|
||||
wan_ip: wan_ip,
|
||||
probed_ip: probed_ip
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
probe_ip: {
|
||||
call: function() {
|
||||
unlink(PROBE_FILE);
|
||||
// Run udhcpc on br-lan with a short timeout and our probe script
|
||||
// -n: exit if no lease
|
||||
// -q: quit after obtaining lease
|
||||
// -t 3: send 3 discover packets
|
||||
// -T 2: pause 2 seconds between packets
|
||||
let rc = system('udhcpc -i br-lan -n -q -t 3 -T 2 -s /usr/share/luci-app-ap-switch/dhcp-probe.sh >/dev/null 2>&1');
|
||||
|
||||
let ip = rtrim(readfile(PROBE_FILE) || '');
|
||||
return { success: (rc === 0), ip: ip };
|
||||
}
|
||||
},
|
||||
|
||||
set_mode: {
|
||||
args: { mode: 'string' },
|
||||
call: function(request) {
|
||||
let target_mode = request.args?.mode;
|
||||
if (target_mode !== 'router' && target_mode !== 'ap') {
|
||||
return { error: 'Invalid mode' };
|
||||
}
|
||||
|
||||
let uci_cursor = cursor();
|
||||
let current_mode = uci_cursor.get(UCI_CONFIG, 'settings', 'mode') || 'router';
|
||||
|
||||
if (target_mode === current_mode) {
|
||||
return { result: 'Already in target mode' };
|
||||
}
|
||||
|
||||
if (target_mode === 'ap') {
|
||||
// --- Switch to AP Mode (Full Lossless Snapshot) ---
|
||||
|
||||
// 1. Identify WAN device
|
||||
let wan_device = uci_cursor.get('network', 'wan', 'device');
|
||||
if (!wan_device) {
|
||||
wan_device = uci_cursor.get('network', 'wan', 'ifname');
|
||||
}
|
||||
|
||||
// 2. Full Snapshot Backup of Router Mode state
|
||||
let lan_proto = uci_cursor.get('network', 'lan', 'proto') || 'static';
|
||||
let lan_ipaddr = uci_cursor.get('network', 'lan', 'ipaddr') || '';
|
||||
let lan_netmask = uci_cursor.get('network', 'lan', 'netmask') || '';
|
||||
let lan_gateway = uci_cursor.get('network', 'lan', 'gateway') || '';
|
||||
let lan_dns = uci_cursor.get('network', 'lan', 'dns') || [];
|
||||
let dhcp_ignore = uci_cursor.get('dhcp', 'lan', 'ignore') || '0';
|
||||
let wan_disabled = uci_cursor.get('network', 'wan', 'disabled') || '0';
|
||||
let wan6_disabled = uci_cursor.get('network', 'wan6', 'disabled') || '0';
|
||||
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_proto', lan_proto);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_ipaddr', lan_ipaddr);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_netmask', lan_netmask);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_gateway', lan_gateway);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'lan_dns', lan_dns);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'dhcp_ignore', dhcp_ignore);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'wan_device', wan_device || '');
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'wan_disabled', wan_disabled);
|
||||
uci_cursor.set(UCI_CONFIG, 'router_state', 'wan6_disabled', wan6_disabled);
|
||||
uci_cursor.set(UCI_CONFIG, 'settings', 'mode', 'ap');
|
||||
|
||||
// 3. Bridge WAN device to LAN bridge
|
||||
if (wan_device) {
|
||||
uci_cursor.foreach('network', 'device', (s) => {
|
||||
if (s.name === 'br-lan') {
|
||||
let ports = s.ports || [];
|
||||
if (type(ports) === 'string') ports = [ports];
|
||||
let exists = false;
|
||||
for (let p in ports) if (p === wan_device) exists = true;
|
||||
if (!exists) {
|
||||
push(ports, wan_device);
|
||||
uci_cursor.set('network', s['.name'], 'ports', ports);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Set LAN to DHCP client
|
||||
uci_cursor.set('network', 'lan', 'proto', 'dhcp');
|
||||
uci_cursor.delete('network', 'lan', 'ipaddr');
|
||||
uci_cursor.delete('network', 'lan', 'netmask');
|
||||
uci_cursor.delete('network', 'lan', 'gateway');
|
||||
uci_cursor.delete('network', 'lan', 'dns');
|
||||
|
||||
// 5. Disable WAN interface
|
||||
uci_cursor.set('network', 'wan', 'disabled', '1');
|
||||
uci_cursor.set('network', 'wan6', 'disabled', '1');
|
||||
|
||||
// 6. Disable DHCP Server
|
||||
uci_cursor.set('dhcp', 'lan', 'ignore', '1');
|
||||
|
||||
} else {
|
||||
// --- Switch to Router Mode (Lossless State Restoration) ---
|
||||
|
||||
// 1. Read Snapshot
|
||||
let wan_device = uci_cursor.get(UCI_CONFIG, 'router_state', 'wan_device');
|
||||
let lan_proto = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_proto') || 'static';
|
||||
let lan_ipaddr = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_ipaddr') || '192.168.1.1';
|
||||
let lan_netmask = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_netmask') || '';
|
||||
let lan_gateway = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_gateway') || '';
|
||||
let lan_dns = uci_cursor.get(UCI_CONFIG, 'router_state', 'lan_dns') || [];
|
||||
let dhcp_ignore = uci_cursor.get(UCI_CONFIG, 'router_state', 'dhcp_ignore') || '0';
|
||||
let wan_disabled = uci_cursor.get(UCI_CONFIG, 'router_state', 'wan_disabled') || '0';
|
||||
let wan6_disabled = uci_cursor.get(UCI_CONFIG, 'router_state', 'wan6_disabled') || '0';
|
||||
|
||||
uci_cursor.set(UCI_CONFIG, 'settings', 'mode', 'router');
|
||||
|
||||
// 2. Remove WAN device from LAN bridge
|
||||
if (wan_device) {
|
||||
uci_cursor.foreach('network', 'device', (s) => {
|
||||
if (s.name === 'br-lan') {
|
||||
let ports = s.ports || [];
|
||||
if (type(ports) === 'string') ports = [ports];
|
||||
let new_ports = [];
|
||||
for (let p in ports) if (p !== wan_device) push(new_ports, p);
|
||||
uci_cursor.set('network', s['.name'], 'ports', new_ports);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Restore LAN settings accurately without hardcoding
|
||||
uci_cursor.set('network', 'lan', 'proto', lan_proto);
|
||||
if (lan_ipaddr) uci_cursor.set('network', 'lan', 'ipaddr', lan_ipaddr);
|
||||
if (lan_netmask) uci_cursor.set('network', 'lan', 'netmask', lan_netmask);
|
||||
if (lan_gateway) uci_cursor.set('network', 'lan', 'gateway', lan_gateway);
|
||||
if (type(lan_dns) === 'array' && length(lan_dns) > 0) {
|
||||
uci_cursor.set('network', 'lan', 'dns', lan_dns);
|
||||
} else if (type(lan_dns) === 'string' && lan_dns !== '') {
|
||||
uci_cursor.set('network', 'lan', 'dns', lan_dns);
|
||||
}
|
||||
|
||||
// 4. Restore WAN / WAN6 states
|
||||
if (wan_disabled === '1') {
|
||||
uci_cursor.set('network', 'wan', 'disabled', '1');
|
||||
} else {
|
||||
uci_cursor.delete('network', 'wan', 'disabled');
|
||||
}
|
||||
if (wan6_disabled === '1') {
|
||||
uci_cursor.set('network', 'wan6', 'disabled', '1');
|
||||
} else {
|
||||
uci_cursor.delete('network', 'wan6', 'disabled');
|
||||
}
|
||||
|
||||
// 5. Restore DHCP Server state
|
||||
if (dhcp_ignore === '1') {
|
||||
uci_cursor.set('dhcp', 'lan', 'ignore', '1');
|
||||
} else {
|
||||
uci_cursor.delete('dhcp', 'lan', 'ignore');
|
||||
}
|
||||
}
|
||||
|
||||
uci_cursor.commit('ap-switch');
|
||||
uci_cursor.commit('network');
|
||||
uci_cursor.commit('dhcp');
|
||||
|
||||
// Apply changes in background
|
||||
system('/etc/init.d/network restart &');
|
||||
system('/etc/init.d/dnsmasq restart &');
|
||||
|
||||
return { success: true, result: 'success' };
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return { 'luci.ap-switch': methods };
|
||||
@@ -1,515 +0,0 @@
|
||||
#chartjs-tooltip {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
background: rgba(0, 0, 0, .7);
|
||||
color: white;
|
||||
padding: 3px;
|
||||
border-radius: 3px;
|
||||
transition: all .1s ease;
|
||||
pointer-events: none;
|
||||
transform: translate(-50%, 0);
|
||||
z-index: 20000;
|
||||
}
|
||||
|
||||
#chartjs-tooltip.above {
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
#chartjs-tooltip.above:before {
|
||||
border: solid;
|
||||
border-color: #111 transparent;
|
||||
border-color: rgba(0, 0, 0, .8) transparent;
|
||||
border-width: 8px 8px 0 8px;
|
||||
bottom: 1em;
|
||||
content: "";
|
||||
display: block;
|
||||
left: 50%;
|
||||
top: 100%;
|
||||
position: absolute;
|
||||
z-index: 99;
|
||||
transform: translate(-50%, 0);
|
||||
}
|
||||
|
||||
.pie label {
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.kpi ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.kpi li {
|
||||
margin: 10px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.kpi big {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.head {
|
||||
text-align: center;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.head .pie {
|
||||
/* min-width: 200px; */
|
||||
padding: 5px;
|
||||
flex: 1 1 30%;
|
||||
}
|
||||
|
||||
.cbi-tooltip .head .pie {
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.head .kpi {
|
||||
padding: 5px;
|
||||
font-size: smaller;
|
||||
text-align: left;
|
||||
align-self: center;
|
||||
flex: 1 0 33%;
|
||||
min-width: 150px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.head .kpi ul {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.td.double > span {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.cbi-tooltip {
|
||||
box-shadow: 0 0 5px #000;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 992px) {
|
||||
.td.hide-xs {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span {
|
||||
white-space: nowrap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span:first-child::before {
|
||||
content: "IPv4: ";
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.td.double:not(.hide-xs) > span:last-child::before {
|
||||
content: "IPv6: ";
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
align-items: center; /* 垂直居中 */
|
||||
margin-bottom: 15px; /* 项间距 */
|
||||
gap: 10px; /* Label 和 Input 间距 */
|
||||
}
|
||||
|
||||
/* 标签固定最小宽度 + 右对齐 */
|
||||
.form-label {
|
||||
flex: 0 0 120px; /* 不伸缩、不收缩、基础宽度120px */
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.th-sort-asc::after {
|
||||
content: " ▲";
|
||||
}
|
||||
|
||||
.th-sort-desc::after {
|
||||
content: " ▼";
|
||||
}
|
||||
|
||||
.table .th {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.table .th:hover {
|
||||
background-color: #f0f0f0;
|
||||
}
|
||||
|
||||
/* L7 View Specific Styles */
|
||||
.l7-view-container #l7-error-message { color: red; background-color: #ffefef; border: 1px solid red; padding: 10px; margin-bottom: 10px; display: none; }
|
||||
.l7-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; }
|
||||
.l7-view-container .line-chart-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .kpi-card { background-color: #f9f9f9; border-radius: 8px; padding: 15px; text-align: center; border: 1px solid #e0e0e0; }
|
||||
.l7-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: #3771c8; }
|
||||
.l7-view-container .kpi-card-label { font-size: 0.9em; color: #666; }
|
||||
.l7-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||
.l7-view-container .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0; }
|
||||
.l7-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; }
|
||||
.l7-view-container .l7-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
color: white;
|
||||
}
|
||||
.l7-view-container .l7-controls-left, .l7-view-container .l7-controls-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Display View Specific Styles */
|
||||
.display-view-container .dashboard-container { display: flex; flex-direction: column; gap: 20px; margin-bottom: 20px; }
|
||||
.display-view-container .kpi-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 20px; }
|
||||
.display-view-container .kpi-card { background-color: #f9f9f9; border-radius: 8px; padding: 15px; text-align: center; border: 1px solid #e0e0e0; }
|
||||
.display-view-container .kpi-card big { display: block; font-size: 1.8em; font-weight: bold; color: #3771c8; }
|
||||
.display-view-container .kpi-card-label { font-size: 0.9em; color: #666; }
|
||||
.display-view-container .chart-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; }
|
||||
.display-view-container .chart-card { background-color: #ffffff; border-radius: 8px; padding: 20px; border: 1px solid #e0e0e0; }
|
||||
.display-view-container .chart-card h4 { margin-top: 0; margin-bottom: 15px; text-align: center; font-size: 1.1em; }
|
||||
.display-view-container .display-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Control Groups */
|
||||
.control-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 6px;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.control-icon {
|
||||
font-size: 1.2em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.control-label {
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.control-input {
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
padding: 6px 12px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.control-input:focus {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.control-buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.status-group {
|
||||
background-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.last-updated-text {
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Enhanced Select Styling */
|
||||
.cbi-input-select {
|
||||
padding: 6px 30px 6px 12px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
background-color: rgba(255, 255, 255, 0.9);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23667eea' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 8px center;
|
||||
background-size: 12px;
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cbi-input-select:hover {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.cbi-input-select:focus {
|
||||
background-color: white;
|
||||
border-color: #667eea;
|
||||
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.2);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Button Enhancements for Controls */
|
||||
.l7-controls .cbi-button,
|
||||
.display-controls .cbi-button {
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
font-weight: 500;
|
||||
transition: all 0.3s ease;
|
||||
border: 2px solid transparent;
|
||||
}
|
||||
|
||||
.cbi-button-action {
|
||||
background: rgba(255, 193, 7, 0.9);
|
||||
color: #333;
|
||||
border-color: rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
.cbi-button-action:hover {
|
||||
background: #ffc107;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(255, 193, 7, 0.4);
|
||||
}
|
||||
|
||||
.cbi-button-positive {
|
||||
background: rgba(40, 167, 69, 0.9);
|
||||
color: white;
|
||||
border-color: rgba(40, 167, 69, 0.3);
|
||||
}
|
||||
|
||||
.cbi-button-positive:hover {
|
||||
background: #28a745;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 8px rgba(40, 167, 69, 0.4);
|
||||
}
|
||||
|
||||
/* Responsive Controls */
|
||||
@media screen and (max-width: 768px) {
|
||||
.l7-controls,
|
||||
.display-controls {
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.l7-controls-left,
|
||||
.l7-controls-right,
|
||||
.control-buttons {
|
||||
width: 100%;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.control-group {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
/* Table Cell Icon Styles */
|
||||
.activity-indicator {
|
||||
font-size: 12px;
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.icon, .btn-icon, .th-icon {
|
||||
display: inline-block;
|
||||
font-style: normal;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.th-icon {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Cell Specific Styles */
|
||||
.sid-cell, .id-cell, .host-cell, .hostname-cell, .protocol-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.speed-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
font-weight: 500;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
.speed-cell.download {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.speed-cell.upload {
|
||||
color: #007bff;
|
||||
}
|
||||
|
||||
.volume-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
min-width: 90px;
|
||||
}
|
||||
|
||||
.volume-cell.download {
|
||||
color: #17a2b8;
|
||||
}
|
||||
|
||||
.volume-cell.upload {
|
||||
color: #6610f2;
|
||||
}
|
||||
|
||||
.packet-cell {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
color: #6c757d;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
/* Data value with monospace font for better alignment */
|
||||
.data-value {
|
||||
font-family: 'Courier New', Consolas, monospace;
|
||||
font-size: 0.95em;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Table header enhancements */
|
||||
.table .th {
|
||||
font-weight: 600;
|
||||
padding: 10px 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.table .td {
|
||||
padding: 8px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.protocol-icon.l7::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #5470c6;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.protocol-icon.domain::before {
|
||||
content: '';
|
||||
display: inline-block;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background-color: #91cc75;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
/* Button Enhancements */
|
||||
.cbi-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.cbi-button:hover {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.cbi-button-edit {
|
||||
background-color: #ffc107;
|
||||
border-color: #ffc107;
|
||||
}
|
||||
|
||||
.cbi-button-edit:hover {
|
||||
background-color: #e0a800;
|
||||
border-color: #e0a800;
|
||||
}
|
||||
|
||||
.cbi-button-remove {
|
||||
background-color: #dc3545;
|
||||
border-color: #dc3545;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cbi-button-remove:hover {
|
||||
background-color: #c82333;
|
||||
border-color: #bd2130;
|
||||
}
|
||||
|
||||
.cbi-button-add {
|
||||
background-color: #28a745;
|
||||
border-color: #28a745;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.cbi-button-add:hover:not(:disabled) {
|
||||
background-color: #218838;
|
||||
border-color: #1e7e34;
|
||||
}
|
||||
|
||||
.cbi-button-add:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Table Row Hover Effects */
|
||||
.table .tr:not(.table-titles):not(.placeholder):hover {
|
||||
background-color: #f8f9fa;
|
||||
transition: background-color 0.2s ease;
|
||||
}
|
||||
|
||||
/* Active/Inactive Status Styles */
|
||||
.activity-indicator[title*="Active"] {
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
/* Responsive Icon Handling */
|
||||
@media screen and (max-width: 768px) {
|
||||
.icon, .btn-icon {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.btn-icon + span {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.th-icon {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
@@ -30,219 +30,6 @@ var callGetWanMac = rpc.declare({
|
||||
expect: { result: {} }
|
||||
});
|
||||
|
||||
// Generate Device ID: AW + 7 random digits + 12-char MAC (total 21 chars)
|
||||
function generateDeviceId(macAddress) {
|
||||
var macPart = macAddress.replace(/-/g, ''); // Remove hyphens from MAC
|
||||
var randomPart = '';
|
||||
for (var i = 0; i < 7; i++) {
|
||||
randomPart += Math.floor(Math.random() * 10);
|
||||
}
|
||||
return 'AW' + randomPart + macPart;
|
||||
}
|
||||
|
||||
// Generate Location ID: 14 digits (AAAAAABBBCCCCC)
|
||||
// AAAAAA: Administrative division code (GB/T 2260) - using Beijing as default (110101)
|
||||
// BBB: Service type (100 for commercial internet service locations, positions 7-9)
|
||||
// CCCCC: 5-digit random sequence number (positions 10-14)
|
||||
function generateLocationId() {
|
||||
var adminCode = '110101'; // Default to Beijing Dongcheng District (positions 1-6)
|
||||
var serviceType = '100'; // Commercial internet service location (positions 7-9)
|
||||
var sequenceNum = '';
|
||||
|
||||
// Generate 5-digit random sequence number (positions 10-14)
|
||||
for (var i = 0; i < 5; i++) {
|
||||
sequenceNum += Math.floor(Math.random() * 10);
|
||||
}
|
||||
|
||||
return adminCode + serviceType + sequenceNum; // 6 + 3 + 5 = 14 digits
|
||||
}
|
||||
|
||||
// Auto-fill helper functions
|
||||
function showAutoFillModal() {
|
||||
ui.showModal(_('Auto Fill'), [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-descr' }, _('Getting device information and location data, please wait...')),
|
||||
E('div', { 'id': 'auto-fill-progress' }, [
|
||||
E('ul', { 'style': 'margin: 10px 0;' }, [
|
||||
E('li', { 'id': 'mac-status' }, _('Getting WAN MAC address...')),
|
||||
E('li', { 'id': 'device-id-status' }, _('Generating Device ID...')),
|
||||
E('li', { 'id': 'location-id-status' }, _('Generating Location ID...')),
|
||||
E('li', { 'id': 'location-status' }, _('Getting location coordinates...'))
|
||||
])
|
||||
])
|
||||
])
|
||||
]);
|
||||
}
|
||||
|
||||
function updateMacStatus(macAddr, isSuccess, errorMsg, isFallback) {
|
||||
var statusElement = document.getElementById('mac-status');
|
||||
if (isSuccess) {
|
||||
var prefix = isFallback ? _('✓ WAN MAC address obtained (fallback): ') : _('✓ WAN MAC address obtained: ');
|
||||
var suffix = isFallback ? '' : _(' (interface: ') + arguments[4] + _(')');
|
||||
statusElement.innerHTML = prefix + macAddr + suffix;
|
||||
} else {
|
||||
statusElement.innerHTML = _('✗ Failed to get WAN MAC address: ') + (errorMsg || _('Unknown error'));
|
||||
}
|
||||
}
|
||||
|
||||
function generateAndUpdateIds(macAddr) {
|
||||
var deviceId = generateDeviceId(macAddr);
|
||||
var locationId = generateLocationId();
|
||||
|
||||
document.getElementById('device-id-status').innerHTML = _('✓ Device ID generated: ') + deviceId;
|
||||
document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + locationId;
|
||||
|
||||
return { deviceId: deviceId, locationId: locationId };
|
||||
}
|
||||
|
||||
function getMacAddressPromise(results) {
|
||||
return callGetWanMac().then(function (response) {
|
||||
if (response && response.status === 'success') {
|
||||
var macAddr = response.mac.toUpperCase().replace(/:/g, '-');
|
||||
results.macAddress = macAddr;
|
||||
updateMacStatus(macAddr, true, null, false, response.interface);
|
||||
|
||||
var ids = generateAndUpdateIds(macAddr);
|
||||
results.deviceId = ids.deviceId;
|
||||
results.locationId = ids.locationId;
|
||||
} else {
|
||||
return getMacAddressFallback(results);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
return getMacAddressFallback(results);
|
||||
});
|
||||
}
|
||||
|
||||
function getMacAddressFallback(results) {
|
||||
document.getElementById('mac-status').innerHTML = _('RPC failed, trying fallback...');
|
||||
|
||||
return fs.exec('/bin/sh', ['-c', 'ip route | grep default | awk \'{print $5}\' | head -n1 | xargs -I {} cat /sys/class/net/{}/address 2>/dev/null || echo "failed"']).then(function (response) {
|
||||
if (response.code === 0 && response.stdout && response.stdout.trim() !== 'failed') {
|
||||
var macAddr = response.stdout.trim().toUpperCase().replace(/:/g, '-');
|
||||
results.macAddress = macAddr;
|
||||
updateMacStatus(macAddr, true, null, true);
|
||||
|
||||
var ids = generateAndUpdateIds(macAddr);
|
||||
results.deviceId = ids.deviceId;
|
||||
results.locationId = ids.locationId;
|
||||
} else {
|
||||
updateMacStatus(null, false, _('Unknown error'));
|
||||
document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID without MAC address');
|
||||
}
|
||||
}).catch(function (error) {
|
||||
updateMacStatus(null, false, error.message);
|
||||
document.getElementById('device-id-status').innerHTML = _('✗ Cannot generate Device ID due to MAC error');
|
||||
});
|
||||
}
|
||||
|
||||
function formatCoordinate(coord) {
|
||||
var num = parseFloat(coord).toFixed(6);
|
||||
if (num >= 0) {
|
||||
return num.padStart(10, '0');
|
||||
} else {
|
||||
return '-' + Math.abs(num).toFixed(6).padStart(9, '0');
|
||||
}
|
||||
}
|
||||
|
||||
function getLocationPromise(results) {
|
||||
return callGetLocation().then(function (response) {
|
||||
if (response && response.status === 'success') {
|
||||
var lat = formatCoordinate(response.lat);
|
||||
var lon = formatCoordinate(response.lon);
|
||||
|
||||
results.longitude = lon;
|
||||
results.latitude = lat;
|
||||
document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + lat + ', ' + lon;
|
||||
} else {
|
||||
var errorMsg = (response && response.message) ? response.message : _('Unknown error');
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to get location: ') + errorMsg;
|
||||
}
|
||||
}).catch(function (error) {
|
||||
return getLocationFallback(results);
|
||||
});
|
||||
}
|
||||
|
||||
function getLocationFallback(results) {
|
||||
document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...');
|
||||
|
||||
return fs.exec('/bin/sh', ['-c', 'curl -s --connect-timeout 10 --max-time 30 "https://ipapi.co/json" 2>/dev/null || curl -s --connect-timeout 10 --max-time 30 "http://ipinfo.io/json" 2>/dev/null || echo "failed"']).then(function (response) {
|
||||
if (response.code === 0 && response.stdout && response.stdout.trim() !== 'failed') {
|
||||
try {
|
||||
var data = JSON.parse(response.stdout);
|
||||
var lat, lon;
|
||||
|
||||
if (data.latitude && data.longitude) {
|
||||
lat = formatCoordinate(data.latitude);
|
||||
lon = formatCoordinate(data.longitude);
|
||||
} else if (data.loc) {
|
||||
var coords = data.loc.split(',');
|
||||
lat = formatCoordinate(coords[0]);
|
||||
lon = formatCoordinate(coords[1]);
|
||||
}
|
||||
|
||||
if (lat && lon) {
|
||||
results.longitude = lon;
|
||||
results.latitude = lat;
|
||||
document.getElementById('location-status').innerHTML = _('✓ Location obtained (fallback): ') + lat + ', ' + lon;
|
||||
} else {
|
||||
document.getElementById('location-status').innerHTML = _('✗ No valid coordinates found');
|
||||
}
|
||||
} catch (e) {
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to parse location data');
|
||||
}
|
||||
} else {
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to get location data');
|
||||
}
|
||||
}).catch(function (fallbackError) {
|
||||
document.getElementById('location-status').innerHTML = _('✗ Error getting location: ') + fallbackError.message;
|
||||
});
|
||||
}
|
||||
|
||||
function fillFormFields(results) {
|
||||
var successCount = 0;
|
||||
var totalFields = 0;
|
||||
var messages = [];
|
||||
var fieldMappings = [
|
||||
{ result: 'macAddress', selector: 'ap_mac_address', label: _('MAC Address: ') },
|
||||
{ result: 'deviceId', selector: 'ap_device_id', label: _('Device ID: ') },
|
||||
{ result: 'locationId', selector: 'location_id', label: _('Location ID: ') },
|
||||
{ result: 'longitude', selector: 'ap_longitude', label: _('Longitude: ') },
|
||||
{ result: 'latitude', selector: 'ap_latitude', label: _('Latitude: ') }
|
||||
];
|
||||
|
||||
fieldMappings.forEach(function (mapping) {
|
||||
if (results[mapping.result]) {
|
||||
var field = document.querySelector('input[data-name="' + mapping.selector + '"]') ||
|
||||
document.querySelector('input[name*="' + mapping.selector + '"]') ||
|
||||
document.querySelector('input[id*="' + mapping.selector + '"]') ||
|
||||
document.querySelector('#cbid\\.wifidogx\\.default\\.' + mapping.selector);
|
||||
|
||||
if (field) {
|
||||
field.value = results[mapping.result];
|
||||
field.dispatchEvent(new Event('input', { bubbles: true }));
|
||||
field.dispatchEvent(new Event('change', { bubbles: true }));
|
||||
successCount++;
|
||||
messages.push(mapping.label + results[mapping.result]);
|
||||
}
|
||||
totalFields++;
|
||||
}
|
||||
});
|
||||
|
||||
return { successCount: successCount, totalFields: totalFields, messages: messages };
|
||||
}
|
||||
|
||||
function showAutoFillResult(stats) {
|
||||
ui.hideModal();
|
||||
|
||||
if (stats.successCount === stats.totalFields && stats.totalFields > 0) {
|
||||
ui.addNotification(null, E('p', _('Auto fill completed successfully! All fields have been filled.') + '<br>' + stats.messages.join('<br>')), 'info');
|
||||
} else if (stats.successCount > 0) {
|
||||
ui.addNotification(null, E('p', _('Auto fill partially completed.') + ' ' + stats.successCount + '/' + stats.totalFields + ' ' + _('fields filled successfully.') + '<br>' + stats.messages.join('<br>')), 'warning');
|
||||
} else {
|
||||
ui.addNotification(null, E('p', _('Auto fill failed. No fields could be filled. Please check your network connection and try again.')), 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function getServiceStatus() {
|
||||
return L.resolveDefault(callServiceList('wifidogx'), {}).then(function (res) {
|
||||
var isRunning = false;
|
||||
@@ -253,6 +40,25 @@ function getServiceStatus() {
|
||||
});
|
||||
}
|
||||
|
||||
function generateDeviceId(macAddress) {
|
||||
var macPart = macAddress.replace(/-/g, '');
|
||||
var randomPart = '';
|
||||
for (var i = 0; i < 7; i++) {
|
||||
randomPart += Math.floor(Math.random() * 10);
|
||||
}
|
||||
return 'AW' + randomPart + macPart;
|
||||
}
|
||||
|
||||
function generateLocationId() {
|
||||
var adminCode = '110101';
|
||||
var serviceType = '100';
|
||||
var sequenceNum = '';
|
||||
for (var i = 0; i < 5; i++) {
|
||||
sequenceNum += Math.floor(Math.random() * 10);
|
||||
}
|
||||
return adminCode + serviceType + sequenceNum;
|
||||
}
|
||||
|
||||
function renderStatus(isRunning) {
|
||||
var renderHTML = "";
|
||||
var spanTemp = '<em><span style="color:%s"><strong>%s %s</strong></span></em>';
|
||||
@@ -284,7 +90,6 @@ return view.extend({
|
||||
s.tab('gateway', _('Gateway Settings'));
|
||||
s.tab('advanced', _('Advanced Settings'));
|
||||
s.tab('rule', _('Rule Settings'));
|
||||
s.tab('qos', _('QoS Settings'));
|
||||
s.tab('location', _('Authentication Location Settings'));
|
||||
|
||||
// basic settings
|
||||
@@ -368,7 +173,7 @@ return view.extend({
|
||||
o = s.taboption('basic', form.Flag, 'disable_portal_auth', _('Disable Portal Authentication'),
|
||||
_('When enabled, users can access the internet without portal authentication. Firewall redirect rules will not be created. Use this mode for pure traffic statistics without captive portal.'));
|
||||
o.rmempty = false;
|
||||
o.default = '1';
|
||||
o.default = '0';
|
||||
|
||||
o = s.taboption('basic', form.ListValue, 'log_level', _('Log Level'),
|
||||
_('The log level of the apfree-wifidog.'));
|
||||
@@ -380,7 +185,7 @@ return view.extend({
|
||||
o.value(2, _('Critical'));
|
||||
o.value(1, _('Alert'));
|
||||
o.value(0, _('Emergency'));
|
||||
o.defaulValue = 0;
|
||||
o.default = 0;
|
||||
o.optional = false;
|
||||
|
||||
// gateway settings
|
||||
@@ -392,7 +197,7 @@ return view.extend({
|
||||
o = ss.option(form.Flag, 'gateway_auth_enabled', _('Auth Enabled'),
|
||||
_('Enable the authentication of the gateway.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = true;
|
||||
o.default = true;
|
||||
|
||||
o = ss.option(widgets.DeviceSelect, 'gateway_name', _('Gateway Name'));
|
||||
o.rmempty = false;
|
||||
@@ -425,14 +230,14 @@ return view.extend({
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.defaulValue = 60;
|
||||
o.default = 60;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'client_timeout', _('Client Timeout'),
|
||||
_('The timeout of the client.'));
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.defaulValue = 5;
|
||||
o.default = 5;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'wired_passed', _('Wired Passed'),
|
||||
_('Wired users do not need to authenticate to access the internet.'));
|
||||
@@ -441,23 +246,30 @@ return view.extend({
|
||||
o = s.taboption('advanced', form.Flag, 'apple_cna', _('Apple CNA'),
|
||||
_('Enable Apple Captive Network Assistant.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = false;
|
||||
o.default = false;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'js_filter', _('JS Filter'),
|
||||
_('Enable JS redirect.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = true;
|
||||
o.default = true;
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'enable_anti_nat', _('Enable Anti NAT'),
|
||||
_('Enable Anti NAT devices.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = false;
|
||||
o.default = false;
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'privileged_ops_secret', _('Privileged Ops Secret'),
|
||||
_('Local secret required for AWAS cloud platform to dynamically authorize remote OTA, reboot and system changes. Change this from the default on production devices.'));
|
||||
o.password = true;
|
||||
o.rmempty = false;
|
||||
o.optional = false;
|
||||
o.default = 'chawrt@2026';
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'ttl_value', _('TTL Value'),
|
||||
_('The TTL value of the gateway support.'));
|
||||
o.datatype = 'string';
|
||||
o.rmempty = false;
|
||||
o.defaulValue = '64,128';
|
||||
o.default = '64,128';
|
||||
o.depends('enable_anti_nat', '1');
|
||||
|
||||
o = s.taboption('advanced', form.Value, 'anti_nat_permit_macs', _('Anti NAT Permit MAC'),
|
||||
@@ -466,17 +278,87 @@ return view.extend({
|
||||
o.rmempty = true;
|
||||
o.depends('enable_anti_nat', '1');
|
||||
|
||||
o = s.taboption('advanced', form.Flag, 'enable_event_log', _('Enable Internet Access Log'),
|
||||
_('Enable logging of internet access events and user activities.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = false;
|
||||
|
||||
// Auth Server Settings
|
||||
o = s.taboption('auth', form.SectionValue, '_auth', form.GridSection, 'auth');
|
||||
ss = o.subsection;
|
||||
ss.addremove = true;
|
||||
ss.nodescriptions = true;
|
||||
|
||||
ss.handleAdd = function (ev, name) {
|
||||
var self = this;
|
||||
if (name) {
|
||||
var longconnSections = uci.sections('wifidogx', 'longconn');
|
||||
for (var i = 0; i < longconnSections.length; i++) {
|
||||
if (longconnSections[i]['.name'] === name) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(name)), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
var authSections = uci.sections('wifidogx', 'auth');
|
||||
for (var i = 0; i < authSections.length; i++) {
|
||||
if (authSections[i]['.name'] === name) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(name)), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.super('handleAdd', [ev, name]);
|
||||
};
|
||||
|
||||
ss.handleRename = function (section_id, ev) {
|
||||
var self = this;
|
||||
var oldName = section_id;
|
||||
var nameEl = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'id': 'auth-rename-input',
|
||||
'value': oldName,
|
||||
'style': 'width: 100%;'
|
||||
});
|
||||
ui.showModal(_('Rename Authentication Server'), [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-descr' }, _('Enter a new unique name for this authentication server profile.')),
|
||||
E('div', { 'class': 'cbi-section-table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, _('Name')),
|
||||
E('div', { 'class': 'td' }, [nameEl])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': function () { ui.hideModal(); } }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function () {
|
||||
var newName = nameEl.value.trim();
|
||||
if (!newName) {
|
||||
ui.addNotification(null, E('p', _('Please enter a name.')), 'error');
|
||||
return;
|
||||
}
|
||||
if (newName === oldName) {
|
||||
ui.hideModal();
|
||||
return;
|
||||
}
|
||||
var longconnSections = uci.sections('wifidogx', 'longconn');
|
||||
for (var i = 0; i < longconnSections.length; i++) {
|
||||
if (longconnSections[i]['.name'] === newName) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(newName)), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
var authSections = uci.sections('wifidogx', 'auth');
|
||||
for (var i = 0; i < authSections.length; i++) {
|
||||
if (authSections[i]['.name'] === newName && authSections[i]['.name'] !== oldName) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(newName)), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
ui.hideModal();
|
||||
uci.rename('wifidogx', section_id, newName);
|
||||
self.map.save(null, true);
|
||||
}}, _('Rename'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
o = ss.option(form.Value, 'auth_server_hostname', _('Auth Server Hostname'),
|
||||
_('The domain or IP address of the authentication server.'));
|
||||
o.rmempty = false;
|
||||
@@ -499,29 +381,6 @@ return view.extend({
|
||||
o.optional = false;
|
||||
o.default = '/wifidog/';
|
||||
|
||||
|
||||
// QoS settings
|
||||
o = s.taboption('qos', form.Flag, 'enable_qos', _('Enable Global QoS'),
|
||||
_('Enable Global QoS.'));
|
||||
o.rmempty = false;
|
||||
o.defaulValue = false;
|
||||
|
||||
o = s.taboption('qos', form.Value, 'qos_up', _('Global QoS Up'),
|
||||
_('The global QoS up value(Mbps).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.defaulValue = 0;
|
||||
o.depends('enable_qos', '1');
|
||||
|
||||
o = s.taboption('qos', form.Value, 'qos_down', _('Global QoS Down'),
|
||||
_('The global QoS down value(Mbps).'));
|
||||
o.datatype = 'uinteger';
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.defaulValue = 0;
|
||||
o.depends('enable_qos', '1');
|
||||
|
||||
// Authentication Location Settings
|
||||
// Add auto-fill button
|
||||
o = s.taboption('location', form.Button, '_auto_fill', _('Auto Fill All Fields'),
|
||||
@@ -544,11 +403,8 @@ return view.extend({
|
||||
getLocationPromise(results)
|
||||
];
|
||||
|
||||
// Wait for all promises to complete and fill form fields
|
||||
Promise.all(promises).then(function () {
|
||||
setTimeout(function () {
|
||||
fillFormFields(results);
|
||||
}, 2000);
|
||||
fillFormFields(results);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -583,10 +439,11 @@ return view.extend({
|
||||
results.locationId = generateLocationId();
|
||||
document.getElementById('location-id-status').innerHTML = _('✓ Location ID generated: ') + results.locationId;
|
||||
} else {
|
||||
document.getElementById('mac-status').innerHTML = _('RPC returned error, trying fallback...');
|
||||
return getMacAddressFallback(results);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
document.getElementById('mac-status').innerHTML = _('✗ RPC call failed, trying fallback...');
|
||||
document.getElementById('mac-status').innerHTML = _('RPC call failed, trying fallback...');
|
||||
return getMacAddressFallback(results);
|
||||
});
|
||||
}
|
||||
@@ -625,8 +482,8 @@ return view.extend({
|
||||
formatAndSetCoordinates(results, lat, lon);
|
||||
document.getElementById('location-status').innerHTML = _('✓ Location obtained: ') + results.latitude + ', ' + results.longitude;
|
||||
} else {
|
||||
var errorMsg = (response && response.message) ? response.message : 'Unknown error';
|
||||
document.getElementById('location-status').innerHTML = _('✗ Failed to get location: ') + errorMsg;
|
||||
document.getElementById('location-status').innerHTML = _('RPC returned error, trying fallback...');
|
||||
return getLocationFallback(results);
|
||||
}
|
||||
}).catch(function (error) {
|
||||
document.getElementById('location-status').innerHTML = _('RPC failed, trying direct method...');
|
||||
@@ -741,8 +598,6 @@ return view.extend({
|
||||
o.rmempty = false;
|
||||
o.datatype = 'string';
|
||||
o.optional = false;
|
||||
o.depends('auth_server_mode', 'cloud');
|
||||
o.depends('auth_server_mode', 'bypass');
|
||||
o.placeholder = '110101100123456';
|
||||
o.validate = function (section_id, value) {
|
||||
if (!value || value === '')
|
||||
@@ -766,7 +621,8 @@ return view.extend({
|
||||
|
||||
// Validate service type code (7-9 digits)
|
||||
var serviceType = value.substring(6, 9);
|
||||
if (serviceType !== '100' && serviceType !== '2' + value.charAt(7) + value.charAt(8) && serviceType !== '3' + value.charAt(7) + value.charAt(8)) {
|
||||
var firstDigit = serviceType.charAt(0);
|
||||
if (serviceType !== '100' && firstDigit !== '2' && firstDigit !== '3') {
|
||||
return _('Service type code (7-9 digits) must be "100" for commercial internet service locations, "2XX" for non-commercial locations, or "3XX" for WiFi wireless collection terminals');
|
||||
}
|
||||
|
||||
@@ -936,6 +792,81 @@ return view.extend({
|
||||
ss.addremove = true;
|
||||
ss.nodescriptions = true;
|
||||
|
||||
ss.handleAdd = function (ev, name) {
|
||||
var self = this;
|
||||
if (name) {
|
||||
var authSections = uci.sections('wifidogx', 'auth');
|
||||
for (var i = 0; i < authSections.length; i++) {
|
||||
if (authSections[i]['.name'] === name) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(name)), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
var longconnSections = uci.sections('wifidogx', 'longconn');
|
||||
for (var i = 0; i < longconnSections.length; i++) {
|
||||
if (longconnSections[i]['.name'] === name) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(name)), 'error');
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
||||
return this.super('handleAdd', [ev, name]);
|
||||
};
|
||||
|
||||
ss.handleRename = function (section_id, ev) {
|
||||
var self = this;
|
||||
var oldName = section_id;
|
||||
var nameEl = E('input', {
|
||||
'type': 'text',
|
||||
'class': 'cbi-input-text',
|
||||
'id': 'longconn-rename-input',
|
||||
'value': oldName,
|
||||
'style': 'width: 100%;'
|
||||
});
|
||||
ui.showModal(_('Rename Long Connection Profile'), [
|
||||
E('div', { 'class': 'cbi-section' }, [
|
||||
E('div', { 'class': 'cbi-section-descr' }, _('Enter a new unique name for this long connection profile.')),
|
||||
E('div', { 'class': 'cbi-section-table' }, [
|
||||
E('div', { 'class': 'tr' }, [
|
||||
E('div', { 'class': 'td' }, _('Name')),
|
||||
E('div', { 'class': 'td' }, [nameEl])
|
||||
])
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': function () { ui.hideModal(); } }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': function () {
|
||||
var newName = nameEl.value.trim();
|
||||
if (!newName) {
|
||||
ui.addNotification(null, E('p', _('Please enter a name.')), 'error');
|
||||
return;
|
||||
}
|
||||
if (newName === oldName) {
|
||||
ui.hideModal();
|
||||
return;
|
||||
}
|
||||
var authSections = uci.sections('wifidogx', 'auth');
|
||||
for (var i = 0; i < authSections.length; i++) {
|
||||
if (authSections[i]['.name'] === newName) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Auth Server profiles. Please choose a different name.').format(newName)), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
var longconnSections = uci.sections('wifidogx', 'longconn');
|
||||
for (var i = 0; i < longconnSections.length; i++) {
|
||||
if (longconnSections[i]['.name'] === newName && longconnSections[i]['.name'] !== oldName) {
|
||||
ui.addNotification(null, E('p', _('The name "%s" already exists in Long Connection profiles. Please choose a different name.').format(newName)), 'error');
|
||||
return;
|
||||
}
|
||||
}
|
||||
ui.hideModal();
|
||||
uci.rename('wifidogx', section_id, newName);
|
||||
self.map.save(null, true);
|
||||
}}, _('Rename'))
|
||||
])
|
||||
]);
|
||||
};
|
||||
|
||||
o = ss.option(form.ListValue, 'long_conn_mode', _('Connection Mode'),
|
||||
_('The type of persistent connection to the remote management server.'));
|
||||
o.value('ws', _('WebSocket (ws://)'));
|
||||
@@ -1011,7 +942,7 @@ return view.extend({
|
||||
|
||||
// rule settings
|
||||
o = s.taboption('rule', form.DynamicList, 'trusted_wildcard_domains', _('Trusted Wildcard Domains'),
|
||||
_('The trusted wildcard domains of the gateway'));
|
||||
_('Requires aw-bpf DNS XDP (dns_ringbuf_portal). Patterns like .example.com; resolved IPs are added to the captive portal firewall (inet wifidogx), not inet awbpf.'));
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.datatype = 'wildcard';
|
||||
@@ -1025,7 +956,7 @@ return view.extend({
|
||||
o.placeholder = 'www.example.com';
|
||||
|
||||
o = s.taboption('rule', form.DynamicList, 'trusted_macs', _('Trusted MACs'),
|
||||
_('The trusted wildcard domains of the gateway.'));
|
||||
_('The trusted MAC addresses of the gateway.'));
|
||||
o.rmempty = true;
|
||||
o.optional = true;
|
||||
o.datatype = 'macaddr';
|
||||
|
||||
@@ -1,650 +0,0 @@
|
||||
'use strict';
|
||||
'require view';
|
||||
'require fs';
|
||||
'require ui';
|
||||
'require poll';
|
||||
'require rpc';
|
||||
'require dom';
|
||||
'require uci';
|
||||
|
||||
// Global variables from original display.js
|
||||
var chartRegistry = {};
|
||||
var hostNames = {}; // mac => hostname
|
||||
var hostInfo = {}; // ip => mac
|
||||
var hostNameMacSectionId = "";
|
||||
var isPaused = false;
|
||||
var lastUpdated = null;
|
||||
|
||||
// Line chart variables (from l7.js)
|
||||
var downloadLineChart = {}, uploadLineChart = {};
|
||||
var lineCategories = { ipv4: [], ipv6: [], mac: [] };
|
||||
var downloadSeriesData = { ipv4: {}, ipv6: {}, mac: {} };
|
||||
var uploadSeriesData = { ipv4: {}, ipv6: {}, mac: {} };
|
||||
|
||||
// Color palette for chart series
|
||||
var colorPalette = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4', '#ea7ccc'];
|
||||
|
||||
var resizeListenerAdded = false;
|
||||
|
||||
// Pre-fill with 60 empty points for a smooth start
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
for (var i = 0; i < 60; i++) {
|
||||
lineCategories[type].push('');
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to convert hex to rgba (from l7.js)
|
||||
function hexToRgba(hex, opacity) {
|
||||
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
return result ?
|
||||
'rgba(' + parseInt(result[1], 16) + ', ' + parseInt(result[2], 16) + ', ' + parseInt(result[3], 16) + ', ' + opacity + ')' :
|
||||
null;
|
||||
};
|
||||
|
||||
return view.extend({
|
||||
// --- Core Data Logic from display.js ---
|
||||
|
||||
loadHostNames: async function() {
|
||||
try {
|
||||
await uci.sections('hostnames', "hostname", function (params) {
|
||||
hostNameMacSectionId = params['.name'];
|
||||
for (var key in params) {
|
||||
if (key.startsWith('.')) continue;
|
||||
var macAddr = key.split('_').join(':');
|
||||
hostNames[macAddr] = params[key];
|
||||
}
|
||||
});
|
||||
|
||||
const dhcpLeases = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $2, $3, $4}', '/tmp/dhcp.leases'], 'text');
|
||||
dhcpLeases.split('\n').forEach(function(line) {
|
||||
if (line === '') return;
|
||||
const [mac, ip, hostname] = line.split(' ');
|
||||
if (!hostNames.hasOwnProperty(mac)) {
|
||||
hostNames[mac] = hostname;
|
||||
}
|
||||
});
|
||||
|
||||
const arp = await fs.exec_direct('/usr/bin/awk', ['-F', ' ', '{print $1, $4}', '/proc/net/arp'], 'text');
|
||||
arp.split('\n').forEach(function(line, i) {
|
||||
if (i === 0 || line === '') return;
|
||||
const [ip, mac] = line.split(' ');
|
||||
hostInfo[ip] = mac;
|
||||
});
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error getting host names:', e);
|
||||
}
|
||||
},
|
||||
|
||||
loadHostSpeedData: async function() {
|
||||
var self = this;
|
||||
try {
|
||||
const results = await Promise.all([
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv4', 'json'], 'json'),
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['ipv6', 'json'], 'json'),
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', ['mac', 'json'], 'json')
|
||||
]);
|
||||
|
||||
const defaultData = {status: "success", data: []};
|
||||
const ipv4Data = results[0] || defaultData;
|
||||
const ipv6Data = results[1] || defaultData;
|
||||
const macData = results[2] || defaultData;
|
||||
|
||||
ipv4Data.data.forEach(function(item) {
|
||||
const mac = hostInfo[item.ip];
|
||||
if (mac) {
|
||||
item.mac = mac;
|
||||
item.hostname = hostNames[mac];
|
||||
}
|
||||
});
|
||||
macData.data.forEach(function(item) {
|
||||
const mac = item.mac;
|
||||
if (mac) {
|
||||
item.hostname = hostNames[mac];
|
||||
}
|
||||
});
|
||||
|
||||
self.renderHostSpeed(ipv4Data, "ipv4");
|
||||
self.renderHostSpeed(ipv6Data, "ipv6");
|
||||
self.renderHostSpeed(macData, "mac");
|
||||
|
||||
lastUpdated = new Date();
|
||||
var timestampEl = document.getElementById('display-last-updated');
|
||||
if (timestampEl) {
|
||||
timestampEl.textContent = _('Last updated: %s').format(lastUpdated.toLocaleTimeString());
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
console.error('Error polling data:', e);
|
||||
}
|
||||
},
|
||||
|
||||
pollData: function() {
|
||||
poll.add(L.bind(async function() {
|
||||
if (isPaused) return;
|
||||
await this.loadHostNames();
|
||||
await this.loadHostSpeedData();
|
||||
}, this), 5);
|
||||
},
|
||||
|
||||
// --- UI Rendering and Interaction (New structure based on l7.js) ---
|
||||
|
||||
pie: function(id, data, valueFormatter) {
|
||||
var total = data.reduce(function(n, d) { return n + d.value; }, 0);
|
||||
data.sort(function(a, b) { return b.value - a.value; });
|
||||
|
||||
if (total === 0) {
|
||||
data = [{ value: 1, color: '#cccccc', name: _('no traffic') }];
|
||||
}
|
||||
|
||||
data.forEach(function(d, i) {
|
||||
if (!d.color) {
|
||||
var hue = (i * 137.508) % 360;
|
||||
d.color = 'hsl(' + hue + ', 75%, 55%)';
|
||||
}
|
||||
});
|
||||
|
||||
var option = {
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: function(params) {
|
||||
if (valueFormatter) {
|
||||
// 将 ECharts params 对象转换为自定义格式
|
||||
return valueFormatter({
|
||||
name: params.name,
|
||||
value: params.value,
|
||||
percent: params.percent.toFixed(2)
|
||||
});
|
||||
}
|
||||
return params.name + ': ' + params.value + ' (' + params.percent.toFixed(2) + '%)';
|
||||
}
|
||||
},
|
||||
series: [{
|
||||
type: 'pie',
|
||||
radius: ['25%', '80%'],
|
||||
avoidLabelOverlap: false,
|
||||
padAngle: 10,
|
||||
itemStyle: { borderRadius: 10, borderColor: '#fff', borderWidth: 2 },
|
||||
label: { show: false, position: 'center' },
|
||||
emphasis: { label: { show: true, fontSize: 14, fontWeight: 'bold' } },
|
||||
labelLine: { show: false },
|
||||
data: data.map(function(d) {
|
||||
return { value: d.value, name: d.label || d.name, itemStyle: { color: d.color } };
|
||||
})
|
||||
}]
|
||||
};
|
||||
|
||||
var dom = typeof id === 'string' ? document.getElementById(id) : id;
|
||||
if (!chartRegistry[id]) {
|
||||
chartRegistry[id] = echarts.init(dom);
|
||||
}
|
||||
chartRegistry[id].setOption(option, true);
|
||||
return chartRegistry[id];
|
||||
},
|
||||
|
||||
updateStackedLineCharts: function(type, perHostDownload, perHostUpload) {
|
||||
var now = new Date().toLocaleTimeString();
|
||||
lineCategories[type].push(now);
|
||||
lineCategories[type].shift();
|
||||
|
||||
var processChartData = function(seriesData, perHostData) {
|
||||
var allHosts = Object.keys(seriesData);
|
||||
Object.keys(perHostData).forEach(function(host) {
|
||||
if (allHosts.indexOf(host) === -1) {
|
||||
allHosts.push(host);
|
||||
}
|
||||
});
|
||||
|
||||
allHosts.forEach(function(host) {
|
||||
if (!seriesData[host]) {
|
||||
seriesData[host] = Array(59).fill(0);
|
||||
}
|
||||
var rate = perHostData[host] || 0;
|
||||
seriesData[host].push(rate);
|
||||
seriesData[host].shift();
|
||||
});
|
||||
|
||||
return Object.keys(seriesData).map(function(host, index) {
|
||||
var color = colorPalette[index % colorPalette.length];
|
||||
return {
|
||||
name: host,
|
||||
type: 'line',
|
||||
stack: 'Total',
|
||||
smooth: true,
|
||||
lineStyle: { width: 1, color: color },
|
||||
showSymbol: false,
|
||||
itemStyle: { color: color },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: hexToRgba(color, 0.5) },
|
||||
{ offset: 1, color: hexToRgba(color, 0) }
|
||||
])
|
||||
},
|
||||
data: seriesData[host]
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
var downloadChartSeries = processChartData(downloadSeriesData[type], perHostDownload);
|
||||
var uploadChartSeries = processChartData(uploadSeriesData[type], perHostUpload);
|
||||
|
||||
var legendData = downloadChartSeries.map(function(s) { return s.name; });
|
||||
|
||||
if (downloadLineChart[type]) {
|
||||
downloadLineChart[type].setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center' },
|
||||
series: downloadChartSeries,
|
||||
xAxis: { data: lineCategories[type] }
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadLineChart[type]) {
|
||||
uploadLineChart[type].setOption({
|
||||
legend: { data: legendData, type: 'scroll', top: 0, left: 'center' },
|
||||
series: uploadChartSeries,
|
||||
xAxis: { data: lineCategories[type] }
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderHostSpeed: function(data, type) {
|
||||
if (!data || data.status !== "success" || !Array.isArray(data.data)) return;
|
||||
|
||||
var rows = [];
|
||||
var txRateData = [], rxRateData = [];
|
||||
var txVolumeData = [], rxVolumeData = [];
|
||||
var tx_rate_total = 0, rx_rate_total = 0;
|
||||
var tx_bytes_total = 0, rx_bytes_total = 0;
|
||||
var perHostTxRate = {};
|
||||
var perHostRxRate = {};
|
||||
|
||||
data.data.forEach(item => {
|
||||
if (!item || !item.incoming || !item.outgoing) return;
|
||||
|
||||
var host = item.ip || item.mac || '';
|
||||
var hostname = item.hostname || hostNames[item.mac] || '';
|
||||
var displayName = hostname || host;
|
||||
|
||||
// 判断连接是否活跃
|
||||
var isActive = item.incoming.rate > 0 || item.outgoing.rate > 0;
|
||||
var activityIcon = isActive ? '🟢' : '⚪';
|
||||
|
||||
rows.push([
|
||||
E('span', { 'class': 'host-cell' }, [
|
||||
E('span', { 'class': 'activity-indicator', 'title': isActive ? _('Active') : _('Inactive') }, activityIcon),
|
||||
E('span', {}, ' ' + host)
|
||||
]),
|
||||
E('span', { 'class': 'hostname-cell' }, [
|
||||
E('span', { 'class': 'icon' }, hostname ? '👤' : '❓'),
|
||||
E('span', {}, ' ' + (hostname || _('Unknown')))
|
||||
]),
|
||||
E('span', { 'class': 'speed-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.incoming.rate))
|
||||
]),
|
||||
E('span', { 'class': 'volume-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.incoming.total_bytes))
|
||||
]),
|
||||
E('span', { 'class': 'packet-cell download' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.incoming.total_packets))
|
||||
]),
|
||||
E('span', { 'class': 'speed-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mBps'.format(item.outgoing.rate))
|
||||
]),
|
||||
E('span', { 'class': 'volume-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1024.2mB'.format(item.outgoing.total_bytes))
|
||||
]),
|
||||
E('span', { 'class': 'packet-cell upload' }, [
|
||||
E('span', { 'class': 'data-value' }, '%1000.2mP'.format(item.outgoing.total_packets))
|
||||
]),
|
||||
E('div', { 'class': 'button-container' }, [
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-edit',
|
||||
'style': 'margin-right: 5px;',
|
||||
'click': ui.createHandlerFn(this, () => this.handleEditSpeed(host, item.mac, hostname, type))
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '✏️'),
|
||||
E('span', {}, ' ' + _('Edit'))
|
||||
]),
|
||||
E('button', {
|
||||
'class': 'btn cbi-button cbi-button-remove',
|
||||
'click': ui.createHandlerFn(this, () => this.handleDeleteHost(host, type))
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '🗑️'),
|
||||
E('span', {}, ' ' + _('Delete'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
rx_rate_total += item.outgoing.rate;
|
||||
tx_rate_total += item.incoming.rate;
|
||||
rx_bytes_total += item.outgoing.total_bytes;
|
||||
tx_bytes_total += item.incoming.total_bytes;
|
||||
|
||||
rxRateData.push({ value: item.outgoing.rate, label: displayName });
|
||||
txRateData.push({ value: item.incoming.rate, label: displayName });
|
||||
rxVolumeData.push({ value: item.outgoing.total_bytes, label: displayName });
|
||||
txVolumeData.push({ value: item.incoming.total_bytes, label: displayName });
|
||||
|
||||
perHostTxRate[displayName] = (perHostTxRate[displayName] || 0) + item.incoming.rate;
|
||||
perHostRxRate[displayName] = (perHostRxRate[displayName] || 0) + item.outgoing.rate;
|
||||
});
|
||||
|
||||
this.updateStackedLineCharts(type, perHostTxRate, perHostRxRate);
|
||||
|
||||
var table = document.getElementById(type + '-speed-data');
|
||||
cbi_update_table(table, rows, E('em', _('No data recorded yet.')));
|
||||
|
||||
this.pie(type + '-tx-rate-pie', txRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-rx-rate-pie', rxRateData, (p) => `${p.name}: ${'%1024.2mBps'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-tx-volume-pie', txVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`);
|
||||
this.pie(type + '-rx-volume-pie', rxVolumeData, (p) => `${p.name}: ${'%1024.2mB'.format(p.value)} (${p.percent}%)`);
|
||||
|
||||
var hostEl = document.getElementById(type + '-host-val');
|
||||
if (hostEl) hostEl.textContent = data.data.length;
|
||||
|
||||
var txRateEl = document.getElementById(type + '-tx-rate-val');
|
||||
if (txRateEl) txRateEl.textContent = '%1024.2mBps'.format(tx_rate_total);
|
||||
|
||||
var rxRateEl = document.getElementById(type + '-rx-rate-val');
|
||||
if (rxRateEl) rxRateEl.textContent = '%1024.2mBps'.format(rx_rate_total);
|
||||
|
||||
var txVolEl = document.getElementById(type + '-tx-volume-val');
|
||||
if (txVolEl) txVolEl.textContent = '%1024.2mB'.format(tx_bytes_total);
|
||||
|
||||
var rxVolEl = document.getElementById(type + '-rx-volume-val');
|
||||
if (rxVolEl) rxVolEl.textContent = '%1024.2mB'.format(rx_bytes_total);
|
||||
},
|
||||
|
||||
// --- Interaction Handlers from display.js ---
|
||||
|
||||
handleDeleteHost: function(host, type) {
|
||||
ui.showModal(_('Delete Host'), [
|
||||
E('p', _('Are you sure you want to delete this host?')),
|
||||
E('div', { 'class': 'right' }, [
|
||||
E('button', { 'class': 'btn', 'click': ui.hideModal }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button-negative', 'click': ui.createHandlerFn(this, async () => {
|
||||
try {
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'del', host], 'text');
|
||||
this.loadHostSpeedData();
|
||||
ui.hideModal();
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
ui.hideModal();
|
||||
}
|
||||
})}, _('Delete'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
handleEditSpeed: function(host, mac, hostname, type) {
|
||||
fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'json'], 'json').then(L.bind(res => {
|
||||
let rate_limit_dl = 0, rate_limit_ul = 0;
|
||||
if (res && res.status === 'success' && Array.isArray(res.data)) {
|
||||
const item = res.data.find(d => (d.ip === host || d.mac === host));
|
||||
if (item) {
|
||||
rate_limit_dl = (item.incoming.incoming_rate_limit || 0) / 1024 / 1024;
|
||||
rate_limit_ul = (item.outgoing.outgoing_rate_limit || 0) / 1024 / 1024;
|
||||
}
|
||||
}
|
||||
this.displaySpeedLimitDialog(host, mac, hostname, type, rate_limit_dl, rate_limit_ul);
|
||||
}, this)).catch(e => {
|
||||
console.error('Error getting speed limit:', e);
|
||||
this.displaySpeedLimitDialog(host, mac, hostname, type, 0, 0);
|
||||
});
|
||||
},
|
||||
|
||||
displaySpeedLimitDialog: function(host, mac, hostname, type, dl, ul) {
|
||||
const inputDom = E('input', { type: 'text', id: 'host-name', class: 'cbi-input-text', value: hostname, disabled: !mac });
|
||||
|
||||
ui.showModal(_('Edit Speed Limit'), [
|
||||
E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Host')), E('span',{}, host) ]),
|
||||
E('div', { 'class': 'form-group' }, [ E('label', { 'class': 'form-label' }, _('Hostname')), inputDom ]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', { 'class': 'form-label' }, _('Download Limit')),
|
||||
E('input', { type: 'number', id: 'dl-rate', class: 'cbi-input-number', min: '0', value: dl }),
|
||||
E('span',{}, " Mbps")
|
||||
]),
|
||||
E('div', { 'class': 'form-group' }, [
|
||||
E('label', { 'class': 'form-label' }, _('Upload Limit')),
|
||||
E('input', { type: 'number', id: 'ul-rate', class: 'cbi-input-number', min: '0', value: ul }),
|
||||
E('span',{}, " Mbps")
|
||||
]),
|
||||
E('div', { 'class': 'cbi-page-actions right' }, [
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-neutral', 'click': ui.hideModal }, _('Cancel')),
|
||||
E('button', { 'class': 'btn cbi-button cbi-button-positive', 'click': ui.createHandlerFn(this, async ev => {
|
||||
const dl_val = document.getElementById('dl-rate').value;
|
||||
const ul_val = document.getElementById('ul-rate').value;
|
||||
const newName = document.getElementById('host-name').value;
|
||||
try {
|
||||
if (mac && newName !== hostname) {
|
||||
hostNames[mac] = newName;
|
||||
await uci.set('hostnames', hostNameMacSectionId, mac.split(':').join('_'), newName);
|
||||
await uci.save('hostnames');
|
||||
await uci.apply('hostnames');
|
||||
}
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'update', host, "downrate", dl_val*1024*1024 || '0', "uprate", ul_val*1024*1024 || '0']);
|
||||
this.loadHostSpeedData();
|
||||
ui.addNotification(null, E('p',_('Speed limit updated')));
|
||||
ui.hideModal();
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
}
|
||||
})}, _('Save'))
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
validateData: function(value, type) {
|
||||
if (typeof value !== 'string') return false;
|
||||
const ipv4Regex = /^((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$/;
|
||||
const ipv6Regex = /^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$|^(([0-9a-fA-F]{1,4}:){0,6}::([0-9a-fA-F]{1,4}:){0,6}[0-9a-fA-F]{1,4})$/i;
|
||||
const macRegex = /^([0-9A-Fa-f]{2}([-:]))([0-9A-Fa-f]{2}\2){4}[0-9A-Fa-f]{2}$|^([0-9A-Fa-f]{12})$/i;
|
||||
return (type === 'ipv4') ? ipv4Regex.test(value) : (type === 'ipv6') ? ipv6Regex.test(value) : macRegex.test(value);
|
||||
},
|
||||
|
||||
createAddControls: function(type, placeholder) {
|
||||
const input = E('input', {
|
||||
type: 'text',
|
||||
class: 'cbi-input-text control-input',
|
||||
style: (type === 'ipv6') ? 'width:320px' : 'width:180px',
|
||||
placeholder: _(placeholder)
|
||||
});
|
||||
const addBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-add',
|
||||
disabled: true
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '➕'),
|
||||
E('span', {}, ' ' + _('Add'))
|
||||
]);
|
||||
const refreshBtn = E('button', {
|
||||
class: 'btn cbi-button cbi-button-action',
|
||||
click: () => this.loadHostSpeedData()
|
||||
}, [
|
||||
E('span', { 'class': 'btn-icon' }, '🔄'),
|
||||
E('span', {}, ' ' + _('Refresh'))
|
||||
]);
|
||||
|
||||
input.addEventListener('input', () => { addBtn.disabled = (input.value.trim() === ''); });
|
||||
addBtn.addEventListener('click', ui.createHandlerFn(this, async () => {
|
||||
const value = input.value.trim();
|
||||
if (!this.validateData(value, type)) {
|
||||
return ui.addNotification(null, E('p', _('Data format error')));
|
||||
}
|
||||
try {
|
||||
await fs.exec_direct('/usr/bin/aw-bpfctl', [type, 'add', value]);
|
||||
this.loadHostSpeedData();
|
||||
ui.addNotification(null, E('p',_('Updated successfully!')));
|
||||
input.value = '';
|
||||
addBtn.disabled = true;
|
||||
} catch (e) {
|
||||
ui.addNotification(null, E('p', _('Error: ') + e.message));
|
||||
}
|
||||
}));
|
||||
|
||||
return E('div', { 'class': 'display-controls' }, [
|
||||
E('div', { 'class': 'control-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '🖥️'),
|
||||
E('label', { 'class': 'control-label' }, _('Add Host:')),
|
||||
input
|
||||
]),
|
||||
E('div', { 'class': 'control-buttons' }, [
|
||||
addBtn,
|
||||
refreshBtn,
|
||||
E('div', { 'class': 'control-group status-group' }, [
|
||||
E('span', { 'class': 'control-icon' }, '🕐'),
|
||||
E('span', { 'id': 'display-last-updated', 'class': 'last-updated-text' }, _('Ready'))
|
||||
])
|
||||
])
|
||||
]);
|
||||
},
|
||||
|
||||
initializeUI: function() {
|
||||
if (window.echarts) {
|
||||
var self = this;
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
var dlChartEl = document.getElementById(type + '-download-speed-line-chart');
|
||||
var ulChartEl = document.getElementById(type + '-upload-speed-line-chart');
|
||||
if (!dlChartEl || !ulChartEl) return;
|
||||
|
||||
var baseChartOption = {
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
formatter: function (params) {
|
||||
if (!params || params.length === 0) {
|
||||
return null;
|
||||
}
|
||||
var tooltipContent = params[0].axisValueLabel + '<br/>';
|
||||
params.sort(function(a, b) { return b.value - a.value; });
|
||||
params.forEach(function(item) {
|
||||
if (item.value > 0) {
|
||||
tooltipContent += item.marker + ' ' + item.seriesName + ': ' + '%1024.2mBps'.format(item.value) + '<br/>';
|
||||
}
|
||||
});
|
||||
return tooltipContent;
|
||||
}
|
||||
},
|
||||
grid: { left: '3%', right: '4%', bottom: '10%', top: '50px', containLabel: true },
|
||||
xAxis: { type: 'category', boundaryGap: false, data: lineCategories[type] },
|
||||
yAxis: { type: 'value', axisLabel: { formatter: function(val) { return '%1024.2mBps'.format(val); } } },
|
||||
series: []
|
||||
};
|
||||
|
||||
downloadLineChart[type] = echarts.init(dlChartEl);
|
||||
downloadLineChart[type].setOption(baseChartOption);
|
||||
|
||||
uploadLineChart[type] = echarts.init(ulChartEl);
|
||||
uploadLineChart[type].setOption(baseChartOption);
|
||||
});
|
||||
|
||||
// 添加窗口大小变化监听器,使图表能够响应式调整
|
||||
if (!resizeListenerAdded) {
|
||||
var resizeTimer = null;
|
||||
var resizeHandler = function() {
|
||||
// 使用防抖,避免频繁触发 resize
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer);
|
||||
}
|
||||
resizeTimer = setTimeout(function() {
|
||||
// 调整折线图大小
|
||||
['ipv4', 'ipv6', 'mac'].forEach(function(type) {
|
||||
if (downloadLineChart[type]) {
|
||||
downloadLineChart[type].resize();
|
||||
}
|
||||
if (uploadLineChart[type]) {
|
||||
uploadLineChart[type].resize();
|
||||
}
|
||||
});
|
||||
// 调整饼图大小
|
||||
Object.keys(chartRegistry).forEach(function(chartId) {
|
||||
if (chartRegistry[chartId]) {
|
||||
chartRegistry[chartId].resize();
|
||||
}
|
||||
});
|
||||
}, 200);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', resizeHandler);
|
||||
resizeListenerAdded = true;
|
||||
}
|
||||
|
||||
this.pollData();
|
||||
} else {
|
||||
setTimeout(this.initializeUI.bind(this), 50);
|
||||
}
|
||||
},
|
||||
|
||||
// --- Main Render Function (New) ---
|
||||
|
||||
render: function() {
|
||||
var self = this;
|
||||
|
||||
const createTab = (type, title, placeholder) => {
|
||||
return E('div', { 'class': 'cbi-section', 'data-tab': type, 'data-tab-title': _(title) }, [
|
||||
E('div', { 'class': 'dashboard-container' }, [
|
||||
E('div', { 'class': 'line-chart-row' }, [
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Download Speed')]),
|
||||
E('div', { id: type + '-download-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
]),
|
||||
E('div', { 'class': 'chart-card' }, [
|
||||
E('h4', [_('Real-time Upload Speed')]),
|
||||
E('div', { id: type + '-upload-speed-line-chart', style: 'width: 100%; height: 350px;' })
|
||||
])
|
||||
]),
|
||||
E('div', { 'class': 'kpi-row' }, [
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-host-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Hosts')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-rate-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Speed')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-tx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Download Total')) ]),
|
||||
E('div', { 'class': 'kpi-card' }, [ E('big', { id: type + '-rx-volume-val' }, '0'), E('span', { 'class': 'kpi-card-label' }, _('Upload Total')) ])
|
||||
]),
|
||||
E('div', { 'class': 'chart-grid' }, [
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Speed / Host')]), E('div', { id: type + '-tx-rate-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Speed / Host')]), E('div', { id: type + '-rx-rate-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Download Total')]), E('div', { id: type + '-tx-volume-pie', style: 'width:100%; height:300px;' }) ]),
|
||||
E('div', { 'class': 'chart-card' }, [ E('h4', [_('Upload Total')]), E('div', { id: type + '-rx-volume-pie', style: 'width:100%; height:300px;' }) ])
|
||||
])
|
||||
]),
|
||||
E('table', { 'class': 'table', 'id': type + '-speed-data' }, [
|
||||
E('tr', { 'class': 'tr table-titles' }, [
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '🖥️'), ' ', _('Host') ]),
|
||||
E('th', { 'class': 'th left' }, [ E('span', { 'class': 'th-icon' }, '👤'), ' ', _('Hostname') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬇️'), ' ', _('Download Speed') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Download Total') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Download Packets') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '⬆️'), ' ', _('Upload Speed') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📦'), ' ', _('Upload Total') ]),
|
||||
E('th', { 'class': 'th right' }, [ E('span', { 'class': 'th-icon' }, '📨'), ' ', _('Upload Packets') ]),
|
||||
E('th', { 'class': 'th center' }, [ E('span', { 'class': 'th-icon' }, '⚙️'), ' ', _('Actions') ])
|
||||
]),
|
||||
E('tr', { 'class': 'tr placeholder' }, [ E('td', { 'class': 'td', 'colspan': '9' }, [ E('em', { 'class': 'spinning' }, [ _('Collecting data...') ]) ]) ])
|
||||
]),
|
||||
self.createAddControls(type, placeholder)
|
||||
]);
|
||||
};
|
||||
|
||||
var tabContainer = E('div', {}, [
|
||||
createTab('ipv4', 'IPv4', 'Please enter a valid IPv4 address'),
|
||||
createTab('ipv6', 'IPv6', 'Please enter a valid IPv6 address'),
|
||||
createTab('mac', 'MAC', 'Please enter a valid MAC address')
|
||||
]);
|
||||
|
||||
var node = E([], [
|
||||
E('link', { 'rel': 'stylesheet', 'href': L.resource('view/wifidogx.css') }),
|
||||
E('script', { 'type': 'text/javascript', 'src': L.resource('echarts.min.js') }),
|
||||
E('div', { 'class': 'l7-view-container' }, [
|
||||
E('h2', [ _('Auth User Speed Monitor') ]),
|
||||
tabContainer
|
||||
])
|
||||
]);
|
||||
|
||||
ui.tabs.initTabGroup(tabContainer.childNodes);
|
||||
|
||||
setTimeout(() => this.initializeUI(), 0);
|
||||
|
||||
return node;
|
||||
},
|
||||
|
||||
handleSave: null,
|
||||
handleSaveApply: null,
|
||||
handleReset: null
|
||||
});
|
||||
@@ -266,14 +266,14 @@ return view.extend({
|
||||
}
|
||||
}
|
||||
|
||||
// Initial update
|
||||
updateStatus();
|
||||
|
||||
// Poll status every 5 seconds
|
||||
L.Poll.add(function() {
|
||||
return updateStatus();
|
||||
}, 5);
|
||||
|
||||
// Initial update
|
||||
updateStatus();
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user