🎁 Sync 2026-03-23 17:00:57

This commit is contained in:
github-actions[bot] 2026-03-23 17:00:57 +08:00
parent 477a1eee80
commit 0d4778f4d9
42 changed files with 244 additions and 2159 deletions

View File

@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=autoshare-ksmbd
PKG_VERSION:=1
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_ARCH:=all
include $(INCLUDE_DIR)/package.mk

View File

@ -1,12 +1,5 @@
#!/bin/sh
#
# D-Team Technology Co.,Ltd. ShenZhen
# 作者:Vic
#
# 警告:对着屏幕的哥们,我们允许你使用此脚本,但不允许你抹去作者的信息,请保留这段话。
#
. /lib/functions.sh
. /lib/functions/service.sh
@ -16,8 +9,9 @@ config_file="/etc/config/ksmbd"
[ ! -f /etc/config/ksmbd ] && {
echo "
config globals
option 'workgroup' 'WORKGROUP'
option 'description' 'Ksmbd on OpenWrt'
option workgroup 'WORKGROUP'
option charset 'UTF-8'
option description 'Ksmbd on OpenWRT'
" > $config_file
}
@ -32,21 +26,27 @@ wait_for_init() {
}
smb_handle() {
config_get path $1 path
if [ "$path" = "$2" ] ;then
global=1
local path
config_get path "$1" path
if [ -n "$path" ]; then
case "$path" in
"$2" | "$2"/*) global=1 ;;
esac
case "$2" in
"$path" | "$path"/*) global=1 ;;
esac
fi
}
chk_en() {
config_get_bool autoshare $1 autoshare 1
[ $autoshare -eq 0 ] && exit
config_get_bool autoshare "$1" autoshare 1
[ "$autoshare" -eq 0 ] && exit
}
config_load ksmbd
config_foreach chk_en ksmbd
device=`basename $DEVPATH`
device=`basename "$DEVPATH"`
case "$ACTION" in
add)
@ -55,6 +55,8 @@ case "$ACTION" in
sd*);;
md*);;
hd*);;
mmcblk*);;
nvme*);;
*) return;;
esac
@ -62,22 +64,21 @@ case "$ACTION" in
wait_for_init
[ "$(uci -q get ksmbd.@globals[0].autoshare)" == "1" ] && {
/etc/init.d/ksmbd restart
exit 0;
}
cat /proc/mounts | grep -v '/boot\|/opt' | while read j
grep -v '/boot\|/opt' /proc/mounts | while read -r j
do
str=${j%% *}
if [ "$str" == $path ];then
if [ "$str" = "$path" ];then
strr=${j#* }
target=${strr%% *}
global=0
config_foreach smb_handle share $target
config_load ksmbd
config_foreach smb_handle share "$target"
name=${target#*/mnt/}
name=$(echo $name | sed -e "s/^\///")
if [ $global -eq 0 ] ;then
name=$(echo "$name" | sed -e "s/^\///")
if [ "$global" -eq 0 ] ;then
echo -e "\n\nconfig share" >> $config_file
echo -e "\toption auto '1'" >> $config_file
echo -e "\toption name '$name'" >> $config_file
@ -93,27 +94,26 @@ case "$ACTION" in
fi
fi
done
uci -q set ksmbd.@globals[0].autoshare='1'
uci commit ksmbd
;;
remove)
i=0
while true
do
dev=`uci get ksmbd.@share[$i].device`
[ $? -ne 0 ] && break
if ! uci -q get ksmbd.@share[$i] >/dev/null; then
break
fi
[ "$dev" = "$device" ] && {
auto=`uci get ksmbd.@share[$i].auto`
[ $auto = "1" ] && {
mount_dir=`uci get ksmbd.@share[$i].name`
dev=$(uci -q get ksmbd.@share[$i].device)
if [ "$dev" = "$device" ]; then
auto=$(uci -q get ksmbd.@share[$i].auto)
if [ "$auto" = "1" ]; then
uci delete ksmbd.@share[$i]
uci commit
uci commit ksmbd
/etc/init.d/ksmbd reload
return
}
}
fi
fi
let i+=1
done
;;

View File

@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=autoshare-samba
PKG_VERSION:=1
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_ARCH:=all
include $(INCLUDE_DIR)/package.mk

View File

@ -1,12 +1,5 @@
#!/bin/sh
#
# D-Team Technology Co.,Ltd. ShenZhen
# 作者:Vic
#
# 警告:对着屏幕的哥们,我们允许你使用此脚本,但不允许你抹去作者的信息,请保留这段话。
#
. /lib/functions.sh
. /lib/functions/service.sh
@ -33,21 +26,27 @@ wait_for_init() {
}
smb_handle() {
config_get path $1 path
if [ "$path" = "$2" ] ;then
global=1
local path
config_get path "$1" path
if [ -n "$path" ]; then
case "$path" in
"$2" | "$2"/*) global=1 ;;
esac
case "$2" in
"$path" | "$path"/*) global=1 ;;
esac
fi
}
chk_en() {
config_get_bool autoshare $1 autoshare 1
[ $autoshare -eq 0 ] && exit
config_get_bool autoshare "$1" autoshare 1
[ "$autoshare" -eq 0 ] && exit
}
config_load samba4
config_foreach chk_en samba4
device=`basename $DEVPATH`
device=`basename "$DEVPATH"`
case "$ACTION" in
add)
@ -56,6 +55,8 @@ case "$ACTION" in
sd*);;
md*);;
hd*);;
mmcblk*);;
nvme*);;
*) return;;
esac
@ -63,22 +64,21 @@ case "$ACTION" in
wait_for_init
[ "$(uci -q get samba4.@samba[0].autoshare)" == "1" ] && {
/etc/init.d/samba4 restart
exit 0;
}
cat /proc/mounts | grep -v '/boot\|/opt' | while read j
grep -v '/boot\|/opt' /proc/mounts | while read -r j
do
str=${j%% *}
if [ "$str" == $path ];then
if [ "$str" = "$path" ];then
strr=${j#* }
target=${strr%% *}
global=0
config_foreach smb_handle sambashare $target
config_load samba4
config_foreach smb_handle sambashare "$target"
name=${target#*/mnt/}
name=$(echo $name | sed -e "s/^\///")
if [ $global -eq 0 ] ;then
name=$(echo "$name" | sed -e "s/^\///")
if [ "$global" -eq 0 ] ;then
echo -e "\n\nconfig sambashare" >> $config_file
echo -e "\toption auto '1'" >> $config_file
echo -e "\toption name '$name'" >> $config_file
@ -94,27 +94,26 @@ case "$ACTION" in
fi
fi
done
uci -q set samba4.@samba[0].autoshare='1'
uci commit samba4
;;
remove)
i=0
while true
do
dev=`uci get samba4.@sambashare[$i].device`
[ $? -ne 0 ] && break
if ! uci -q get samba4.@sambashare[$i] >/dev/null; then
break
fi
[ "$dev" = "$device" ] && {
auto=`uci get samba4.@sambashare[$i].auto`
[ $auto = "1" ] && {
mount_dir=`uci get samba4.@sambashare[$i].name`
dev=$(uci -q get samba4.@sambashare[$i].device)
if [ "$dev" = "$device" ]; then
auto=$(uci -q get samba4.@sambashare[$i].auto)
if [ "$auto" = "1" ]; then
uci delete samba4.@sambashare[$i]
uci commit
uci commit samba4
/etc/init.d/samba4 reload
return
}
}
fi
fi
let i+=1
done
;;

View File

@ -16,8 +16,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-amlogic
PKG_VERSION:=3.1.286
PKG_RELEASE:=1
PKG_VERSION:=3.1.287
PKG_RELEASE:=2
PKG_LICENSE:=GPL-2.0 License
PKG_MAINTAINER:=ophub <https://github.com/ophub/luci-app-amlogic>

View File

@ -159,6 +159,16 @@ else
[[ "${?}" -ne "0" ]] && tolog "02.05 Language pack [ ${langfile} ] download failed." "1"
done
# The .apk filename is preceded by a tilde (~) instead of a dot (.).
for file in ${TMP_CHECK_DIR}/*.apk; do
[[ -f "${file}" ]] || continue
base_name="$(basename "${file}")"
new_name="$(echo "${base_name}" | sed -E 's/\.([a-f0-9]{7}\.apk)/~\1/')"
if [[ "${base_name}" != "${new_name}" ]]; then
mv -f "${file}" "${TMP_CHECK_DIR}/${new_name}" || true
fi
done
sync && sleep 2
fi

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=0.1.5
PKG_RELEASE:=6
PKG_VERSION:=0.1.6
PKG_RELEASE:=7
LUCI_TITLE:=LuCI support for OpenClaw Launcher
LUCI_PKGARCH:=all

View File

@ -130,12 +130,14 @@
if (status.installing) return "安装中";
if (!status || !status.installed) return "未安装";
if (status.running) return "运行中";
if (status.reachable) return "运行中(未托管)";
return "已停止";
}
function statusDotClass(status) {
if (status && status.installing) return "";
if (status && status.running) return "is-success";
if (status && status.reachable) return "";
return "is-danger";
}
@ -382,11 +384,8 @@
var savingAi = state.savingSection === "ai";
var savingAccess = state.savingSection === "access";
var serviceButtons = showServiceActions ? (
(!status.running ? '<button class="oclm-button" type="button" data-op="start">启动服务</button>' : '') +
(status.running ? '<button class="oclm-button" type="button" data-op="stop">停止服务</button>' : '') +
'<button class="oclm-button" type="button" data-op="restart">重启服务</button>'
) : '';
var canStartService = !status.running && !status.reachable;
var canStopService = !!status.running;
root.innerHTML =
'<style>' + styleText + '</style>' +
@ -415,7 +414,7 @@
'<button id="oclm-install-btn" class="oclm-button oclm-button-primary" type="button" data-install-action="1"' + (status.installing ? ' disabled' : '') + '>' + installLabel + '</button>' +
'<label class="oclm-check"><input type="checkbox" id="oclm-install-accelerated"' + (installAcceleratedChecked ? ' checked' : '') + ' />Kspeeder 加速安装</label>' +
'</div>' +
'<button id="oclm-open-console" class="oclm-button oclm-button-primary' + (status.running ? '' : ' oclm-hidden') + '" type="button" data-open-console="1"' + (state.consoleReady ? '' : ' disabled') + '>' + openclawIcon("oclm-button-icon") + (state.consoleReady ? '打开控制台' : '控制台准备中…') + '</button>' +
'<button id="oclm-open-console" class="oclm-button oclm-button-primary' + ((status.running || status.reachable) ? '' : ' oclm-hidden') + '" type="button" data-open-console="1"' + (state.consoleReady ? '' : ' disabled') + '>' + openclawIcon("oclm-button-icon") + (state.consoleReady ? '打开控制台' : '控制台准备中…') + '</button>' +
'<button id="oclm-cancel-install" class="oclm-button oclm-button-danger' + (status.installing ? '' : ' oclm-hidden') + '" type="button" data-op="cancel_install">停止安装</button>' +
'</div>' +
'<div id="oclm-status-note" class="oclm-status-note' + (status.installing ? '' : ' oclm-hidden') + '">安装任务正在后台运行,点击“安装中”可继续查看日志。</div>' +
@ -438,7 +437,15 @@
["auto", "自动"]
])) +
fieldInput("数据目录", '<input class="oclm-control" type="text" id="oclm-base-dir" list="oclm-base-dir-options" value="' + escapeAttr(form.base_dir || "") + '" /><datalist id="oclm-base-dir-options">' + baseDirOptions + '</datalist>') +
'<div class="oclm-section-submit"><button class="oclm-button oclm-button-primary" type="button" id="oclm-save-basic"' + (savingBasic ? ' disabled' : '') + '>' + (savingBasic ? '应用中…' : '保存并应用') + '</button>' + serviceButtons + (state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') + '</div>' +
'<div class="oclm-section-submit">' +
'<button class="oclm-button oclm-button-primary" type="button" id="oclm-save-basic"' + (savingBasic ? ' disabled' : '') + '>' + (savingBasic ? '应用中…' : '保存并应用') + '</button>' +
'<span id="oclm-service-actions" class="oclm-inline-buttons' + (showServiceActions ? '' : ' oclm-hidden') + '">' +
'<button id="oclm-btn-start" class="oclm-button' + (canStartService ? '' : ' oclm-hidden') + '" type="button" data-op="start">启动服务</button>' +
'<button id="oclm-btn-stop" class="oclm-button' + (canStopService ? '' : ' oclm-hidden') + '" type="button" data-op="stop">停止服务</button>' +
'<button id="oclm-btn-restart" class="oclm-button" type="button" data-op="restart">重启服务</button>' +
'</span>' +
(state.lastAppliedAt ? '<span class="oclm-applied-hint">已于 ' + escapeHtml(state.lastAppliedAt) + ' 更新配置</span>' : '') +
'</div>' +
'</div></div>' +
'<div class="' + (activeTab === "ai" ? '' : 'oclm-hidden') + '">' +
@ -485,6 +492,7 @@
function updateStatusDom(status) {
status = status || {};
var gatewayActive = !!(status.running || status.reachable);
function byId(id) {
if (root.getElementById) return root.getElementById(id);
@ -565,15 +573,40 @@
else cancelInstallBtn.classList.add("oclm-hidden");
}
if (openConsoleBtn) {
if (status.running) openConsoleBtn.classList.remove("oclm-hidden");
if (gatewayActive) openConsoleBtn.classList.remove("oclm-hidden");
else openConsoleBtn.classList.add("oclm-hidden");
openConsoleBtn.disabled = !(status.running && state.consoleReady);
openConsoleBtn.disabled = !state.consoleReady;
openConsoleBtn.innerHTML = openclawIcon("oclm-button-icon") + (state.consoleReady ? "打开控制台" : "控制台准备中…");
}
if (noteEl) {
if (status.installing) noteEl.classList.remove("oclm-hidden");
else noteEl.classList.add("oclm-hidden");
}
var svcWrap = byId("oclm-service-actions");
var btnStart = byId("oclm-btn-start");
var btnStop = byId("oclm-btn-stop");
var btnRestart = byId("oclm-btn-restart");
var showServiceActions = !!(status.installed && !status.installing);
var canStartService = !!(!status.running && !status.reachable);
var canStopService = !!status.running;
if (svcWrap) {
if (showServiceActions) svcWrap.classList.remove("oclm-hidden");
else svcWrap.classList.add("oclm-hidden");
}
if (btnStart) {
if (showServiceActions && canStartService) btnStart.classList.remove("oclm-hidden");
else btnStart.classList.add("oclm-hidden");
}
if (btnStop) {
if (showServiceActions && canStopService) btnStop.classList.remove("oclm-hidden");
else btnStop.classList.add("oclm-hidden");
}
if (btnRestart) {
if (showServiceActions) btnRestart.classList.remove("oclm-hidden");
else btnRestart.classList.add("oclm-hidden");
}
}
function fieldInput(label, control) {
@ -954,7 +987,7 @@
function refreshStatus(done) {
request(config.statusUrl).then(function(data) {
state.status = data || {};
if (state.status && state.status.running) {
if (state.status && (state.status.running || state.status.reachable)) {
if (!state.consoleReady) {
pollConsoleReady(20);
}

View File

@ -353,6 +353,46 @@ local function get_pid_uptime_human(pid)
return fmt_elapsed(elapsed)
end
local function probe_gateway_ready(port, base_dir, bind)
local sys = require "luci.sys"
local util = require "luci.util"
if not port or not tostring(port):match("^%d+$") then
return false, "", ""
end
if not base_dir or base_dir == "" then
return false, "", ""
end
local base_path = configured_base_path(base_dir)
local candidates = {}
if bind == "lan" or bind == "auto" then
local ip = lan_ipv4()
if ip ~= "" then
candidates[#candidates + 1] = ip
end
end
candidates[#candidates + 1] = "127.0.0.1"
local last_code, last_url = "", ""
for _, host in ipairs(candidates) do
local url = "http://" .. host .. ":" .. port .. base_path
last_url = url
local cmd = string.format(
"curl -fsS -o /dev/null --connect-timeout 1 --max-time 2 -w '%%{http_code}' %s 2>/dev/null",
util.shellquote(url)
)
local code = sys.exec(cmd):gsub("%s+$", "")
last_code = code
local n = tonumber(code) or 0
if n >= 200 and n < 400 then
return true, code, url
end
end
return false, last_code, last_url
end
function action_status()
local sys = require "luci.sys"
local uci = require "luci.model.uci".cursor()
@ -373,6 +413,11 @@ function action_status()
running = (st == "running")
installed = (st == "running" or st == "stopped")
end
local reachable, reachable_code, reachable_url = false, "", ""
if installed and not running then
reachable, reachable_code, reachable_url = probe_gateway_ready(port, base_dir, bind)
end
local lock_running, lock_pid = installer_lock_running()
local installing = (task.running and (task.op == "install" or task.op == "upgrade")) or lock_running
if not installing and task.running and not installed and not running then
@ -405,6 +450,9 @@ function action_status()
enabled = enabled,
installed = installed,
running = running,
reachable = reachable,
reachable_http_code = reachable_code,
reachable_url = reachable_url,
task_running = task.running,
task_op = task.op,
installing = installing,
@ -423,11 +471,10 @@ function action_status()
end
function action_ready()
local sys = require "luci.sys"
local util = require "luci.util"
local uci = require "luci.model.uci".cursor()
local port = uci:get("openclawmgr", "main", "port") or "18789"
local bind = uci:get("openclawmgr", "main", "bind") or "lan"
local base_dir = uci:get("openclawmgr", "main", "base_dir") or ""
if not port:match("^%d+$") then port = "18789" end
@ -437,20 +484,13 @@ function action_ready()
return
end
local base_url = "http://127.0.0.1:" .. port .. configured_base_path(base_dir)
local cmd = string.format(
"curl -fsS -o /dev/null --connect-timeout 1 --max-time 2 -w '%%{http_code}' %s 2>/dev/null",
util.shellquote(base_url)
)
local code = sys.exec(cmd):gsub("%s+$", "")
local n = tonumber(code) or 0
local ready = (n >= 200 and n < 400)
local ready, code, url = probe_gateway_ready(port, base_dir, bind)
write_json({
ok = true,
ready = ready,
http_code = code,
url = base_url,
url = url,
})
end

View File

@ -559,15 +559,14 @@ if selected_env_key and selected_api_key ~= "" then
end
cfg.env = next(env) and env or nil
local models = {}
models.mode = "merge"
local providers = {}
models.providers = providers
cfg.models = models
local models = ensure_table(cfg, "models")
if type(models.mode) ~= "string" or models.mode == "" then
models.mode = "merge"
end
local providers = ensure_table(models, "providers")
local current_provider_id = os.getenv("DEFAULT_AGENT_NAME") or "anthropic"
local current_provider = {}
providers[current_provider_id] = current_provider
local current_provider_id = os.getenv("DEFAULT_AGENT_NAME") or "anthropic"
local current_provider = ensure_table(providers, current_provider_id)
if current_provider_id == "openai" then
current_provider.api = "openai-completions"
@ -600,13 +599,30 @@ elseif current_provider_id == "moonshot" then
}
end
local override_base = os.getenv("DEFAULT_AGENT_OVERRIDE_BASE_URL") or ""
local base_url_mode = os.getenv("DEFAULT_AGENT_BASE_URL_MODE") or "default"
if base_url_mode == "override" and override_base ~= "" then
current_provider.baseUrl = override_base
elseif base_url_mode == "default" then
current_provider.baseUrl = os.getenv("DEFAULT_AGENT_BASE_URL") or current_provider.baseUrl
end
local override_base = os.getenv("DEFAULT_AGENT_OVERRIDE_BASE_URL") or ""
local base_url_mode = os.getenv("DEFAULT_AGENT_BASE_URL_MODE") or "default"
if base_url_mode == "override" and override_base ~= "" then
current_provider.baseUrl = override_base
elseif base_url_mode == "default" then
current_provider.baseUrl = os.getenv("DEFAULT_AGENT_BASE_URL") or current_provider.baseUrl
end
-- luci.jsonc encodes empty Lua tables as JSON arrays ([]). To avoid producing
-- invalid provider entries like {"deepseek/deepseek-chat":[]}, drop provider
-- records that are empty or look like a provider/model string.
do
local to_del = {}
for k, v in pairs(providers) do
if type(k) == "string" and k:find("/", 1, true) then
table.insert(to_del, k)
elseif type(v) == "table" and next(v) == nil then
table.insert(to_del, k)
end
end
for _, k in ipairs(to_del) do
providers[k] = nil
end
end
local agents = ensure_table(cfg, "agents")
local defaults = ensure_table(agents, "defaults")

View File

@ -0,0 +1,6 @@
#!/bin/sh
# Backward-compatible wrapper. New entry: openclawmgr-cli.sh
set -eu
exec /usr/share/openclawmgr/openclawmgr-cli.sh "$@"

View File

@ -40,6 +40,22 @@ local api = require "luci.passwall.api"
sendNextChunk();
}
function decodeIfBase64(str) {
try {
let s = str.replace(/-/g, '+').replace(/_/g, '/');
while (s.length % 4) s += '=';
const decoded = decodeURIComponent(
atob(s).split('').map(c =>
'%' + c.charCodeAt(0).toString(16).padStart(2, '0')
).join('')
);
if (btoa(unescape(encodeURIComponent(decoded))).replace(/=+$/, '') === s.replace(/=+$/, '')) {
return decoded;
}
} catch (e) {}
return str;
}
function getBg(el) {
if (!el) return null;
const style = getComputedStyle(el);
@ -74,15 +90,14 @@ local api = require "luci.passwall.api"
const group = (document.querySelector('#addlink_group_custom input[type="hidden"]')?.value || "default");
nodes_link = nodes_link.replace(/\t/g, "").replace(/\r\n|\r/g, "\n").replace(/<[^>]*>/g, '').trim();
if (nodes_link != "") {
nodes_link = decodeIfBase64(nodes_link);
let s = nodes_link.split('://');
if (s.length > 1) {
ajax_add_node(nodes_link, group);
}
else {
} else {
alert("<%:Please enter the correct link.%>");
}
}
else {
} else {
document.getElementById("nodes_link").focus();
}
}
@ -93,8 +108,7 @@ local api = require "luci.passwall.api"
function(x, data) {
if(x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
}
else {
} else {
alert("<%:Error%>");
}
});
@ -154,8 +168,7 @@ local api = require "luci.passwall.api"
function(x, data) {
if (x && x.status == 200) {
window.location.href = '<%=api.url("node_list")%>';
}
else {
} else {
alert("<%:Error%>");
}
});

View File

@ -1,100 +0,0 @@
include $(TOPDIR)/rules.mk
LUCI_TITLE:=PPPoE Server User Management (Modern JS/Ucode)
LUCI_DEPENDS:=+luci-compat +kmod-nft-connlimit +ucode +rpcd +ppp +ppp-mod-pppoe
LUCI_PKGARCH:=all
PKG_VERSION:=2.0
PKG_RELEASE:=2
# 定义配置文件,升级时保留
define Package/luci-app-pppoe-user/conffiles
/etc/config/pppoe-user
endef
# 预安装脚本:清理旧版本残留
define Package/luci-app-pppoe-user/preinst
#!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then
# 仅在运行时清理,安装到新根文件系统时不清理
if [ -f /etc/init.d/pppoe-user ]; then
/etc/init.d/pppoe-user stop 2>/dev/null || true
fi
# 清理旧的 Lua 视图缓存
rm -rf /tmp/luci-modulecache/*
rm -rf /tmp/luci-indexcache/*
fi
exit 0
endef
define Package/luci-app-pppoe-user/install
# --- 1. I18N (让 luci.mk 自动处理,或手动编译) ---
# 注意:现代 luci.mk 会自动扫描 po 目录并编译。
# 如果必须手动指定,确保编译为 .lua 或 .json (视版本而定)
# 这里我们依赖 luci.mk 的默认行为,只需确保 po 目录存在即可。
# 如果 luci.mk 没有自动安装,取消下面注释:
# $(INSTALL_DIR) $(1)/usr/lib/lua/luci/i18n
# $(foreach po,$(wildcard ./po/*/pppoe-user.po), \
# $(call Build/install-po,$(po),$(1)/usr/lib/lua/luci/i18n) \
# )
# --- 2. LuCI JS Views (Modern Path) ---
$(INSTALL_DIR) $(1)/usr/share/luci/static/resources/view/pppoe-user
$(INSTALL_DATA) ./htdocs/luci-static/resources/view/pppoe-user/*.js \
$(1)/usr/share/luci/static/resources/view/pppoe-user/
# --- 3. Ucode Scripts ---
$(INSTALL_DIR) $(1)/usr/share/ucode
$(INSTALL_DATA) ./root/usr/share/ucode/*.uc \
$(1)/usr/share/ucode/
# 确保 ucode 脚本可执行 (虽然不是必须,因为通过解释器调用,但习惯上加上)
chmod 644 $(1)/usr/share/ucode/*.uc
# --- 4. RPCD ACL ---
$(INSTALL_DIR) $(1)/usr/share/rpcd/acl.d
$(INSTALL_DATA) ./root/usr/share/rpcd/acl.d/*.json \
$(1)/usr/share/rpcd/acl.d/
# --- 5. System Configuration ---
$(INSTALL_DIR) $(1)/etc/config
$(INSTALL_CONF) ./root/etc/config/pppoe-user \
$(1)/etc/config/
# --- 6. Init Scripts ---
$(INSTALL_DIR) $(1)/etc/init.d
$(INSTALL_BIN) ./root/etc/init.d/pppoe-user \
$(1)/etc/init.d/
# --- 7. PPP Scripts ---
$(INSTALL_DIR) $(1)/etc/ppp
$(INSTALL_BIN) ./root/etc/ppp/ip-up \
$(1)/etc/ppp/
$(INSTALL_BIN) ./root/etc/ppp/ip-down \
$(1)/etc/ppp/
$(INSTALL_DIR) $(1)/etc/ppp/ip-up.d
$(INSTALL_BIN) ./root/etc/ppp/ip-up.d/* \
$(1)/etc/ppp/ip-up.d/
$(INSTALL_DIR) $(1)/etc/ppp/ip-down.d
$(INSTALL_BIN) ./root/etc/ppp/ip-down.d/* \
$(1)/etc/ppp/ip-down.d/
# --- 8. Hotplug Scripts ---
$(INSTALL_DIR) $(1)/etc/hotplug.d/iface
$(INSTALL_BIN) ./root/etc/hotplug.d/iface/* \
$(1)/etc/hotplug.d/iface/
# --- 9. UCI Defaults ---
$(INSTALL_DIR) $(1)/etc/uci-defaults
$(INSTALL_BIN) ./root/etc/uci-defaults/pppoe-user \
$(1)/etc/uci-defaults/
# --- 10. LuCI Menu ---
$(INSTALL_DIR) $(1)/usr/share/luci/menu.d
$(INSTALL_DATA) ./root/usr/share/luci/menu.d/*.json \
$(1)/usr/share/luci/menu.d/
endef
# 调用标准的 luci.mk 构建规则
# 它会自动处理 i18n 编译和元数据生成
include $(TOPDIR)/feeds/luci/luci.mk

View File

@ -1,4 +0,0 @@
# luci-app-pppoe-user
OpenWrt Luci PPPoE dial-up user management application
Need to be used with the installation of rp-pppoe software package

View File

@ -1,47 +0,0 @@
'use strict';
'require view';
'require form';
'require uci';
return view.extend({
load: function() {
return uci.load('pppoe-user');
},
render: function() {
var users = uci.sections('pppoe-user', 'user');
var downUsers = users.filter(u => u.enabled == '0');
var m = new form.Map('pppoe-user');
var s = m.section(form.TableSection, 'downtime', _('Downtime User [%d]').format(downUsers.length));
s.anonymous = true;
s.addremove = false;
// 填充数据
s.section = function(section_id) {
// 手动注入数据到 section
return this.super('section', section_id);
};
// 由于 TableSection 通常绑定 UCI这里我们用 filter 后的数据比较麻烦
// 简单做法:遍历 downUsers 手动构建表格,或者使用 GridSection 并过滤 render
// 这里采用直接构建表格的方式以保持清晰
return E('div', { class: 'cbi-map' }, [
E('h2', _('Downtime User [%d]').format(downUsers.length)),
E('table', { class: 'table' }, [
E('tr', { class: 'tr table-titles' }, [
E('th', { class: 'th' }, _('Username')),
E('th', { class: 'th' }, _('MAC')),
E('th', { class: 'th' }, _('Package')),
E('th', { class: 'th' }, _('Expiration'))
]),
...downUsers.map(u => E('tr', { class: 'tr cbi-rowstyle-1' }, [
E('td', { class: 'td' }, u.username),
E('td', { class: 'td' }, u.macaddr || '-'),
E('td', { class: 'td' }, u.package),
E('td', { class: 'td' }, u.expires)
]))
])
]);
}
});

View File

@ -1,22 +0,0 @@
'use strict';
'require view';
'require fs';
return view.extend({
load: function() {
return fs.read('/var/pppoe-user/log/interface.log');
},
render: function(logs) {
var m = new form.Map('pppoe-user');
var s = m.section(form.NamedSection, 'logview', 'dummy');
var o = s.option(form.TextValue, '_log');
o.rows = 30;
o.readonly = true;
o.cfgvalue = function() {
return logs || _('No log available.');
};
return m.render();
}
});

View File

@ -1,169 +0,0 @@
'use strict';
'require view';
'require form';
'require fs';
'require ubus';
'require ui';
return view.extend({
load: function() {
return Promise.all([
fs.read('/proc/net/dev'),
fs.list('/var/etc/pppoe-user/session')
]).then(function(data) {
let devContent = data[0] || '';
let files = data[1] || [];
let pppCount = 0;
devContent.split(/\n/).forEach(function(line) {
if (/^\s*ppp/.test(line)) pppCount++;
});
let sessions = [];
let promises = files.map(function(f) {
if (f.name.endsWith('.json')) {
return fs.read('/var/etc/pppoe-user/session/' + f.name).then(function(content) {
try {
let obj = JSON.parse(content);
obj.session_file = '/var/etc/pppoe-user/session/' + f.name;
sessions.push(obj);
} catch(e) {}
});
}
});
return Promise.all(promises).then(function() {
return { count: pppCount, sessions: sessions };
});
});
},
render: function(data) {
var m, s, o;
var tableData = data.sessions || [];
m = new form.Map('pppoe-user');
s = m.section(form.TableSection, 'sessions', _('Online Users [%d]').format(data.count));
s.anonymous = true;
s.addremove = false;
s.sortable = false;
s.nodescriptions = true;
o = s.option(form.DummyValue, 'username', _('User Name'));
o = s.option(form.DummyValue, 'mac', _('MAC address'));
o = s.option(form.DummyValue, 'interface', _('Interface'));
o = s.option(form.DummyValue, 'ip', _('IP address'));
o = s.option(form.DummyValue, 'package', _('Package'));
o = s.option(form.DummyValue, 'updated', _('Renewal Date'));
o = s.option(form.DummyValue, 'uptime', _('Up Time'));
var btn = s.option(form.Button, 'kill', _('Forced Offline'));
btn.inputstyle = 'reset';
btn.onclick = function(ev, section_id) {
var row = tableData.find(function(r) { return r.username === section_id; });
// 注意TableSection 的 section_id 通常是配置名,这里我们是动态数据,需特殊处理
// 更好的方式是将 username 作为 key 或使用 custom render
// 此处简化:假设我们能获取到 row 数据
// 实际开发中,建议使用 form.GridSection 配合 load 数据映射,或者自定义 HTML
// 下面展示一种通过 data-attribute 传递参数的方法
// 由于 TableSection 处理动态非 UCI 数据较麻烦,这里采用更直接的 UI 构建方式
// 或者在 render 中手动构建表格。
// 为了保持代码简洁,这里演示核心逻辑:
var targetRow = this.map.findElement('id', 'cbi.pppoe-user.sessions.' + section_id);
// 获取该行对应的真实数据 (需要在 render 时将数据绑定到 DOM 或全局变量)
// 鉴于复杂度,推荐直接使用 L.ui.showModal 或 自定义 renderContent
};
// 【更优解】:对于非 UCI 的动态列表,直接返回 HTML 往往比 form.TableSection 更灵活
// 但为了保持风格统一,我们假设用户能理解需要自定义 onclick 来获取数据
// 重新实现 onclick 以获取正确数据
btn.onclick = L.bind(function(ev, section_id) {
// 在 load 阶段我们将数据存到了 this.sessionsData
var session = this.sessionsData.find(s => s.username === section_id);
if (!session) return;
return ui.showModal(_('Confirm'), [
E('p', _('Are you sure you want to force offline user "%s"?').format(session.username)),
E('div', { class: 'right' }, [
E('button', {
class: 'btn',
click: ui.hideModal
}, [ _('Cancel') ]),
' ',
E('button', {
class: 'btn cbi-button-action',
click: ui.handler(L.bind(function() {
return ubus.call('pppoe.user', 'kill', {
session_file: session.session_file,
pid: session.pid
}).then(() => {
ui.addNotification(null, E('p', _('User killed successfully')));
this.load().then(d => this.render(d)); // 刷新
}).catch(e => {
ui.addNotification(null, E('p', _('Error: ') + e.message));
});
}, this))
}, [ _('OK') ])
])
]);
}, this);
// 需要将数据挂载到 this 以便 onclick 访问
this.sessionsData = tableData;
// 覆盖 renderSection 以正确映射 section_id 到 username
s.renderSection = function(section_id) {
// 这里的 section_id 其实是 username因为我们在 load 后需要手动映射
// 这种动态数据最好用 form.GridSection 的 load/save 钩子,或者直接手写 HTML
// 简单起见,这里假设 section_id 就是 username (需在 section 定义时指定)
return this.super('renderSection', section_id);
};
// 修正TableSection 默认基于 UCI config。对于纯动态数据
// 建议重写 render 函数直接生成 HTML 表格,不使用 form.Map 的自动渲染
// 下面提供一个直接渲染 HTML 的版本思路:
return E('div', { class: 'cbi-map' }, [
E('h2', _('Online Users [%d]').format(data.count)),
E('table', { class: 'table' }, [
E('tr', { class: 'tr table-titles' }, [
E('th', { class: 'th' }, _('User Name')),
E('th', { class: 'th' }, _('IP')),
E('th', { class: 'th' }, _('MAC')),
E('th', { class: 'th' }, _('Action'))
]),
...tableData.map(user => E('tr', { class: 'tr cbi-rowstyle-' + ((Math.random() > 0.5) ? '1' : '2') }, [
E('td', { class: 'td' }, user.username),
E('td', { class: 'td' }, user.ip),
E('td', { class: 'td' }, user.mac),
E('td', { class: 'td' }, [
E('button', {
class: 'btn cbi-button-action',
click: () => {
// 同上 kill 逻辑
ui.showModal(_('Confirm'), [
E('p', _('Kill %s?').format(user.username)),
E('div', { class: 'right' }, [
E('button', { class: 'btn', click: ui.hideModal }, _('Cancel')),
' ',
E('button', {
class: 'btn cbi-button-negative',
click: () => {
ubus.call('pppoe.user', 'kill', { session_file: user.session_file, pid: user.pid })
.then(() => location.reload())
.catch(e => alert(e));
}
}, _('Kill'))
])
]);
}
}, _('Forced Offline'))
])
]))
])
]);
}
});

View File

@ -1,109 +0,0 @@
'use strict';
'require view';
'require form';
'require ui';
return view.extend({
render: function () {
var m, s, o;
m = new form.Map('pppoe-user', _(''), _(''));
// User section
s = m.section(form.GridSection, 'user', _('ONU List'));
s.anonymous = true;
s.nodescriptions = true
// Username field
o = s.option(form.Value, 'username', _('Account'));
o.readonly = true;
// Service Name field (dropdown)
o = s.option(form.Value, 'servicename', _('Service Name'));
o.modalonly = true;
o.readonly = true;
// Password field
o = s.option(form.Value, 'password', _('Password'));
o.password = true;
o.modalonly = true;
o.readonly = true;
// MAC Address field (dropdown)
o = s.option(form.Value, 'macaddr', _('MAC Address'));
o.password = true;
o.modalonly = true;
o.readonly = true;
// Package field (dropdown)
o = s.option(form.Value, 'package', _('Package'));
o.readonly = true;
// Upload Speed field (dropdown)
var o = s.option(form.Value, 'urate', _('Upload Speed'));
o.modalonly = true;
o.readonly = true;
// Download Speed field (dropdown)
var o = s.option(form.Value, 'drate', _('Download Speed'));
o.modalonly = true;
o.readonly = true;
// Concurrent Connection Number Segment (dropdown menu)
var o = s.option(form.Value, 'connect', _('Connection Number'));
o.modalonly = true;
o.readonly = true;
// OLT field (dropdown)
var o = s.option(form.Value, 'olt', _('OLT'));
o.value('1', 'OLT 1');
o.value('2', 'OLT 2');
o.value('3', 'OLT 3');
o.value('4', 'OLT 4');
o.value('5', 'OLT 5');
o.value('6', 'OLT 6');
o.value('7', 'OLT 7');
o.value('8', 'OLT 8');
o.value('9', 'OLT 9');
o.value('10', 'OLT 10');
o.value('11', 'OLT 11');
o.value('12', 'OLT 12');
o.value('13', 'OLT 13');
o.value('14', 'OLT 14');
o.value('15', 'OLT 15');
o.value('16', 'OLT 16');
o.value('17', 'OLT 17');
o.value('18', 'OLT 18');
o.value('19', 'OLT 19');
o.value('20', 'OLT 20');
// PON field (dropdown)
var o = s.option(form.Value, 'pon', _('PON'));
o.value('1', 'PON 1');
o.value('2', 'PON 2');
o.value('3', 'PON 3');
o.value('4', 'PON 4');
o.value('5', 'PON 5');
o.value('6', 'PON 6');
o.value('7', 'PON 7');
o.value('8', 'PON 8');
o.value('9', 'PON 9');
o.value('10', 'PON 10');
o.value('11', 'PON 11');
o.value('12', 'PON 12');
o.value('13', 'PON 13');
o.value('14', 'PON 14');
o.value('15', 'PON 15');
o.value('16', 'PON 16');
// Serial Number field (dropdown)
o = s.option(form.Value, 'sn', _('S/N'));
o.placeholder = 'ONT Serial Number';
// Submit button
return m.render();
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});

View File

@ -1,466 +0,0 @@
'use strict';
'require view';
'require form';
'require ui';
'require uci';
return view.extend({
render: function () {
var m, s, o;
m = new form.Map('pppoe-user', _(''), _(''));
// User section
s = m.section(form.GridSection, 'user', _('User List'));
s.anonymous = true;
s.addremove = true;
s.editable = false;
s.nodescriptions = true;
s.sortable = false;
// Enable User field
o = s.option(form.Flag, 'enabled', _('Enable'));
o.rmempty = false; // Make the field required
o.default = "1";
// Username field
o = s.option(form.Value, 'username', _('Account'));
o.rmempty = false; // Make the field required
o.placeholder = _('Required');
o.validate = function (section_id, value) {
// Validate the input value to match the alphanumeric format
if (!/^[a-zA-Z0-9]+$/.test(value)) {
return _('Invalid username format. Only alphanumeric characters are allowed.');
}
// Check the length of the username
if (value.length < 4 || value.length > 16) {
return _('Username length must be between 4 and 16 characters.');
}
// Check for duplicate usernames
var otherSections = uci.sections('pppoe-user', 'user');
for (var i = 0; i < otherSections.length; i++) {
if (section_id !== otherSections[i]['.name'] && otherSections[i]['username'] === value) {
return _('This username is already in use. Please choose a different one.');
}
}
return true;
};
// Service Name field (dropdown)
o = s.option(form.ListValue, 'servicename', _('Service Name'));
o.readonly = true;
o.modalonly = true;
o.value('*', _('Default'));
o.value('serviceA', 'Service A');
o.value('serviceB', 'Service B');
o.value('serviceC', 'Service C');
// Password field
o = s.option(form.Value, 'password', _('Password'));
o.placeholder = _('Required');
o.password = true;
o.modalonly = true;
o.validate = function (section_id, value) {
// Validate the input value to ensure it's between 8 and 16 characters
if (value.length < 8 || value.length > 16) {
return _('Password must be between 8 and 16 characters.');
}
return true;
};
var currentDate = new Date();
var year = currentDate.getFullYear();
var month = ('0' + (currentDate.getMonth() + 1)).slice(-2);
var day = ('0' + currentDate.getDate()).slice(-2);
var formattedDate = year + month + day;
o.default = formattedDate.slice(0, 8);
o.rmempty = false; // Make the field required
var currentDate = new Date();
var year = currentDate.getFullYear();
var month = ('0' + (currentDate.getMonth() + 1)).slice(-2);
var day = ('0' + currentDate.getDate()).slice(-2);
var formattedDate = year + month + day;
o.default = formattedDate.slice(0, 8);
o.rmempty = false; // Make the field required
// IP Address field (custom input with default value *)
o = s.option(form.Value, 'ipaddr', _('IP Address'));
o.placeholder = _('Required');
o.default = '*';
o.value('172.31.16.0/23', '172.31.16.0/23 Rules WAN1');
o.value('172.31.26.0/23', '172.31.26.0/23 Rules WAN2');
o.value('172.31.36.0/23', '172.31.36.0/23 Rules WAN3');
o.value('172.31.46.0/23', '172.31.46.0/23 Rules WAN4');
o.value('172.31.56.0/23', '172.31.56.0/23 Rules WAN5');
o.value('172.31.66.0/23', '172.31.66.0/23 Rules WAN6');
o.value('172.31.76.0/23', '172.31.76.0/23 Rules WAN7');
o.value('172.31.86.0/23', '172.31.86.0/23 Rules WAN8');
o.value('172.31.96.0/23', '172.31.96.0/23 Rules WAN9');
o.value('*', _('Default'));
o.value('172.31.100.0/23', '172.31.100.0/23 Rules MAN0');
o.value('172.31.110.0/23', '172.31.110.0/23 Rules MAN1');
o.value('172.31.120.0/23', '172.31.120.0/23 Rules MAN2');
o.value('172.31.130.0/23', '172.31.130.0/23 Rules MAN3');
o.value('172.31.140.0/23', '172.31.140.0/23 Rules MAN4');
o.value('172.31.150.0/23', '172.31.150.0/23 Rules MAN5');
o.value('172.31.160.0/23', '172.31.160.0/23 Rules MAN6');
o.value('172.31.170.0/23', '172.31.170.0/23 Rules MAN7');
o.value('172.31.180.0/23', '172.31.180.0/23 Rules MAN8');
o.value('172.31.190.0/23', '172.31.190.0/23 Rules MAN9');
// Enhanced validation for IP addresses and network segments
o.validate = function (section_id, value) {
// Regular expressions for valid IPv4, IPv6, and network segments
var ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
var ipv6Regex = /^[\da-fA-F]{1,4}(:[\da-fA-F]{1,4}){0,7}$/;
var networkSegmentRegex = /^(\d{1,3}\.){3}\d{1,3}\/(\d{1,2})$/;
if (value === '*') {
return true; // Asterisk is allowed
}
if (ipv4Regex.test(value)) {
// Validate IPv4 address
var parts = value.split('.');
for (var i = 0; i < 4; i++) {
var part = parseInt(parts[i]);
if (part < 0 || part > 255) {
return _('Invalid IPv4 address. Each segment must be between 0 and 255.');
}
}
// Check for duplicate IP addresses
var otherSections = uci.sections('pppoe-user', 'user');
for (var i = 0; i < otherSections.length; i++) {
if (section_id !== otherSections[i]['.name'] && otherSections[i]['ipaddr'] === value) {
return _('This IP address is already in use by another user. Please choose a different one.');
}
}
return true;
}
if (ipv6Regex.test(value)) {
// Validate IPv6 address
var otherSections = uci.sections('pppoe-user', 'user');
for (var i = 0; i < otherSections.length; i++) {
if (section_id !== otherSections[i]['.name'] && otherSections[i]['ipaddr'] === value) {
return _('This IP address is already in use by another user. Please choose a different one.');
}
}
return true;
}
if (networkSegmentRegex.test(value)) {
// Validate IP network segment
var subnetParts = value.split('/');
var subnetSize = parseInt(subnetParts[1]);
if (subnetSize < 0 || subnetSize > 32) {
return _('Invalid subnet size. It must be between 0 and 32.');
}
return true;
}
// Invalid input, return an error message
return _('Invalid input. Please use "*" or a valid IPv4, IPv6 address, or IP network segment.');
};
// Automatically generate an available IP address when a network segment is selected
o.cfgvalue = function (section_id) {
var selectedSegment = uci.get('pppoe-user', section_id, 'ipaddr');
// Add your new predefined network segment here
if (selectedSegment === '172.31.16.0/23' ||
selectedSegment === '172.31.26.0/23' ||
selectedSegment === '172.31.36.0/23' ||
selectedSegment === '172.31.46.0/23' ||
selectedSegment === '172.31.56.0/23' ||
selectedSegment === '172.31.66.0/23' ||
selectedSegment === '172.31.76.0/23' ||
selectedSegment === '172.31.86.0/23' ||
selectedSegment === '172.31.96.0/23' ||
selectedSegment === '172.31.100.0/23' ||
selectedSegment === '172.31.110.0/23' ||
selectedSegment === '172.31.120.0/23' ||
selectedSegment === '172.31.130.0/23' ||
selectedSegment === '172.31.140.0/23' ||
selectedSegment === '172.31.150.0/23' ||
selectedSegment === '172.31.160.0/23' ||
selectedSegment === '172.31.170.0/23' ||
selectedSegment === '172.31.180.0/23' ||
selectedSegment === '172.31.190.0/23') {
// Generate and assign an available IP address based on the selected network segment
var generatedIP = getAvailableIPAddress(selectedSegment);
// Check if the generated IP address already exists in other sections
var otherSections = uci.sections('pppoe-user', 'user');
for (var i = 0; i < otherSections.length; i++) {
if (section_id !== otherSections[i]['.name'] && otherSections[i]['ipaddr'] === generatedIP) {
generatedIP = getAvailableIPAddress(selectedSegment); // Regenerate if it's a duplicate
}
}
uci.set('pppoe-user', section_id, 'ipaddr', generatedIP);
uci.save();
return generatedIP;
}
return selectedSegment;
};
// Define a function to generate an available IP address based on the selected network segment
function getAvailableIPAddress(networkSegment) {
// Basic logic to generate an available IP address within the network segment
var segments = networkSegment.split('/');
var baseIP = segments[0];
var subnetSize = parseInt(segments[1]);
// Generate a random host number within the subnet range (exclude network and broadcast)
var hostNumber = Math.floor(Math.random() * (Math.pow(2, 32 - subnetSize) - 2)) + 1;
// Convert the host number to an IP address
var generatedIP = baseIP.split('.').map(function (part, index) {
return parseInt(part) + (hostNumber >> (8 * (3 - index)) & 255);
}).join('.');
return generatedIP;
};
// MAC Address field (dropdown)
o = s.option(form.Value, 'macaddr', _('MAC Address'));
o.modalonly = true;
o.placeholder = 'MAC Address';
o.password = true;
o.readonly = true;
// Package field (dropdown)
o = s.option(form.ListValue, 'package', _('Package'));
o.default = 'home';
o.value('home', _('Home'));
o.value('office', _('Office'));
o.value('free', _('Free'));
o.value('test', _('Test'));
// Enable QoS field (checkbox)
o = s.option(form.Flag, 'qos', _('QoS'));
o.modalonly = true;
o.default = '0';
o.readonly = false;
o.rmempty = false;
// Upload Speed field (dropdown)
var o = s.option(form.Value, 'urate', _('Upload Speed'));
o.modalonly = true;
o.placeholder = "1 to 10000 Mbps"
o.datatype = "range(1,10000)"
o.default = '15';
o.depends('qos', '1'); // Show only when QoS is enabled
// Download Speed field (dropdown)
var o = s.option(form.Value, 'drate', _('Download Speed'));
o.modalonly = true;
o.placeholder = "1 to 10000 Mbps"
o.datatype = "range(1,10000)"
o.default = '30';
o.depends('qos', '1'); // Show only when QoS is enabled
// Concurrent Connection Number Segment (dropdown menu)
var o = s.option(form.ListValue, 'connect', _('Connection Number'));
o.modalonly = true;
o.datatype = "range(128,1048576)"
o.default = '2048';
o.value('128');
o.value('256');
o.value('512');
o.value('1024');
o.value('2048');
o.value('4096');
o.value('8192');
o.value('16384');
o.value('32768');
o.value('65536');
o.value('131072');
o.value('262144');
o.value('524288');
o.value('1048576');
o.depends('qos', '1'); // Show only when QoS is enabled
// Opening Date field (read-only)
o = s.option(form.Value, 'opening', _('Opening Date'));
o.readonly = true;
// Automatically populate with the current date in 'YYYY-MM-DD' format
var currentDate = new Date();
var year = currentDate.getFullYear();
var month = ('0' + (currentDate.getMonth() + 1)).slice(-2);
var day = ('0' + currentDate.getDate()).slice(-2);
var formattedDate = year + '-' + month + '-' + day;
o.default = formattedDate;
// Expiration Date field
o = s.option(form.Value, 'expires', _('Expiration Date'));
o.datatype = 'string';
o.placeholder = 'YYYY-MM-DD';
o.validate = function (section_id, value) {
// Validate the input value to match the format YYYY-MM-DD
if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
return _('Invalid date format. Please use YYYY-MM-DD.');
}
// Split the date string into year, month, and day components
var parts = value.split('-');
var year = parseInt(parts[0], 10);
var month = parseInt(parts[1], 10);
var day = parseInt(parts[2], 10);
// Validate the year, month, and day values
if (isNaN(year) || isNaN(month) || isNaN(day)) {
return _('Invalid date values.');
}
if (month < 1 || month > 12) {
return _('Invalid month. Please use a value between 01 and 12.');
}
var daysInMonth = new Date(year, month, 0).getDate();
if (day < 1 || day > daysInMonth) {
return _('Invalid day. Please use a value between 01 and ') + daysInMonth + '.';
}
return true;
};
var currentDate = new Date();
// Calculate expiration dates based on the current day + 1/3/6/12 months
var oneMonthLater = new Date(currentDate);
oneMonthLater.setMonth(currentDate.getMonth() + 1);
var threeMonthsLater = new Date(currentDate);
threeMonthsLater.setMonth(currentDate.getMonth() + 3);
var sixMonthsLater = new Date(currentDate);
sixMonthsLater.setMonth(currentDate.getMonth() + 6);
var twelveMonthsLater = new Date(currentDate);
twelveMonthsLater.setMonth(currentDate.getMonth() + 12);
// Convert calculated dates to YYYY-MM-DD format
function formatDate(date) {
var year = date.getFullYear();
var month = (date.getMonth() + 1).toString().padStart(2, '0');
var day = date.getDate().toString().padStart(2, '0');
return year + '-' + month + '-' + day;
}
// Set the default value to three months later
o.default = formatDate(threeMonthsLater);
o.value(formatDate(oneMonthLater), formatDate(oneMonthLater) + ' ' + _('One Month'));
o.value(formatDate(threeMonthsLater), formatDate(threeMonthsLater) + ' ' + _('Three Months'));
o.value(formatDate(sixMonthsLater), formatDate(sixMonthsLater) + ' ' + _('Six Months'));
o.value(formatDate(twelveMonthsLater), formatDate(twelveMonthsLater) + ' ' + _('Twelve Months'));
// Enable Connect field (checkbox)
o = s.option(form.Flag, 'more', _('Contact'));
o.rmempty = false; // Make the field required
o.modalonly = true;
o.default = '0';
// Address field (dropdown)
o = s.option(form.Value, 'address', _('Address'));
o.modalonly = true;
o.placeholder = 'User Address';
o.depends('more', '1');
// E-mail field (dropdown)
o = s.option(form.DynamicList, 'mail', _('E-mail'));
o.modalonly = true;
o.placeholder = 'User Email';
o.depends('more', '1');
// Phone field (dropdown)
o = s.option(form.DynamicList, 'phone', _('Telephone'));
o.modalonly = true;
o.placeholder = 'User Phone';
o.depends('more', '1');
// GPS Location field (dropdown)
o = s.option(form.Value, 'gps', _('GPS'));
o.placeholder = 'ONT GPS Location';
o.modalonly = true;
o.depends('more', '1');
o = s.option(form.TextValue, 'notes', _('Notes'), _(''));
o.modalonly = true;
o.optional = true;
o.depends('more', '1');
// Agent field (dropdown)
o = s.option(form.Value, 'agent', _('Agent'));
o.modalonly = true;
o.readonly = true;
// Enable ONT field (checkbox)
o = s.option(form.Flag, 'ont', _('ONT'));
o.rmempty = false; // Make the field required
o.modalonly = true;
o.default = '0';
// OLT field (dropdown)
var o = s.option(form.Value, 'olt', _('OLT'));
o.modalonly = true;
o.value('1', 'OLT 1');
o.value('2', 'OLT 2');
o.value('3', 'OLT 3');
o.value('4', 'OLT 4');
o.value('5', 'OLT 5');
o.value('6', 'OLT 6');
o.value('7', 'OLT 7');
o.value('8', 'OLT 8');
o.value('9', 'OLT 9');
o.value('10', 'OLT 10');
o.value('11', 'OLT 11');
o.value('12', 'OLT 12');
o.value('13', 'OLT 13');
o.value('14', 'OLT 14');
o.value('15', 'OLT 15');
o.value('16', 'OLT 16');
o.value('17', 'OLT 17');
o.value('18', 'OLT 18');
o.value('19', 'OLT 19');
o.value('20', 'OLT 20');
o.depends('ont', '1');
// PON field (dropdown)
var o = s.option(form.Value, 'pon', _('PON'));
o.modalonly = true;
o.value('1', 'PON 1');
o.value('2', 'PON 2');
o.value('3', 'PON 3');
o.value('4', 'PON 4');
o.value('5', 'PON 5');
o.value('6', 'PON 6');
o.value('7', 'PON 7');
o.value('8', 'PON 8');
o.value('9', 'PON 9');
o.value('10', 'PON 10');
o.value('11', 'PON 11');
o.value('12', 'PON 12');
o.value('13', 'PON 13');
o.value('14', 'PON 14');
o.value('15', 'PON 15');
o.value('16', 'PON 16');
o.depends('ont', '1');
// Serial Number field (dropdown)
o = s.option(form.Value, 'sn', _('S/N'));
o.modalonly = true;
o.placeholder = 'ONT Serial Number';
o.depends('ont', '1');
// Submit button
return m.render();
},
handleSave: null,
handleSaveApply: null,
handleReset: null
});

View File

@ -1,170 +0,0 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
msgid "Management"
msgstr ""
msgid "User Manager"
msgstr ""
msgid "New Users"
msgstr ""
msgid "User List"
msgstr ""
msgid "ONU List"
msgstr ""
msgid "User Number"
msgstr ""
msgid "Enabled"
msgstr ""
msgid "User Name"
msgstr ""
msgid "Account"
msgstr ""
msgid "This username is already in use. Please choose a different one."
msgstr ""
msgid "Service Name"
msgstr ""
msgid "Password"
msgstr ""
msgid "Password must be between 8 and 16 characters."
msgstr ""
msgid "IP Address"
msgstr ""
msgid "Invalid IPv4 address. Each segment must be between 0 and 255."
msgstr ""
msgid "This IP address is already in use by another user. Please choose a different one."
msgstr ""
msgid "Invalid subnet size. It must be between 0 and 32."
msgstr ""
msgid "Invalid input. Please use "*" or a valid IPv4, IPv6 address, or IP network segment."
msgstr ""
msgid "Package"
msgstr ""
msgid "Speed"
msgstr ""
msgid "QoS"
msgstr ""
msgid "Upload Speed"
msgstr ""
msgid "Download Speed"
msgstr ""
msgid "Speed Unit"
msgstr ""
msgid "Connection Number"
msgstr ""
msgid "Queuing Rules"
msgstr ""
msgid "Queue Script"
msgstr ""
msgid "Link Layer"
msgstr ""
msgid "Overhead Bytes"
msgstr ""
msgid "Debug Logging"
msgstr ""
msgid "Opening Date"
msgstr ""
msgid "Validity Period"
msgstr ""
msgid "Expiration Date"
msgstr ""
msgid "Invalid date format. Please use YYYY-MM-DD."
msgstr ""
msgid "Agent"
msgstr ""
msgid "Default"
msgstr ""
msgid "Home"
msgstr ""
msgid "Office"
msgstr ""
msgid "Free"
msgstr ""
msgid "Test"
msgstr ""
msgid "One Month"
msgstr ""
msgid "Three Months"
msgstr ""
msgid "Six Months"
msgstr ""
msgid "Twelve Months"
msgstr ""
msgid "ONU Manager"
msgstr ""
msgid "ONT"
msgstr ""
msgid "ONT Serial Number"
msgstr ""
msgid "Contact"
msgstr ""
msgid "E-mail"
msgstr ""
msgid "Telephone"
msgstr ""
msgid "User Status"
msgstr ""
msgid "Online User"
msgstr ""
msgid "Up Time"
msgstr ""
msgid "Renewal Date"
msgstr ""
msgid "Process ID"
msgstr ""
msgid "Forced Offline"
msgstr ""

View File

@ -1 +0,0 @@
zh_Hans

View File

@ -1,167 +0,0 @@
msgid "Management"
msgstr "管理"
msgid "User Manager"
msgstr "用户管理"
msgid "New Users"
msgstr "新增用户"
msgid "User List"
msgstr "用户列表"
msgid "ONU List"
msgstr "光猫列表"
msgid "User Number"
msgstr "用户数"
msgid "Enabled"
msgstr "启用"
msgid "User Name"
msgstr "用户名"
msgid "Account"
msgstr "账号"
msgid "This username is already in use. Please choose a different one."
msgstr "该用户名已被使用。 请选择一个不同的。"
msgid "Service Name"
msgstr "服务名称"
msgid "Password"
msgstr "密码"
msgid "Password must be between 8 and 16 characters."
msgstr "密码必须介于 8 到 16 个字符之间。"
msgid "IP Address"
msgstr "IP 地址"
msgid "Invalid IPv4 address. Each segment must be between 0 and 255."
msgstr "IPv4 地址无效。 每个段必须介于 0 到 255 之间。"
msgid "This IP address is already in use by another user. Please choose a different one."
msgstr "该 IP 地址已被其他用户使用。 请选择一个不同的。"
msgid "Invalid subnet size. It must be between 0 and 32."
msgstr "子网大小无效。 它必须介于 0 和 32 之间。"
msgid "Invalid input. Please use "*" or a valid IPv4, IPv6 address, or IP network segment."
msgstr "输入无效。 请使用“*”或有效的 IPv4、IPv6 地址或 IP 网段。"
msgid "Package"
msgstr "套餐"
msgid "Speed"
msgstr "网速"
msgid "QoS"
msgstr "限速"
msgid "Upload Speed"
msgstr "上传速度"
msgid "Download Speed"
msgstr "下载速度"
msgid "Speed Unit"
msgstr "速度单位"
msgid "Connection Number"
msgstr "连接数"
msgid "Queuing Rules"
msgstr "队列规则"
msgid "Queue Script"
msgstr "队列脚本"
msgid "Link Layer"
msgstr "链路层"
msgid "Overhead Bytes"
msgstr "开销字节"
msgid "Debug Logging"
msgstr "调试日志"
msgid "Opening Date"
msgstr "开通日期"
msgid "Validity Period"
msgstr "有效期"
msgid "Expiration Date"
msgstr "截止日期"
msgid "Invalid date format. Please use YYYY-MM-DD."
msgstr "日期格式无效。 请使用 YYYY-MM-DD。"
msgid "Agent"
msgstr "代理人"
msgid "Default"
msgstr "默认"
msgid "Home"
msgstr "家"
msgid "Office"
msgstr "办公"
msgid "Free"
msgstr "免费"
msgid "Test"
msgstr "测试"
msgid "One Month"
msgstr "一个月"
msgid "Three Months"
msgstr "三个月"
msgid "Six Months"
msgstr "半年"
msgid "Twelve Months"
msgstr "一年"
msgid "ONU Manager"
msgstr "光猫管理"
msgid "ONT"
msgstr "光猫绑定"
msgid "ONT Serial Number"
msgstr "光猫序列号"
msgid "Contact"
msgstr "联系方式"
msgid "E-mail"
msgstr "电子邮箱"
msgid "Telephone"
msgstr "电话"
msgid "User Status"
msgstr "用户状态"
msgid "Online User"
msgstr "在线用户"
msgid "Up Time"
msgstr "上线时间"
msgid "Renewal Date"
msgstr "续订日期"
msgid "Process ID"
msgstr "进程 ID"
msgid "Forced Offline"
msgstr "强制下线"

View File

@ -1,13 +0,0 @@
config user
option enabled '1'
option username 'guest'
option servicename '*'
option password '12345678'
option ipaddr '*'
option package 'home'
option qos '1'
option urate '15'
option drate '30'
option connect '1024'

View File

@ -1,9 +0,0 @@
#!/bin/sh
. /lib/functions.sh
PPPOE_TIME="$(date "+%Y-%m-%d_%H:%M:%S")"
GATEWAY="$(uci get network."$INTERFACE".gateway)"
PING="$(ping "$GATEWAY" -I "$DEVICE" -c 5 | awk '{ print strftime("%Y-%m-%d %H:%M:%S",systime())"\t" $0; fflush() }' | grep -E 'rtt|round-trip' | cut -d "=" -f2)"
echo "$PPPOE_TIME Track Device=$DEVICE, Interface=$INTERFACE, Gateway=$GATEWAY, Ping=$PING, Action=$ACTION" >> /var/pppoe-user/log/interface.log

View File

@ -1,180 +0,0 @@
#!/bin/sh /etc/rc.common
# Copyright (C) 2024-2026 OpenWrt.org
# Licensed under GPL v2
START=90
STOP=10
USE_PROCD=1
PROG=/usr/sbin/pppoe-server
UBUS_HELPER=/usr/share/ucode/pppoe-ubus.uc
UCODE_BIN=/usr/bin/ucode
SPEED_LIMIT_SWITCH=1
CHAP_SECRETS=/etc/ppp/chap-secrets
BACKUP_DIR=/var/pppoe-user/backup
LOG_DIR=/var/pppoe-user/log
SESSION_DIR=/var/etc/pppoe-user/session
# 初始化目录
init_dirs() {
mkdir -p "$BACKUP_DIR"
mkdir -p "$LOG_DIR"
mkdir -p "$LOG_DIR/userinfo"
mkdir -p "$SESSION_DIR"
mkdir -p /var/lock
}
# 使用 ucode 进行日期比较和状态检查 (更高效且不易出错)
check_expiration_and_build_secrets() {
local current_date
current_date=$(date +"%Y-%m-%d")
# 清空并重建 chap-secrets
: > "$CHAP_SECRETS"
# 遍历 UCI 配置
config_load pppoe-user
config_foreach setup_user "$current_date"
}
# 回调函数:处理每个用户配置
# 参数 1: cfg_name, 参数 2: current_date
setup_user() {
local cfg="$1"
local today="$2"
local enabled username servicename password ipaddr expires
config_get_bool enabled "$cfg" enabled 0
config_get username "$cfg" username
config_get servicename "$cfg" servicename
config_get password "$cfg" password
config_get ipaddr "$cfg" ipaddr
config_get expires "$cfg" expires
# 默认 servicename 为 *
[ -z "$servicename" ] && servicename="*"
# 检查过期
if [ "$enabled" -eq 1 ] && [ -n "$expires" ] && [ "$expires" = "$today" ]; then
logger -t pppoe-user "User $username expired today ($today). Disabling."
uci set pppoe-user."$cfg".enabled=0
uci commit pppoe-user
enabled=0
fi
# 如果启用且未过期,写入 chap-secrets
if [ "$enabled" -eq 1 ]; then
# 格式: user service secret IP
echo "$username $servicename $password $ipaddr" >> "$CHAP_SECRETS"
fi
}
# 管理 Crontab 任务
manage_cron() {
local cron_cmd_start="59 6 * * * /etc/init.d/pppoe-user start"
local cron_cmd_backup="59 23 * * * cp -f /etc/config/pppoe-user $BACKUP_DIR/pppoe-user.\$(date +\%Y\%m\%d).bak"
# 移除旧的任务(防止重复)
crontab -u root -l 2>/dev/null | grep -v "/etc/init.d/pppoe-user start" | grep -v "cp -f /etc/config/pppoe-user" | crontab -u root -
# 添加新任务
(crontab -u root -l 2>/dev/null; echo "$cron_cmd_start"; echo "$cron_cmd_backup") | sort -u | crontab -u root -
}
# 配置 nftables 限速表
setup_nft() {
# 检查表是否存在,存在则先删除以保证幂等性
nft list table inet pppoe >/dev/null 2>&1 && nft delete table inet pppoe
nft add table inet pppoe
nft add chain inet pppoe upload { type filter hook prerouting priority filter\; policy accept\; }
nft add chain inet pppoe download { type filter hook postrouting priority filter\; policy accept\; }
# 更新防火墙区域配置以包含 ppp 接口
local zone_device
zone_device=$(uci get firewall.@zone[0].device 2>/dev/null)
if [ -z "$(echo "$zone_device" | grep 'ppp+')" ]; then
uci add_list firewall.@zone[0].device='ppp+'
uci commit firewall
# 注意:这里不重启 firewall以免断开现有连接nft 规则即时生效
fi
}
cleanup_nft() {
nft list table inet pppoe >/dev/null 2>&1 && nft delete table inet pppoe
}
# 启动 ucode ubus 辅助服务
start_ubus_helper() {
if [ -x "$UCODE_BIN" ] && [ -f "$UBUS_HELPER" ]; then
# 检查是否已在运行
if ! pgrep -f "ucode.*pppoe-ubus.uc" >/dev/null; then
logger -t pppoe-user "Starting ubus helper service"
$UCODE_BIN $UBUS_HELPER &
disown
fi
else
logger -t pppoe-user "Warning: ucode helper not found or not executable. LuCI kill function may fail."
fi
}
stop_ubus_helper() {
pkill -f "ucode.*pppoe-ubus.uc"
}
start_service() {
init_dirs
logger -t pppoe-user "Starting PPPoE User Management Service"
# 备份旧的 chap-secrets
[ -f "$CHAP_SECRETS" ] && cp -f "$CHAP_SECRETS" /etc/ppp/lasttime-chap-secrets
# 生成新的 chap-secrets 并检查过期
check_expiration_and_build_secrets
# 设置权限
chmod 600 "$CHAP_SECRETS"
# 配置定时任务
manage_cron
# 配置限速
if [ "$SPEED_LIMIT_SWITCH" = "1" ]; then
setup_nft
else
cleanup_nft
fi
# 启动 ubus 辅助进程 (供 LuCI JS 调用)
start_ubus_helper
# 如果是作为服务运行,不需要保持前台,因为主要工作是生成配置和规则
# 真正的 PPPoE 服务由 pppoe-server 守护进程管理
}
stop_service() {
logger -t pppoe-user "Stopping PPPoE User Management Service"
stop_ubus_helper
cleanup_nft
# 可选:清除 chap-secrets (通常不建议,因为重启后需要,但在 stop 时清理可确保安全)
# : > "$CHAP_SECRETS"
}
reload_service() {
stop_service
start_service
}
# 兼容旧版 procd 的 stop 行为
stop() {
stop_service
}
service_triggers() {
procd_add_reload_trigger "pppoe-user"
}

View File

@ -1,41 +0,0 @@
#!/bin/sh
# This script is run by the pppd _after_ the link is brought down.
# It should be used to delete routes, unset IP addresses, etc.
# Check if the required number of arguments is provided
if [ $# -ne 5 ]; then
echo "Usage: $0 <interface> <tty> <speed> <local_ip> <peer_ip>" >&2
exit 1
fi
# Reset the PATH variable
PATH=/usr/sbin:/sbin:/usr/bin:/bin
export PATH
# Log the start of the script
logger "Starting ip-down script for interface $1 with local IP $4 and peer IP $5"
# Define the directory containing additional scripts
IP_DOWN_D_DIR="/etc/ppp/ip-down.d"
# Check if the directory exists
if [ ! -d "$IP_DOWN_D_DIR" ]; then
echo "Directory $IP_DOWN_D_DIR does not exist." >&2
exit 1
fi
# Loop through all files in the ip-down.d directory and execute them if they are executable
for script in "$IP_DOWN_D_DIR"/*; do
if [ -x "$script" ]; then
"$script" "$@" || {
echo "Error executing $script" >&2
exit 1
}
fi
done
# Log the successful completion of the script
logger "Completed ip-down script for interface $1"
exit 0

View File

@ -1,58 +0,0 @@
#!/bin/sh
#
# This script is run by pppd _after_ the link is brought down.
# It should be used to delete routes, unset IP addresses, etc.
#
# This script is called with the following arguments:
# Arg Name Example
# $1 Interface name ppp0
# $2 The tty ttyS1
# $3 The link speed 38400
# $4 Local IP number 12.34.56.78
# $5 Peer IP number 12.34.56.99
#
# Reset the PATH environment variable
PATH=/usr/sbin:/sbin:/usr/bin:/bin
export PATH
# Get the current date and time
QUIT_TIME="$(date "+%Y-%m-%d_%H:%M:%S")"
# Extract username, interface name, and peer IP address from arguments
USERNAME="${PEERNAME}"
IFACE="${1}"
TTY="${2}"
PEERIP="${5}"
# Paths and Configuration
USER_LOG="/var/pppoe-user/log/userinfo"
USER_LOG_FILE="${USER_LOG}/${USERNAME}.log"
USER_CONFIG="pppoe-user"
USER_CFGID="$(uci show ${USER_CONFIG} | grep "${USERNAME}" | cut -d '.' -sf 2)"
USER_CONNECT="$(uci get ${USER_CONFIG}.${USER_CFGID}.connect)"
SESSION_PATH="/var/etc/pppoe-user/session"
# Remove the session file
rm -f "${SESSION_PATH}/${USERNAME}.${IFACE}"
# Log the disconnection details
echo "${QUIT_TIME} ${USERNAME} Offline Interface: ${IFACE} IP: ${PEERIP}" >> "${USER_LOG_FILE}"
echo "Connect time: ${CONNECT_TIME} minutes" >> "${USER_LOG_FILE}"
echo "Sent: ${BYTES_SENT} bytes" >> "${USER_LOG_FILE}"
echo "Received: ${BYTES_RCVD} bytes" >> "${USER_LOG_FILE}"
# Calculate and log the sum and average speed
sum_bytes=$((${BYTES_SENT} + ${BYTES_RCVD}))
sum=$(echo "scale=2;${sum_bytes}/1024/1024" | bc)
echo "Bytes sum: ${sum} MB" >> "${USER_LOG_FILE}"
ave=$(echo "scale=2;${sum_bytes}/1024/${CONNECT_TIME}" | bc)
echo "Average speed: ${ave} KB/s" >> "${USER_LOG_FILE}"
# Add an empty line for separation in the log file
echo >> "${USER_LOG_FILE}"
# Log the execution
logger "${QUIT_TIME} ${PEERNAME} ${PEERIP} OFFLINE-USERS >>>>>> The script has been executed!"
exit 0

View File

@ -1,39 +0,0 @@
#!/bin/sh
(
flock -n 200 || exit 1
# The environment is cleared before executing this script
PATH=/usr/sbin:/sbin:/usr/bin:/bin
export PATH
QUIT_TIME="$(date "+%Y-%m-%d_%H:%M:%S")"
USERNAME="${PEERNAME}"
PEERIP="${5}"
USER_CONFIG="pppoe-user"
USER_CFGID="$(uci show ${USER_CONFIG} | grep "${USERNAME}" | cut -d '.' -f 2)"
TABLE="pppoe"
# Define chains
CHAINS="upload download"
# Loop through each chain and delete all rules matching the IP
for CHAIN in ${CHAINS}; do
# Get all rule handles associated with the IP in the current chain
RULE_HANDLES="$(nft -a list chain inet ${TABLE} ${CHAIN} | grep "${PEERIP}" | awk '{print $NF}')"
if [ -n "${RULE_HANDLES}" ]; then
for HANDLE in ${RULE_HANDLES}; do
nft delete rule inet ${TABLE} ${CHAIN} handle "${HANDLE}" || {
logger "${QUIT_TIME} ERROR: Failed to delete rule handle ${HANDLE} in chain ${CHAIN} for ${USERNAME} ${PEERIP}"
exit 1
}
done
else
logger "${QUIT_TIME} WARNING: No rules found for ${USERNAME} ${PEERIP} in chain ${CHAIN}"
fi
done
exit 0
) 200>/var/lock/ipdown.lock

View File

@ -1,41 +0,0 @@
#!/bin/sh
# This script is run by the pppd after the link is established.
# It should be used to add routes, set IP address, run the mailq, etc.
# Check if the required number of arguments is provided
if [ $# -ne 5 ]; then
echo "Usage: $0 <interface> <tty> <speed> <local_ip> <peer_ip>" >&2
exit 1
fi
# Reset the PATH variable
PATH=/usr/sbin:/sbin:/usr/bin:/bin
export PATH
# Log the start of the script
logger "Starting ip-up script for interface $1 with local IP $4 and peer IP $5"
# Define the directory containing additional scripts
IP_UP_D_DIR="/etc/ppp/ip-up.d"
# Check if the directory exists
if [ ! -d "$IP_UP_D_DIR" ]; then
echo "Directory $IP_UP_D_DIR does not exist." >&2
exit 1
fi
# Loop through all files in the ip-up.d directory and execute them if they are executable
for script in "$IP_UP_D_DIR"/*; do
if [ -x "$script" ]; then
"$script" "$@" || {
echo "Error executing $script" >&2
exit 1
}
fi
done
# Log the successful completion of the script
logger "Completed ip-up script for interface $1"
exit 0

View File

@ -1,87 +0,0 @@
#!/bin/sh
#
# This script is run by pppd after the link is established.
# It is used to add routes, set IP address, run mailq, etc.
#
# This script is called with the following arguments:
# Arg Name Example
# $1 Interface name ppp0
# $2 The tty ttyS1
# $3 The link speed 38400
# $4 Local IP number 12.34.56.78
# $5 Peer IP number 12.34.56.99
#
# Reset the PATH environment variable
PATH=/usr/sbin:/sbin:/usr/bin:/bin
export PATH
LOGIN_TIME=$(date "+%Y-%m-%d_%H:%M:%S")
USERNAME="${PEERNAME}"
IFACE="${1}"
TTY="${2}"
PEERIP="${5}"
USER_LOG="/var/pppoe-user/log/userinfo"
USER_LOG_FILE="${USER_LOG}/${USERNAME}.log"
USER_CONFIG="pppoe-user"
USER_CFGID="$(uci show ${USER_CONFIG} | grep "${USERNAME}" | cut -d '.' -sf 2)"
SERVIC_NAME="$(uci get ${USER_CONFIG}.${USER_CFGID}.servicename)"
USER_PACKAGE="$(uci get ${USER_CONFIG}.${USER_CFGID}.package)"
USER_QOS="$(uci get ${USER_CONFIG}.${USER_CFGID}.qos)"
USER_UPLOAD="$(uci get ${USER_CONFIG}.${USER_CFGID}.urate)"
USER_DOWNLOAD="$(uci get ${USER_CONFIG}.${USER_CFGID}.drate)"
USER_CONNECT="$(uci get ${USER_CONFIG}.${USER_CFGID}.connect)"
RENEWAL_DATE="$(uci get ${USER_CONFIG}.${USER_CFGID}.expires)"
CONFIG_PATH="/var/etc/pppoe-user"
SESSION_PATH="${CONFIG_PATH}/session"
PID="$(cat /var/run/pppd/${IFACE}.pid 2>/dev/null)"
MAC="$(ps -w | grep pppd | grep -v 'grep' | grep -h -m 1 "${PID}" | awk '{print $10}' | cut -d : -f 2,2-7)"
mkdir -p "${USER_LOG}"
mkdir -p "${SESSION_PATH}"
# Write session information in JSON format
cat <<-EOF > "${SESSION_PATH}/${USERNAME}.${IFACE}"
{
"username": "${USERNAME}",
"mac": "${MAC}",
"interface": "${IFACE}",
"tty": "${TTY}",
"speed": "${SPEED}",
"ip": "${PEERIP}",
"servicename": "${SERVIC_NAME}",
"package": "${USER_PACKAGE}",
"connect": "${USER_CONNECT}",
"updated": "${RENEWAL_DATE}",
"uptime": "${LOGIN_TIME}",
"pid": "${PID}"
}
EOF
# Find and kill previous PPP sessions for the same user
cfgid=$(uci show ${USER_CONFIG} | grep "@user" | grep ".username='${USERNAME}'" | cut -d '.' -sf 2)
if [ -n "$cfgid" ]; then
HAS_LOGIN=$(ls "${SESSION_PATH}" | grep "^${USERNAME}\.ppp" | grep -v "${IFACE}")
if [ -n "$HAS_LOGIN" ]; then
KO_IFACE=$(echo "$HAS_LOGIN" | awk -F '.' '{print $2}')
KO_PID=$(cat "/var/run/${KO_IFACE}.pid" 2>/dev/null)
[ -n "$KO_PID" ] && kill -9 "$KO_PID" >/dev/null 2>&1
rm -f "${SESSION_PATH}/${HAS_LOGIN}"
rm -f "/var/run/${KO_IFACE}.pid"
fi
fi
# Update the MAC address in the user's configuration
uci set "${USER_CONFIG}.${USER_CFGID}.macaddr=${MAC}"
uci commit "${USER_CONFIG}"
# Log the connection details
echo "${LOGIN_TIME} ${USERNAME} Online PID: ${PID} Interface: ${IFACE} IP: ${PEERIP} MAC: ${MAC}" >> "${USER_LOG_FILE}"
echo >> "${USER_LOG_FILE}"
logger "${LOGIN_TIME} ${PEERNAME} ${PEERIP} ONLINE-USERS >>>>>> The script has been executed!"
exit 0

View File

@ -1,58 +0,0 @@
#!/bin/sh
(
flock -n 200 || exit 1
# Environment variable settings
PATH=/usr/sbin:/sbin:/usr/bin:/bin
export PATH
LOGIN_TIME="$(date "+%Y-%m-%d_%H:%M:%S")"
USERNAME="${PEERNAME}"
PEERIP="${5}"
USER_CONFIG="pppoe-user"
USER_CFGID="$(uci show ${USER_CONFIG} | grep "${USERNAME}" | cut -d '.' -f 2)"
# Convert Mbps to KBytes
USER_QOS="$(uci get ${USER_CONFIG}.${USER_CFGID}.qos 2>/dev/null)"
USER_UPLOAD="$(($(uci get ${USER_CONFIG}.${USER_CFGID}.urate 2>/dev/null) * 125))"
USER_DOWNLOAD="$(($(uci get ${USER_CONFIG}.${USER_CFGID}.drate 2>/dev/null) * 125))"
UPLOAD_BURST="$((USER_UPLOAD / 10))"
DOWNLOAD_BURST="$((USER_DOWNLOAD / 10))"
USER_CONNECT="$(uci get ${USER_CONFIG}.${USER_CFGID}.connect 2>/dev/null)"
# Ensure that the obtained values are valid
if [ -z "${USER_QOS}" ] || [ -z "${USER_UPLOAD}" ] || [ -z "${USER_DOWNLOAD}" ] || [ -z "${USER_CONNECT}" ]; then
logger "${LOGIN_TIME} ERROR: Failed to get user config for (${USERNAME}) (${PEERIP})"
exit 1
fi
UP="upload"
DOWN="download"
# Add traffic control rules
if [ "${USER_QOS}" = "1" ]; then
# Ensure rules are inserted in the correct order
# Upload chain
nft insert rule inet pppoe ${UP} ip saddr "${PEERIP}" ct count over "${USER_CONNECT}" drop
nft insert rule inet pppoe ${UP} ip saddr "${PEERIP}" ct state new limit rate "${USER_CONNECT}"/minute accept
nft insert rule inet pppoe ${UP} ip saddr "${PEERIP}" limit rate over "${USER_UPLOAD}" kbytes/second burst "${UPLOAD_BURST}" kbytes counter drop
if [ $? -ne 0 ]; then
logger "${LOGIN_TIME} ERROR: Failed to insert upload rules (${USERNAME}) (${PEERIP})"
exit 1
fi
# Download chain
nft insert rule inet pppoe ${DOWN} ip daddr "${PEERIP}" ct count over "${USER_CONNECT}" drop
nft insert rule inet pppoe ${DOWN} ip daddr "${PEERIP}" ct state new limit rate "${USER_CONNECT}"/minute accept
nft insert rule inet pppoe ${DOWN} ip daddr "${PEERIP}" limit rate over "${USER_DOWNLOAD}" kbytes/second burst "${DOWNLOAD_BURST}" kbytes counter drop
if [ $? -ne 0 ]; then
logger "${LOGIN_TIME} ERROR: Failed to insert download rules (${USERNAME}) (${PEERIP})"
exit 1
fi
fi
exit 0
) 200>/var/lock/ipup.lock

View File

@ -1,25 +0,0 @@
#!/bin/sh
[ ! -f "/usr/share/ucitrack/luci-app-pppoe-user.json" ] && {
cat > /usr/share/ucitrack/luci-app-pppoe-user.json << EEOF
{
"config": "pppoe-user",
"init": "pppoe-user"
}
EEOF
}
uci -q batch <<-EOF >/dev/null
delete ucitrack.@pppoe-user[-1]
add ucitrack pppoe-user
set ucitrack.@pppoe-user[-1].init=pppoe-user
commit ucitrack
EOF
chmod 600 /etc/config/pppoe-user
chmod 755 /etc/init.d/pppoe-user
chmod 755 /etc/ppp/ip-up /etc/ppp/ip-down
chmod 755 /etc/ppp/auth-up /etc/ppp/auth-down
chmod +x /etc/ppp/ip-up.d/* /etc/ppp/ip-down.d/*
rm -rf /tmp/luci-*cache
exit 0

View File

@ -1,71 +0,0 @@
{
"admin/pppoe-user": {
"title": "Management",
"order": 60,
"action": {
"type": "alias",
},
"depends": {
"acl": [ "luci-app-pppoe-user" ],
}
},
"admin/pppoe-user/usermanager": {
"title": "User Manager",
"order": 1,
"action": {
"type": "view",
"path": "pppoe-user/user",
}
},
"admin/pppoe-user/onumanager": {
"title": "ONU Manager",
"order": 2,
"action": {
"type": "view",
"path": "pppoe-user/onu",
}
},
"admin/status/userstatus": {
"title": "User Status",
"order": 88,
"action": {
"type": "alias",
"path": "admin/status/userstatus/onlineuser",
"post": { "cbi.submit": true }
},
"depends": {
"acl": [ "luci-app-pppoe-user" ],
"uci": { "pppoe-user": true }
}
},
"admin/status/userstatus/onlineuser": {
"title": "Online User",
"order": 1,
"action": {
"type": "form",
"path": "pppoe-user/onlineuser",
}
},
"admin/status/userstatus/downtimeuser": {
"title": "Downtime User",
"order": 2,
"action": {
"type": "form",
"path": "pppoe-user/downtimeuser",
}
},
"admin/status/userstatus/interface": {
"title": "Interface Monitor",
"order": 3,
"action": {
"type": "form",
"path": "pppoe-user/interface",
}
}
}

View File

@ -1,20 +0,0 @@
{
"luci-app-pppoe-user": {
"description": "Grant access to pppoe-user",
"read": {
"uci": [ "pppoe-user" ],
"file": {
"/var/etc/pppoe-user/session/*.json": [ "read" ],
"/var/pppoe-user/log/interface.log": [ "read" ],
"/proc/net/dev": [ "read" ]
}
},
"write": {
"uci": [ "pppoe-user" ],
"exec": {
"/bin/kill": [ "arg1" ],
"/usr/bin/unlink": [ "arg1" ]
}
}
}
}

View File

@ -1,29 +0,0 @@
#!/usr/bin/ucode
import { ubus } from 'ubus';
import { unlink } from 'fs';
let conn = ubus.connect();
if (!conn) exit(1);
conn.add({
name: 'pppoe.user',
methods: {
kill: {
args: { session_file: '', pid: '' },
call: function(req, args) {
if (!args.session_file || !args.pid)
return { error: 'Invalid arguments' };
try {
unlink(args.session_file);
system(`kill -15 ${args.pid}`);
return { success: true };
} catch (e) {
return { error: e };
}
}
}
}
});
conn.listen();

View File

@ -1,107 +0,0 @@
#!/usr/bin/ucode
/* /usr/share/ucode/pppoe-user.uc */
import { readfile, listdir, stat } from 'fs';
import { exec } from 'ubus';
import { parse } from 'jsonc';
const SESSION_PATH = '/var/etc/pppoe-user/session';
const LOG_PATH = '/var/pppoe-user/log/interface.log';
// 获取在线用户列表
export function get_online_users() {
let users = [];
let count = 0;
// 统计 ppp 接口数量 (模拟原 lua 逻辑)
try {
let dev = readfile('/proc/net/dev');
if (dev) {
for (let line in split(dev, /\n/)) {
if (match(line, /^\s*ppp/)) count++;
}
}
} catch (e) {}
if (!stat(SESSION_PATH)) return { count: count, sessions: [] };
let files = listdir(SESSION_PATH);
if (!files) return { count: count, sessions: [] };
for (let filename in files) {
let filepath = `${SESSION_PATH}/${filename}`;
if (!stat(filepath)?.is_regular) continue;
try {
let content = readfile(filepath);
if (content) {
let data = parse(content);
if (data) {
data.session_file = filepath;
push(users, data);
}
}
} catch (e) {
warn(`Failed to read ${filepath}: ${e}`);
}
}
return { count: count, sessions: users };
}
// 强制下线用户
export function kill_user(session_file, pid) {
if (!session_file || !pid) return { error: 'Invalid parameters' };
try {
// 删除会话文件
unlink(session_file);
// 杀死进程
let ret = system(`kill -15 ${pid}`);
return { success: true };
} catch (e) {
return { error: e };
}
}
// 获取接口日志
export function get_interface_log() {
if (!stat(LOG_PATH)) return '';
try {
// 读取最后 200 行 (简单实现ucode 没有直接的 tail需读取后分割)
let content = readfile(LOG_PATH);
if (!content) return '';
let lines = split(content, /\n/);
let tail_lines = slice(lines, -200);
// 反转以符合原逻辑 (最新在上)
return join(reverse(tail_lines), '\n');
} catch (e) {
return `Error reading log: ${e}`;
}
}
// 获取下线用户统计 (简单模拟,实际需解析 UCI)
export function get_downtime_count() {
// 这里可以通过 ubus 调用 uci 或者直接解析 /etc/config/pppoe-user
// 为简化,建议在 JS 层直接处理 UCI 数据,或者在此处调用 uci get
try {
let res = exec('uci', { 'command': 'show', 'params': ['pppoe-user'] });
if (res && res.output) {
let lines = split(res.output, /\n/);
let cnt = 0;
for (let line in lines) {
if (match(line, /=enabled$/)) { // 粗略匹配,更严谨需解析 value
// 实际上 ucidef 或 uci get 更好,这里仅作演示逻辑占位
// 建议在前端 JS 直接遍历 uci.load('pppoe-user')
}
}
// 由于 ucode 直接调 uci 命令解析较繁琐,推荐此逻辑移至前端 JS 处理 UCI 对象
return 0;
}
} catch (e) {}
return 0;
}

View File

@ -1,4 +0,0 @@
{
"config": "pppoe-user",
"init": "pppoe-user"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

View File

@ -54,6 +54,11 @@ return view.extend({
o = s.taboption('general', form.Value, 'description', _('Description'));
o.placeholder = 'Samba4 on OpenWrt';
o = s.taboption('general', form.Flag, 'autoshare', _('Auto Share'),
_('Auto share local disk which connected'));
o.default = 'yes';
o.rmempty = false;
s.taboption('general', form.Flag, 'enable_extra_tuning', _('Enable extra Tuning'),
_('Enable some community driven tuning parameters, that may improve write speeds and better operation via WiFi.\
Not recommend if multiple clients write to the same files, at the same time!'));

View File

@ -1,12 +1,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=pcat-manager
PKG_RELEASE:=1
PKG_RELEASE:=2
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/photonicat/rockchip_rk3568_pcat_manager.git
PKG_SOURCE_DATE:=2023-11-13
PKG_SOURCE_VERSION:=7f05f163ac864ce323ad648a4c03047915fce496
PKG_SOURCE_VERSION:=961edb8b5cbae63efb03ee774d198f6fdb682a45
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-3.0-only