mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-08-04 00:29:27 +08:00
🌴 Sync 2026-04-20 08:30:53
This commit is contained in:
parent
24fc7da92a
commit
ce3c712fa3
@ -1,8 +1,8 @@
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_VERSION:=0.1.10
|
||||
PKG_RELEASE:=12
|
||||
PKG_VERSION:=0.1.11
|
||||
PKG_RELEASE:=13
|
||||
|
||||
LUCI_TITLE:=LuCI support for OpenClaw Launcher
|
||||
LUCI_PKGARCH:=all
|
||||
|
||||
@ -828,10 +828,15 @@
|
||||
.oclm-dialog {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
min-width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
z-index: 9998;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
background: rgba(15, 23, 42, 0.42);
|
||||
}
|
||||
@ -932,10 +937,15 @@
|
||||
.oclm-log-modal {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
width: 100vw;
|
||||
min-width: 100vw;
|
||||
height: 100vh;
|
||||
min-height: 100vh;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
background: rgba(15, 23, 42, 0.42);
|
||||
}
|
||||
|
||||
@ -65,6 +65,14 @@
|
||||
}
|
||||
};
|
||||
var styleText = "";
|
||||
var noticeOverlay = null;
|
||||
var noticeCard = null;
|
||||
var noticeTitle = null;
|
||||
var noticeMessage = null;
|
||||
var noticeCloseButton = null;
|
||||
var actionDialogOverlay = null;
|
||||
var actionDialogCard = null;
|
||||
state.formErrors = state.formErrors || { base_dir: "" };
|
||||
|
||||
function request(url, options) {
|
||||
options = options || {};
|
||||
@ -197,6 +205,358 @@
|
||||
return state.security;
|
||||
}
|
||||
|
||||
function ensureNoticeDialog() {
|
||||
if (noticeOverlay && noticeOverlay.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
noticeOverlay = document.createElement("div");
|
||||
noticeOverlay.style.position = "fixed";
|
||||
noticeOverlay.style.inset = "0";
|
||||
noticeOverlay.style.zIndex = "2147483647";
|
||||
noticeOverlay.style.display = "none";
|
||||
noticeOverlay.style.alignItems = "center";
|
||||
noticeOverlay.style.justifyContent = "center";
|
||||
noticeOverlay.style.padding = "20px";
|
||||
noticeOverlay.style.boxSizing = "border-box";
|
||||
noticeOverlay.style.background = "rgba(15, 23, 42, 0.42)";
|
||||
|
||||
noticeCard = document.createElement("div");
|
||||
noticeCard.style.width = "min(560px, calc(100vw - 40px))";
|
||||
noticeCard.style.borderRadius = "18px";
|
||||
noticeCard.style.border = "1px solid rgba(217, 225, 238, 0.95)";
|
||||
noticeCard.style.background = "#ffffff";
|
||||
noticeCard.style.boxShadow = "0 10px 28px rgba(15, 23, 42, 0.18)";
|
||||
noticeCard.style.padding = "20px";
|
||||
noticeCard.style.boxSizing = "border-box";
|
||||
noticeCard.style.color = "#1f2937";
|
||||
|
||||
noticeTitle = document.createElement("h3");
|
||||
noticeTitle.style.margin = "0 0 10px";
|
||||
noticeTitle.style.fontSize = "18px";
|
||||
noticeTitle.style.fontWeight = "700";
|
||||
|
||||
noticeMessage = document.createElement("div");
|
||||
noticeMessage.style.marginBottom = "16px";
|
||||
noticeMessage.style.color = "#6b7280";
|
||||
noticeMessage.style.lineHeight = "1.6";
|
||||
|
||||
var footer = document.createElement("div");
|
||||
footer.style.display = "flex";
|
||||
footer.style.justifyContent = "flex-end";
|
||||
footer.style.gap = "14px";
|
||||
|
||||
noticeCloseButton = document.createElement("button");
|
||||
noticeCloseButton.type = "button";
|
||||
noticeCloseButton.textContent = "知道了";
|
||||
noticeCloseButton.style.appearance = "none";
|
||||
noticeCloseButton.style.display = "inline-flex";
|
||||
noticeCloseButton.style.alignItems = "center";
|
||||
noticeCloseButton.style.justifyContent = "center";
|
||||
noticeCloseButton.style.border = "1px solid #d9e1ee";
|
||||
noticeCloseButton.style.background = "#2f67f6";
|
||||
noticeCloseButton.style.color = "#ffffff";
|
||||
noticeCloseButton.style.borderRadius = "10px";
|
||||
noticeCloseButton.style.height = "36px";
|
||||
noticeCloseButton.style.minWidth = "96px";
|
||||
noticeCloseButton.style.padding = "0 14px";
|
||||
noticeCloseButton.style.fontSize = "13px";
|
||||
noticeCloseButton.style.fontWeight = "600";
|
||||
noticeCloseButton.style.cursor = "pointer";
|
||||
noticeCloseButton.onclick = function() {
|
||||
closeNoticeDialog();
|
||||
};
|
||||
|
||||
footer.appendChild(noticeCloseButton);
|
||||
noticeCard.appendChild(noticeTitle);
|
||||
noticeCard.appendChild(noticeMessage);
|
||||
noticeCard.appendChild(footer);
|
||||
noticeOverlay.appendChild(noticeCard);
|
||||
document.body.appendChild(noticeOverlay);
|
||||
}
|
||||
|
||||
function openNoticeDialog(message, title) {
|
||||
ensureNoticeDialog();
|
||||
noticeTitle.textContent = String(title || "提示");
|
||||
noticeMessage.textContent = String(message || "");
|
||||
noticeOverlay.style.display = "flex";
|
||||
}
|
||||
|
||||
function closeNoticeDialog() {
|
||||
if (!noticeOverlay) {
|
||||
return;
|
||||
}
|
||||
noticeOverlay.style.display = "none";
|
||||
}
|
||||
|
||||
function ensureActionDialog() {
|
||||
if (actionDialogOverlay && actionDialogOverlay.parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
actionDialogOverlay = document.createElement("div");
|
||||
actionDialogOverlay.style.position = "fixed";
|
||||
actionDialogOverlay.style.inset = "0";
|
||||
actionDialogOverlay.style.zIndex = "2147483646";
|
||||
actionDialogOverlay.style.display = "none";
|
||||
actionDialogOverlay.style.alignItems = "center";
|
||||
actionDialogOverlay.style.justifyContent = "center";
|
||||
actionDialogOverlay.style.padding = "20px";
|
||||
actionDialogOverlay.style.boxSizing = "border-box";
|
||||
actionDialogOverlay.style.background = "rgba(15, 23, 42, 0.42)";
|
||||
|
||||
actionDialogCard = document.createElement("div");
|
||||
actionDialogCard.style.width = "min(560px, calc(100vw - 40px))";
|
||||
actionDialogCard.style.borderRadius = "18px";
|
||||
actionDialogCard.style.border = "1px solid rgba(217, 225, 238, 0.95)";
|
||||
actionDialogCard.style.background = "#ffffff";
|
||||
actionDialogCard.style.boxShadow = "0 10px 28px rgba(15, 23, 42, 0.18)";
|
||||
actionDialogCard.style.padding = "20px";
|
||||
actionDialogCard.style.boxSizing = "border-box";
|
||||
actionDialogCard.style.color = "#1f2937";
|
||||
|
||||
actionDialogOverlay.appendChild(actionDialogCard);
|
||||
document.body.appendChild(actionDialogOverlay);
|
||||
}
|
||||
|
||||
function makeActionDialogText(text, muted) {
|
||||
var el = document.createElement("div");
|
||||
el.textContent = String(text || "");
|
||||
el.style.marginBottom = "10px";
|
||||
el.style.lineHeight = "1.6";
|
||||
el.style.color = muted ? "#6b7280" : "#1f2937";
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeActionDialogCode(text) {
|
||||
var el = document.createElement("code");
|
||||
el.textContent = String(text || "");
|
||||
el.style.display = "block";
|
||||
el.style.marginBottom = "14px";
|
||||
el.style.padding = "10px 12px";
|
||||
el.style.borderRadius = "12px";
|
||||
el.style.background = "rgba(47, 103, 246, 0.08)";
|
||||
el.style.color = "#1f2937";
|
||||
el.style.overflowWrap = "anywhere";
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeActionDialogFooter() {
|
||||
var el = document.createElement("div");
|
||||
el.style.display = "flex";
|
||||
el.style.justifyContent = "flex-end";
|
||||
el.style.gap = "14px";
|
||||
el.style.marginTop = "12px";
|
||||
return el;
|
||||
}
|
||||
|
||||
function makeActionDialogButton(label, kind, onclick) {
|
||||
var button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.textContent = String(label || "");
|
||||
button.style.appearance = "none";
|
||||
button.style.display = "inline-flex";
|
||||
button.style.alignItems = "center";
|
||||
button.style.justifyContent = "center";
|
||||
button.style.border = "1px solid " + (kind === "danger" ? "#ef4444" : kind === "primary" ? "#2f67f6" : "#d9e1ee");
|
||||
button.style.background = kind === "danger" ? "#ef4444" : kind === "primary" ? "#2f67f6" : "#ffffff";
|
||||
button.style.color = kind === "default" ? "#1f2937" : "#ffffff";
|
||||
button.style.borderRadius = "10px";
|
||||
button.style.minWidth = "96px";
|
||||
button.style.minHeight = "36px";
|
||||
button.style.padding = "0 14px";
|
||||
button.style.fontSize = "13px";
|
||||
button.style.fontWeight = "600";
|
||||
button.style.cursor = "pointer";
|
||||
button.onclick = onclick;
|
||||
return button;
|
||||
}
|
||||
|
||||
function makeActionDialogOption(title, note, onclick) {
|
||||
var button = document.createElement("button");
|
||||
var titleEl = document.createElement("span");
|
||||
var noteEl = document.createElement("span");
|
||||
|
||||
button.type = "button";
|
||||
button.style.appearance = "none";
|
||||
button.style.display = "flex";
|
||||
button.style.flexDirection = "column";
|
||||
button.style.alignItems = "flex-start";
|
||||
button.style.justifyContent = "flex-start";
|
||||
button.style.gap = "4px";
|
||||
button.style.width = "100%";
|
||||
button.style.padding = "12px 14px";
|
||||
button.style.border = "1px solid #d9e1ee";
|
||||
button.style.borderRadius = "12px";
|
||||
button.style.background = "#ffffff";
|
||||
button.style.cursor = "pointer";
|
||||
button.style.textAlign = "left";
|
||||
button.onclick = onclick;
|
||||
|
||||
titleEl.textContent = String(title || "");
|
||||
titleEl.style.color = "#1f2937";
|
||||
titleEl.style.fontWeight = "700";
|
||||
|
||||
noteEl.textContent = String(note || "");
|
||||
noteEl.style.color = "#6b7280";
|
||||
noteEl.style.fontSize = "12px";
|
||||
noteEl.style.fontWeight = "500";
|
||||
|
||||
button.appendChild(titleEl);
|
||||
button.appendChild(noteEl);
|
||||
return button;
|
||||
}
|
||||
|
||||
function hideActionDialog() {
|
||||
if (actionDialogOverlay) {
|
||||
actionDialogOverlay.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
function syncActionDialog() {
|
||||
ensureActionDialog();
|
||||
while (actionDialogCard.firstChild) {
|
||||
actionDialogCard.removeChild(actionDialogCard.firstChild);
|
||||
}
|
||||
|
||||
var security = getSecurityState();
|
||||
var deleteDialogOpen = security.deleteTargetId != null;
|
||||
var addDialogOpen = !!security.pendingAddPath;
|
||||
|
||||
if (!deleteDialogOpen && !addDialogOpen) {
|
||||
hideActionDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
var title = document.createElement("h3");
|
||||
title.style.margin = "0 0 12px";
|
||||
title.style.fontSize = "18px";
|
||||
title.style.fontWeight = "700";
|
||||
actionDialogCard.appendChild(title);
|
||||
|
||||
if (deleteDialogOpen) {
|
||||
var actions = document.createElement("div");
|
||||
title.textContent = "删除禁止访问目录";
|
||||
actionDialogCard.appendChild(makeActionDialogText("确定要从保护列表中移除以下目录吗?", true));
|
||||
actionDialogCard.appendChild(makeActionDialogCode(security.deleteTargetPath || ""));
|
||||
actionDialogCard.appendChild(makeActionDialogText("请选择删除方式:", true));
|
||||
|
||||
actions.style.display = "grid";
|
||||
actions.style.gap = "10px";
|
||||
actions.appendChild(makeActionDialogOption("直接删除", "仅从列表移除,不改变目录权限", function() {
|
||||
handleSecurityDelete("direct");
|
||||
}));
|
||||
actions.appendChild(makeActionDialogOption("恢复其他用户组可访问(drwxr-xr-x)", "移除并恢复权限", function() {
|
||||
handleSecurityDelete("restore");
|
||||
}));
|
||||
actionDialogCard.appendChild(actions);
|
||||
|
||||
var deleteFooter = makeActionDialogFooter();
|
||||
deleteFooter.appendChild(makeActionDialogButton("取消", "default", function() {
|
||||
var nextSecurity = getSecurityState();
|
||||
nextSecurity.deleteTargetId = null;
|
||||
nextSecurity.deleteTargetPath = "";
|
||||
render();
|
||||
}));
|
||||
actionDialogCard.appendChild(deleteFooter);
|
||||
} else if (addDialogOpen) {
|
||||
title.textContent = "确认添加禁止访问目录";
|
||||
actionDialogCard.appendChild(makeActionDialogText("添加后将尝试修改该目录权限,限制 OpenClaw 对其访问:", true));
|
||||
actionDialogCard.appendChild(makeActionDialogCode(security.pendingAddPath || ""));
|
||||
|
||||
var warning = document.createElement("div");
|
||||
warning.style.marginTop = "4px";
|
||||
warning.style.marginBottom = "14px";
|
||||
warning.style.padding = "12px 14px";
|
||||
warning.style.border = "1px solid rgba(245, 158, 11, 0.24)";
|
||||
warning.style.borderRadius = "12px";
|
||||
warning.style.background = "rgba(245, 158, 11, 0.08)";
|
||||
|
||||
var warningTitle = document.createElement("div");
|
||||
warningTitle.textContent = "请确认你已知晓以下风险:";
|
||||
warningTitle.style.marginBottom = "8px";
|
||||
warningTitle.style.color = "#1f2937";
|
||||
warningTitle.style.fontSize = "13px";
|
||||
warningTitle.style.fontWeight = "700";
|
||||
|
||||
var warningList = document.createElement("ul");
|
||||
warningList.style.margin = "0";
|
||||
warningList.style.paddingLeft = "18px";
|
||||
warningList.style.color = "#6b7280";
|
||||
|
||||
[
|
||||
"该目录的 Linux 权限会被修改为仅允许 root 和 root 组访问。",
|
||||
"这可能影响其他服务、用户或脚本对该目录的正常访问。",
|
||||
"本功能不是容器或沙箱隔离,只是基于系统文件权限限制。",
|
||||
"请勿将 OpenClaw 自身的数据目录、程序目录加入禁止访问列表。"
|
||||
].forEach(function(text) {
|
||||
var item = document.createElement("li");
|
||||
item.textContent = text;
|
||||
warningList.appendChild(item);
|
||||
});
|
||||
|
||||
warning.appendChild(warningTitle);
|
||||
warning.appendChild(warningList);
|
||||
actionDialogCard.appendChild(warning);
|
||||
|
||||
var addFooter = makeActionDialogFooter();
|
||||
addFooter.appendChild(makeActionDialogButton("取消", "default", function() {
|
||||
var nextSecurity = getSecurityState();
|
||||
nextSecurity.pendingAddPath = "";
|
||||
render();
|
||||
}));
|
||||
addFooter.appendChild(makeActionDialogButton("确认添加", "danger", function() {
|
||||
handleSecurityAddConfirm();
|
||||
}));
|
||||
actionDialogCard.appendChild(addFooter);
|
||||
}
|
||||
|
||||
actionDialogOverlay.style.display = "flex";
|
||||
}
|
||||
|
||||
function handleSecurityAddConfirm() {
|
||||
var security = getSecurityState();
|
||||
var path = security.pendingAddPath || "";
|
||||
if (!path) {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
postJson(config.securityAddUrl, { path: path }).then(function(rv) {
|
||||
if (!rv || !rv.ok) {
|
||||
security.pendingAddPath = "";
|
||||
security.directoryError = (rv && rv.error) || "添加目录失败";
|
||||
render();
|
||||
return;
|
||||
}
|
||||
security.pendingAddPath = "";
|
||||
security.newDirectoryPath = "";
|
||||
security.directoryError = "";
|
||||
loadSecurityData();
|
||||
}).catch(function() {
|
||||
security.pendingAddPath = "";
|
||||
security.directoryError = "添加目录失败";
|
||||
render();
|
||||
});
|
||||
}
|
||||
|
||||
function handleSecurityDelete(mode) {
|
||||
var security = getSecurityState();
|
||||
if (security.deleteTargetId == null) {
|
||||
return;
|
||||
}
|
||||
postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: mode }).then(function(rv) {
|
||||
if (!rv || !rv.ok) {
|
||||
openNoticeDialog((rv && rv.error) || (mode === "restore" ? "恢复并删除失败" : "删除失败"));
|
||||
return;
|
||||
}
|
||||
security.deleteTargetId = null;
|
||||
security.deleteTargetPath = "";
|
||||
loadSecurityData();
|
||||
}).catch(function() {
|
||||
openNoticeDialog(mode === "restore" ? "恢复并删除失败" : "删除失败");
|
||||
});
|
||||
}
|
||||
|
||||
function securityDataDirectory() {
|
||||
var formBaseDir = state.form && state.form.base_dir ? String(state.form.base_dir) : "";
|
||||
var statusBaseDir = state.status && state.status.base_dir ? String(state.status.base_dir) : "";
|
||||
@ -204,6 +564,19 @@
|
||||
return formBaseDir || statusBaseDir || suggestedBaseDir || "";
|
||||
}
|
||||
|
||||
function getFormErrors() {
|
||||
state.formErrors = state.formErrors || { base_dir: "" };
|
||||
if (state.formErrors.base_dir == null) {
|
||||
state.formErrors.base_dir = "";
|
||||
}
|
||||
return state.formErrors;
|
||||
}
|
||||
|
||||
function isBaseDirErrorMessage(message) {
|
||||
var text = String(message || "");
|
||||
return text.indexOf("数据目录") >= 0 || text.indexOf("base_dir") >= 0;
|
||||
}
|
||||
|
||||
function validateProtectedDirectory(path) {
|
||||
var security = getSecurityState();
|
||||
var value = String(path || "").replace(/^\s+|\s+$/g, "");
|
||||
@ -443,7 +816,7 @@
|
||||
function showTaskLog(taskId) {
|
||||
taskId = taskId || "openclawmgr";
|
||||
if (!taskWindowAvailable()) {
|
||||
window.alert("任务窗口不可用(未加载 taskd/xterm)。请安装 luci-lib-taskd 和 luci-lib-xterm,然后强制刷新页面。");
|
||||
openNoticeDialog("任务窗口不可用(未加载 taskd/xterm)。请安装 luci-lib-taskd 和 luci-lib-xterm,然后强制刷新页面。");
|
||||
return;
|
||||
}
|
||||
var now = Date.now ? Date.now() : (+new Date());
|
||||
@ -548,6 +921,7 @@
|
||||
function render() {
|
||||
var status = state.status || {};
|
||||
var form = state.form || {};
|
||||
var formErrors = getFormErrors();
|
||||
var options = state.options || { base_dir_choices: [] };
|
||||
var security = getSecurityState();
|
||||
var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : [];
|
||||
@ -590,9 +964,6 @@
|
||||
'</div>' +
|
||||
'</div>';
|
||||
}).join("");
|
||||
var addDialogOpen = !!security.pendingAddPath;
|
||||
var deleteDialogOpen = security.deleteTargetId != null;
|
||||
|
||||
var canStartService = !status.running && !status.reachable;
|
||||
var canStopService = !!status.running;
|
||||
|
||||
@ -648,7 +1019,8 @@
|
||||
["loopback", "仅本机"],
|
||||
["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>') +
|
||||
fieldInput("数据目录", '<input class="oclm-control' + (formErrors.base_dir ? ' oclm-control-danger' : '') + '" 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 id="oclm-base-dir-error" class="oclm-security-error' + (formErrors.base_dir ? '' : ' oclm-hidden') + '">' + escapeHtml(formErrors.base_dir || "") + '</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') + '">' +
|
||||
@ -745,45 +1117,10 @@
|
||||
'<button class="oclm-button oclm-button-danger" type="button" data-op="uninstall">卸载</button>' +
|
||||
'<button class="oclm-button oclm-button-danger" type="button" data-op="purge">彻底清理</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="oclm-dialog' + (deleteDialogOpen ? '' : ' oclm-hidden') + '">' +
|
||||
'<div class="oclm-dialog-card">' +
|
||||
'<h3>删除禁止访问目录</h3>' +
|
||||
'<div class="oclm-dialog-text">确定要从保护列表中移除以下目录吗?</div>' +
|
||||
'<code class="oclm-dialog-code">' + escapeHtml(security.deleteTargetPath || "") + '</code>' +
|
||||
'<div class="oclm-dialog-text">请选择删除方式:</div>' +
|
||||
'<div class="oclm-dialog-actions">' +
|
||||
'<button class="oclm-button oclm-dialog-option" type="button" id="oclm-security-delete-direct"><span class="oclm-dialog-option-title">直接删除</span><span class="oclm-dialog-option-note">仅从列表移除,不改变目录权限</span></button>' +
|
||||
'<button class="oclm-button oclm-dialog-option" type="button" id="oclm-security-delete-restore"><span class="oclm-dialog-option-title">恢复其他用户组可访问(drwxr-xr-x)</span><span class="oclm-dialog-option-note">移除并恢复权限</span></button>' +
|
||||
'</div>' +
|
||||
'<div class="oclm-dialog-footer">' +
|
||||
'<button class="oclm-button oclm-button-danger" type="button" id="oclm-security-delete-cancel">取消</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div class="oclm-dialog' + (addDialogOpen ? '' : ' oclm-hidden') + '">' +
|
||||
'<div class="oclm-dialog-card">' +
|
||||
'<h3>确认添加禁止访问目录</h3>' +
|
||||
'<div class="oclm-dialog-text">添加后将尝试修改该目录权限,限制 OpenClaw 对其访问:</div>' +
|
||||
'<code class="oclm-dialog-code">' + escapeHtml(security.pendingAddPath || "") + '</code>' +
|
||||
'<div class="oclm-dialog-warning">' +
|
||||
'<div class="oclm-dialog-warning-title">请确认你已知晓以下风险:</div>' +
|
||||
'<ul>' +
|
||||
'<li>该目录的 Linux 权限会被修改为仅允许 root 和 root 组访问。</li>' +
|
||||
'<li>这可能影响其他服务、用户或脚本对该目录的正常访问。</li>' +
|
||||
'<li>本功能不是容器或沙箱隔离,只是基于系统文件权限限制。</li>' +
|
||||
'<li>请勿将 OpenClaw 自身的数据目录、程序目录加入禁止访问列表。</li>' +
|
||||
'</ul>' +
|
||||
'</div>' +
|
||||
'<div class="oclm-dialog-footer">' +
|
||||
'<button class="oclm-button" type="button" id="oclm-security-add-cancel">取消</button>' +
|
||||
'<button class="oclm-button oclm-button-danger" type="button" id="oclm-security-add-confirm">确认添加</button>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'</section>' +
|
||||
'</div></div></div>';
|
||||
|
||||
syncActionDialog();
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
@ -1036,7 +1373,7 @@
|
||||
showTaskLog(rv.running_task_id);
|
||||
return;
|
||||
}
|
||||
window.alert((rv && rv.error) || "操作失败");
|
||||
openNoticeDialog((rv && rv.error) || "操作失败");
|
||||
return;
|
||||
}
|
||||
state.lastTaskRunning = true;
|
||||
@ -1049,12 +1386,12 @@
|
||||
Array.prototype.forEach.call(root.querySelectorAll("[data-open-console]"), function(el) {
|
||||
el.onclick = function() {
|
||||
if (!state.consoleReady) {
|
||||
window.alert("控制台正在启动,请稍候再试。");
|
||||
openNoticeDialog("控制台正在启动,请稍候再试。");
|
||||
return;
|
||||
}
|
||||
var url = (state.status && state.status.token_url) || "";
|
||||
if (!url) {
|
||||
window.alert("控制台地址不可用");
|
||||
openNoticeDialog("控制台地址不可用");
|
||||
return;
|
||||
}
|
||||
window.open(url, "_blank", "noreferrer");
|
||||
@ -1068,7 +1405,7 @@
|
||||
var baseDirEl = root.getElementById("oclm-base-dir");
|
||||
var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : "";
|
||||
if (!baseDir) {
|
||||
window.alert("请先选择数据目录并保存应用,再执行安装。");
|
||||
openNoticeDialog("请先选择数据目录并保存应用,再执行安装。");
|
||||
if (baseDirEl) baseDirEl.focus();
|
||||
return;
|
||||
}
|
||||
@ -1083,7 +1420,7 @@
|
||||
if (!cfgRv || !cfgRv.ok) {
|
||||
state.status.installing = false;
|
||||
render();
|
||||
window.alert((cfgRv && cfgRv.error) || "保存安装选项失败");
|
||||
openNoticeDialog((cfgRv && cfgRv.error) || "保存安装选项失败");
|
||||
return;
|
||||
}
|
||||
state.form.install_accelerated = accelerated;
|
||||
@ -1099,7 +1436,7 @@
|
||||
}
|
||||
state.status.installing = false;
|
||||
render();
|
||||
window.alert((rv && rv.error) || "启动安装失败");
|
||||
openNoticeDialog((rv && rv.error) || "启动安装失败");
|
||||
return;
|
||||
}
|
||||
state.lastTaskRunning = true;
|
||||
@ -1108,7 +1445,7 @@
|
||||
}).catch(function() {
|
||||
state.status.installing = false;
|
||||
render();
|
||||
window.alert("启动安装失败");
|
||||
openNoticeDialog("启动安装失败");
|
||||
});
|
||||
};
|
||||
});
|
||||
@ -1121,7 +1458,7 @@
|
||||
var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : "";
|
||||
var op = (state.status && state.status.installed) ? "upgrade" : "install";
|
||||
if (!baseDir) {
|
||||
window.alert("请先选择数据目录并保存应用,再执行安装。");
|
||||
openNoticeDialog("请先选择数据目录并保存应用,再执行安装。");
|
||||
if (baseDirEl) baseDirEl.focus();
|
||||
return;
|
||||
}
|
||||
@ -1138,7 +1475,7 @@
|
||||
state.status.task_running = false;
|
||||
state.status.task_op = "";
|
||||
render();
|
||||
window.alert((cfgRv && cfgRv.error) || "保存安装选项失败");
|
||||
openNoticeDialog((cfgRv && cfgRv.error) || "保存安装选项失败");
|
||||
return;
|
||||
}
|
||||
state.form.install_accelerated = accelerated;
|
||||
@ -1156,7 +1493,7 @@
|
||||
state.status.task_running = false;
|
||||
state.status.task_op = "";
|
||||
render();
|
||||
window.alert((rv && rv.error) || "启动安装失败");
|
||||
openNoticeDialog((rv && rv.error) || "启动安装失败");
|
||||
return;
|
||||
}
|
||||
state.lastTaskRunning = true;
|
||||
@ -1167,7 +1504,7 @@
|
||||
state.status.task_running = false;
|
||||
state.status.task_op = "";
|
||||
render();
|
||||
window.alert("启动安装失败");
|
||||
openNoticeDialog("启动安装失败");
|
||||
});
|
||||
};
|
||||
});
|
||||
@ -1191,7 +1528,7 @@
|
||||
updateCheck.localVersion = String((rv && rv.local_version) || (state.status && state.status.openclaw_version) || "");
|
||||
updateCheck.remoteVersion = String((rv && rv.remote_version) || "");
|
||||
render();
|
||||
window.alert(updateCheck.error);
|
||||
openNoticeDialog(updateCheck.error);
|
||||
return;
|
||||
}
|
||||
updateCheck.checked = true;
|
||||
@ -1206,7 +1543,7 @@
|
||||
updateCheck.hasUpdate = false;
|
||||
updateCheck.error = "检测更新失败";
|
||||
render();
|
||||
window.alert("检测更新失败");
|
||||
openNoticeDialog("检测更新失败");
|
||||
});
|
||||
};
|
||||
});
|
||||
@ -1349,55 +1686,18 @@
|
||||
};
|
||||
}
|
||||
|
||||
var securityAddCancel = root.getElementById("oclm-security-add-cancel");
|
||||
if (securityAddCancel) {
|
||||
securityAddCancel.onclick = function() {
|
||||
var security = getSecurityState();
|
||||
security.pendingAddPath = "";
|
||||
render();
|
||||
};
|
||||
}
|
||||
|
||||
var securityAddConfirm = root.getElementById("oclm-security-add-confirm");
|
||||
if (securityAddConfirm) {
|
||||
securityAddConfirm.onclick = function() {
|
||||
var security = getSecurityState();
|
||||
var path = security.pendingAddPath || "";
|
||||
if (!path) {
|
||||
render();
|
||||
return;
|
||||
}
|
||||
postJson(config.securityAddUrl, { path: path }).then(function(rv) {
|
||||
if (!rv || !rv.ok) {
|
||||
security.pendingAddPath = "";
|
||||
security.directoryError = (rv && rv.error) || "添加目录失败";
|
||||
render();
|
||||
return;
|
||||
}
|
||||
security.pendingAddPath = "";
|
||||
security.newDirectoryPath = "";
|
||||
security.directoryError = "";
|
||||
loadSecurityData();
|
||||
}).catch(function() {
|
||||
security.pendingAddPath = "";
|
||||
security.directoryError = "添加目录失败";
|
||||
render();
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Array.prototype.forEach.call(root.querySelectorAll("[data-security-recheck]"), function(el) {
|
||||
el.onclick = function() {
|
||||
var id = el.getAttribute("data-security-recheck");
|
||||
if (!id) return;
|
||||
postJson(config.securityRecheckUrl, { id: id }).then(function(rv) {
|
||||
if (!rv || !rv.ok) {
|
||||
window.alert((rv && rv.error) || "重新检测失败");
|
||||
openNoticeDialog((rv && rv.error) || "重新检测失败");
|
||||
return;
|
||||
}
|
||||
loadSecurityData();
|
||||
}).catch(function() {
|
||||
window.alert("重新检测失败");
|
||||
openNoticeDialog("重新检测失败");
|
||||
});
|
||||
};
|
||||
});
|
||||
@ -1420,61 +1720,31 @@
|
||||
};
|
||||
});
|
||||
|
||||
var securityDeleteDirect = root.getElementById("oclm-security-delete-direct");
|
||||
if (securityDeleteDirect) {
|
||||
securityDeleteDirect.onclick = function() {
|
||||
var security = getSecurityState();
|
||||
if (security.deleteTargetId == null) return;
|
||||
postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: "direct" }).then(function(rv) {
|
||||
if (!rv || !rv.ok) {
|
||||
window.alert((rv && rv.error) || "删除失败");
|
||||
return;
|
||||
}
|
||||
security.deleteTargetId = null;
|
||||
security.deleteTargetPath = "";
|
||||
loadSecurityData();
|
||||
}).catch(function() {
|
||||
window.alert("删除失败");
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
var securityDeleteRestore = root.getElementById("oclm-security-delete-restore");
|
||||
if (securityDeleteRestore) {
|
||||
securityDeleteRestore.onclick = function() {
|
||||
var security = getSecurityState();
|
||||
if (security.deleteTargetId == null) return;
|
||||
postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: "restore" }).then(function(rv) {
|
||||
if (!rv || !rv.ok) {
|
||||
window.alert((rv && rv.error) || "恢复并删除失败");
|
||||
return;
|
||||
}
|
||||
security.deleteTargetId = null;
|
||||
security.deleteTargetPath = "";
|
||||
loadSecurityData();
|
||||
}).catch(function() {
|
||||
window.alert("恢复并删除失败");
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
var securityDeleteCancel = root.getElementById("oclm-security-delete-cancel");
|
||||
if (securityDeleteCancel) {
|
||||
securityDeleteCancel.onclick = function() {
|
||||
var security = getSecurityState();
|
||||
security.deleteTargetId = null;
|
||||
security.deleteTargetPath = "";
|
||||
render();
|
||||
};
|
||||
}
|
||||
|
||||
var saveBasic = root.getElementById("oclm-save-basic");
|
||||
var baseDirInput = root.getElementById("oclm-base-dir");
|
||||
if (baseDirInput) {
|
||||
baseDirInput.oninput = function() {
|
||||
var formErrors = getFormErrors();
|
||||
if (formErrors.base_dir) {
|
||||
formErrors.base_dir = "";
|
||||
render();
|
||||
}
|
||||
};
|
||||
}
|
||||
if (saveBasic) {
|
||||
saveBasic.onclick = function() {
|
||||
var formErrors = getFormErrors();
|
||||
var baseDirValue = root.getElementById("oclm-base-dir").value;
|
||||
if (!String(baseDirValue || "").replace(/^\s+|\s+$/g, "")) {
|
||||
formErrors.base_dir = "数据目录不能为空";
|
||||
render();
|
||||
return;
|
||||
}
|
||||
formErrors.base_dir = "";
|
||||
var payload = {
|
||||
port: root.getElementById("oclm-port").value,
|
||||
bind: root.getElementById("oclm-bind").value,
|
||||
base_dir: root.getElementById("oclm-base-dir").value,
|
||||
base_dir: baseDirValue,
|
||||
install_accelerated: !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked),
|
||||
};
|
||||
state.savingSection = "basic";
|
||||
@ -1482,7 +1752,7 @@
|
||||
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "basic"); }).catch(function() {
|
||||
state.savingSection = "";
|
||||
render();
|
||||
window.alert("保存失败");
|
||||
openNoticeDialog("保存失败");
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -1494,7 +1764,7 @@
|
||||
var modelValue = normalizeModelValue(root.getElementById("oclm-model").value, defaultModelForAgent(agentValue));
|
||||
var baseUrlValue = root.getElementById("oclm-base-url").value;
|
||||
if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) {
|
||||
window.alert("自定义供应商必须填写服务地址");
|
||||
openNoticeDialog("自定义供应商必须填写服务地址");
|
||||
return;
|
||||
}
|
||||
var payload = {
|
||||
@ -1510,7 +1780,7 @@
|
||||
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "ai"); }).catch(function() {
|
||||
state.savingSection = "";
|
||||
render();
|
||||
window.alert("保存失败");
|
||||
openNoticeDialog("保存失败");
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -1529,7 +1799,7 @@
|
||||
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "access"); }).catch(function() {
|
||||
state.savingSection = "";
|
||||
render();
|
||||
window.alert("保存失败");
|
||||
openNoticeDialog("保存失败");
|
||||
});
|
||||
};
|
||||
}
|
||||
@ -1537,16 +1807,25 @@
|
||||
|
||||
function handleSaveResult(rv, section) {
|
||||
if (!rv || !rv.ok) {
|
||||
var formErrors = getFormErrors();
|
||||
state.savingSection = "";
|
||||
if (section === "basic" && isBaseDirErrorMessage(rv && rv.error)) {
|
||||
formErrors.base_dir = (rv && rv.error) || "数据目录错误";
|
||||
render();
|
||||
return;
|
||||
}
|
||||
render();
|
||||
window.alert((rv && rv.error) || "保存失败");
|
||||
openNoticeDialog((rv && rv.error) || "保存失败");
|
||||
return;
|
||||
}
|
||||
if (section === "basic") {
|
||||
getFormErrors().base_dir = "";
|
||||
}
|
||||
postForm(config.applyUrl, {}).then(function(applyRv) {
|
||||
state.savingSection = "";
|
||||
if (!applyRv || !applyRv.ok) {
|
||||
render();
|
||||
window.alert((applyRv && applyRv.error) || "应用配置失败");
|
||||
openNoticeDialog((applyRv && applyRv.error) || "应用配置失败");
|
||||
loadConfig();
|
||||
scheduleStatusRefresh(3, 600);
|
||||
return;
|
||||
@ -1558,7 +1837,7 @@
|
||||
}).catch(function() {
|
||||
state.savingSection = "";
|
||||
render();
|
||||
window.alert("应用配置失败");
|
||||
openNoticeDialog("应用配置失败");
|
||||
loadConfig();
|
||||
scheduleStatusRefresh(3, 600);
|
||||
});
|
||||
|
||||
@ -211,6 +211,37 @@ local function trim(v)
|
||||
return tostring(v or ""):gsub("^%s+", ""):gsub("%s+$", "")
|
||||
end
|
||||
|
||||
local function normalize_dir_path(path)
|
||||
path = trim(path)
|
||||
if path == "" then
|
||||
return ""
|
||||
end
|
||||
if path ~= "/" then
|
||||
path = path:gsub("/+$", "")
|
||||
if path == "" then
|
||||
path = "/"
|
||||
end
|
||||
end
|
||||
return path
|
||||
end
|
||||
|
||||
local function validate_base_dir_input(path)
|
||||
path = normalize_dir_path(path)
|
||||
if path == "" then
|
||||
return false, "数据目录不能为空"
|
||||
end
|
||||
if not path:match("^/") then
|
||||
return false, "数据目录必须是绝对路径"
|
||||
end
|
||||
if path == "/" then
|
||||
return false, "数据目录不能使用 / 根目录"
|
||||
end
|
||||
if path == "/root" or path:match("^/root/") then
|
||||
return false, "数据目录不能使用 /root 开头的目录"
|
||||
end
|
||||
return true, nil, path
|
||||
end
|
||||
|
||||
local function split_dots(v)
|
||||
local parts = {}
|
||||
for part in tostring(v or ""):gmatch("[^%.]+") do
|
||||
@ -1231,11 +1262,13 @@ function action_config_data()
|
||||
end
|
||||
|
||||
if has("base_dir") then
|
||||
local base_dir = tostring(body.base_dir or "")
|
||||
if base_dir == "" then
|
||||
write_json({ ok = false, error = "base_dir required" })
|
||||
local ok, err, normalized = validate_base_dir_input(body.base_dir or "")
|
||||
if not ok then
|
||||
write_json({ ok = false, error = err })
|
||||
return
|
||||
end
|
||||
body.base_dir = normalized
|
||||
local base_dir = normalized
|
||||
uci:set("openclawmgr", section, "base_dir", base_dir)
|
||||
end
|
||||
|
||||
|
||||
@ -24,6 +24,9 @@ validate_base_dir() {
|
||||
*) return 1 ;;
|
||||
esac
|
||||
[ "$dir" != "/" ] || return 1
|
||||
case "$dir" in
|
||||
/root|/root/*) return 1 ;;
|
||||
esac
|
||||
case "$dir" in
|
||||
*"/../"*|*"/.."|*"/./"*|*"/.") return 1 ;;
|
||||
esac
|
||||
|
||||
@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall
|
||||
PKG_VERSION:=26.4.15
|
||||
PKG_RELEASE:=83
|
||||
PKG_RELEASE:=84
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@ -30,60 +30,61 @@ local has_trojan_plus = api.is_finded("trojan-plus")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
local has_hysteria2 = api.finded_com("hysteria")
|
||||
local allowInsecure_default = nil
|
||||
local DEFAULT_ALLOWINSECURE = true
|
||||
local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
|
||||
local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
|
||||
local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
|
||||
-- 取节点使用core类型(节点订阅页面未设置时,自动取默认)
|
||||
local ss_type_default = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local trojan_type_default = api.get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local vmess_type_default = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local vless_type_default = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local hysteria2_type_default = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local core_has = {
|
||||
["xray"] = has_xray, ["sing-box"] = has_singbox, ["shadowsocks-libev"] = has_ss,["shadowsocks-rust"] = has_ss_rust,
|
||||
["trojan-plus"] = has_trojan_plus, ["hysteria2"] = has_hysteria2
|
||||
["xray"] = has_xray,
|
||||
["sing-box"] = has_singbox,
|
||||
["shadowsocks-libev"] = has_ss,
|
||||
["shadowsocks-rust"] = has_ss_rust,
|
||||
["trojan-plus"] = has_trojan_plus,
|
||||
["hysteria2"] = has_hysteria2
|
||||
}
|
||||
----
|
||||
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
|
||||
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
|
||||
-- 判断是否过滤节点关键字
|
||||
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
|
||||
local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
|
||||
local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
|
||||
local function is_filter_keyword(value)
|
||||
if filter_keyword_mode_default == "1" then
|
||||
for k,v in ipairs(filter_keyword_discard_list_default) do
|
||||
local function is_filter_keyword(mode, discard_list, keep_list, value)
|
||||
if mode == "1" then
|
||||
for k,v in ipairs(discard_list) do
|
||||
if value:find(v, 1, true) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
elseif filter_keyword_mode_default == "2" then
|
||||
elseif mode == "2" then
|
||||
local result = true
|
||||
for k,v in ipairs(filter_keyword_keep_list_default) do
|
||||
for k,v in ipairs(keep_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = false
|
||||
end
|
||||
end
|
||||
return result
|
||||
elseif filter_keyword_mode_default == "3" then
|
||||
elseif mode == "3" then
|
||||
local result = false
|
||||
for k,v in ipairs(filter_keyword_discard_list_default) do
|
||||
for k,v in ipairs(discard_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = true
|
||||
end
|
||||
end
|
||||
for k,v in ipairs(filter_keyword_keep_list_default) do
|
||||
for k,v in ipairs(keep_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = false
|
||||
end
|
||||
end
|
||||
return result
|
||||
elseif filter_keyword_mode_default == "4" then
|
||||
elseif mode == "4" then
|
||||
local result = true
|
||||
for k,v in ipairs(filter_keyword_keep_list_default) do
|
||||
for k,v in ipairs(keep_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = false
|
||||
end
|
||||
end
|
||||
for k,v in ipairs(filter_keyword_discard_list_default) do
|
||||
for k,v in ipairs(discard_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = true
|
||||
end
|
||||
@ -448,16 +449,16 @@ local function get_subscribe_info(cfgid, value)
|
||||
end
|
||||
|
||||
-- 设置 ss 协议实现类型
|
||||
local function set_ss_implementation(result)
|
||||
if ss_type_default == "shadowsocks-libev" and has_ss then
|
||||
local function set_ss_implementation(ss_type, result)
|
||||
if ss_type == "shadowsocks-libev" and has_ss then
|
||||
result.type = "SS"
|
||||
elseif ss_type_default == "shadowsocks-rust" and has_ss_rust then
|
||||
elseif ss_type == "shadowsocks-rust" and has_ss_rust then
|
||||
result.type = 'SS-Rust'
|
||||
elseif ss_type_default == "xray" and has_xray then
|
||||
elseif ss_type == "xray" and has_xray then
|
||||
result.type = 'Xray'
|
||||
result.protocol = 'shadowsocks'
|
||||
result.transport = 'raw'
|
||||
elseif ss_type_default == "sing-box" and has_singbox then
|
||||
elseif ss_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
result.protocol = 'shadowsocks'
|
||||
else
|
||||
@ -468,8 +469,60 @@ local function set_ss_implementation(result)
|
||||
end
|
||||
|
||||
-- 处理数据
|
||||
local function processData(szType, content, add_mode, group)
|
||||
--log(content, add_mode, group)
|
||||
local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
--log(2, content, add_mode, group)
|
||||
local default_allowinsecure = DEFAULT_ALLOWINSECURE
|
||||
default_filter_keyword_mode = DEFAULT_FILTER_KEYWORD_MODE
|
||||
default_filter_keyword_discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST
|
||||
default_filter_keyword_keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST
|
||||
local default_ss_type = DEFAULT_SS_TYPE
|
||||
local default_trojan_type = DEFAULT_TROJAN_TYPE
|
||||
local default_vmess_type = DEFAULT_VMESS_TYPE
|
||||
local default_vless_type = DEFAULT_VLESS_TYPE
|
||||
local default_hysteria2_type = DEFAULT_HYSTERIA2_TYPE
|
||||
if sub_cfg then
|
||||
if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then
|
||||
default_allowinsecure = nil
|
||||
end
|
||||
local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global
|
||||
if filter_keyword_mode == "0" then
|
||||
default_filter_keyword_mode = "0"
|
||||
elseif filter_keyword_mode == "1" then
|
||||
default_filter_keyword_mode = "1"
|
||||
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
|
||||
elseif filter_keyword_mode == "2" then
|
||||
default_filter_keyword_mode = "2"
|
||||
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
|
||||
elseif filter_keyword_mode == "3" then
|
||||
default_filter_keyword_mode = "3"
|
||||
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
|
||||
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
|
||||
elseif filter_keyword_mode == "4" then
|
||||
default_filter_keyword_mode = "4"
|
||||
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
|
||||
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
|
||||
end
|
||||
local ss_type = sub_cfg.ss_type or "global"
|
||||
if ss_type ~= "global" and core_has[ss_type] then
|
||||
default_ss_type = ss_type
|
||||
end
|
||||
local trojan_type = sub_cfg.trojan_type or "global"
|
||||
if trojan_type ~= "global" and core_has[trojan_type] then
|
||||
default_trojan_type = trojan_type
|
||||
end
|
||||
local vmess_type = sub_cfg.vmess_type or "global"
|
||||
if vmess_type ~= "global" and core_has[vmess_type] then
|
||||
default_vmess_type = vmess_type
|
||||
end
|
||||
local vless_type = sub_cfg.vless_type or "global"
|
||||
if vless_type ~= "global" and core_has[vless_type] then
|
||||
default_vless_type = vless_type
|
||||
end
|
||||
local hysteria2_type = sub_cfg.hysteria2_type or "global"
|
||||
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
|
||||
default_hysteria2_type = hysteria2_type
|
||||
end
|
||||
end
|
||||
local result = {
|
||||
timeout = 60,
|
||||
add_mode = add_mode, --0为手动配置,1为导入,2为订阅
|
||||
@ -509,9 +562,9 @@ local function processData(szType, content, add_mode, group)
|
||||
result.remarks = base64Decode(params.remarks)
|
||||
elseif szType == 'vmess' then
|
||||
local info = jsonParse(content)
|
||||
if vmess_type_default == "sing-box" and has_singbox then
|
||||
if default_vmess_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
elseif vmess_type_default == "xray" and has_xray then
|
||||
elseif default_vmess_type == "xray" and has_xray then
|
||||
result.type = "Xray"
|
||||
else
|
||||
log("跳过 VMess 节点,因未适配到 VMess 核心程序,或未正确设置节点使用类型。")
|
||||
@ -606,7 +659,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.tls_CertSha = info.pcs
|
||||
result.tls_CertByName = info.vcn
|
||||
local insecure = info.allowinsecure or info.allowInsecure or info.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
else
|
||||
result.tls = "0"
|
||||
end
|
||||
@ -622,7 +675,7 @@ local function processData(szType, content, add_mode, group)
|
||||
return nil
|
||||
end
|
||||
elseif szType == "ss" then
|
||||
result = set_ss_implementation(result)
|
||||
result = set_ss_implementation(default_ss_type, result)
|
||||
if not result then return nil end
|
||||
|
||||
--SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ]
|
||||
@ -886,7 +939,7 @@ local function processData(szType, content, add_mode, group)
|
||||
end
|
||||
end
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
result.uot = params.udp
|
||||
elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then
|
||||
result.error_msg = "请更换 Xray 或 Sing-Box 来支持 SS 更多的传输方式。"
|
||||
@ -895,7 +948,7 @@ local function processData(szType, content, add_mode, group)
|
||||
|
||||
if params["shadow-tls"] then
|
||||
if result.type ~= "sing-box" and result.type ~= "SS-Rust" then
|
||||
result.error_msg = ss_type_default .. " 不支持 shadow-tls 插件。"
|
||||
result.error_msg = default_ss_type .. " 不支持 shadow-tls 插件。"
|
||||
else
|
||||
-- 解析SS Shadow-TLS 插件参数
|
||||
local function parseShadowTLSParams(b64str, out)
|
||||
@ -937,12 +990,12 @@ local function processData(szType, content, add_mode, group)
|
||||
end
|
||||
end
|
||||
elseif szType == "trojan" then
|
||||
if trojan_type_default == "trojan-plus" and has_trojan_plus then
|
||||
if default_trojan_type == "trojan-plus" and has_trojan_plus then
|
||||
result.type = "Trojan-Plus"
|
||||
elseif trojan_type_default == "sing-box" and has_singbox then
|
||||
elseif default_trojan_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
result.protocol = 'trojan'
|
||||
elseif trojan_type_default == "xray" and has_xray then
|
||||
elseif default_trojan_type == "xray" and has_xray then
|
||||
result.type = 'Xray'
|
||||
result.protocol = 'trojan'
|
||||
else
|
||||
@ -991,7 +1044,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.tls_CertSha = params.pcs
|
||||
result.tls_CertByName = params.vcn
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
|
||||
if not params.type then params.type = "tcp" end
|
||||
params.type = string.lower(params.type)
|
||||
@ -1077,7 +1130,7 @@ local function processData(szType, content, add_mode, group)
|
||||
end
|
||||
|
||||
elseif szType == "ssd" then
|
||||
result = set_ss_implementation(result)
|
||||
result = set_ss_implementation(default_ss_type, result)
|
||||
if not result then return nil end
|
||||
result.address = content.server
|
||||
result.port = content.port
|
||||
@ -1088,9 +1141,9 @@ local function processData(szType, content, add_mode, group)
|
||||
result.group = content.airport
|
||||
result.remarks = content.remarks
|
||||
elseif szType == "vless" then
|
||||
if vless_type_default == "sing-box" and has_singbox then
|
||||
if default_vless_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
elseif vless_type_default == "xray" and has_xray then
|
||||
elseif default_vless_type == "xray" and has_xray then
|
||||
result.type = "Xray"
|
||||
else
|
||||
log("跳过 VLESS 节点,因未适配到 VLESS 核心程序,或未正确设置节点使用类型。")
|
||||
@ -1243,7 +1296,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.reality_mldsa65Verify = params.pqv or nil
|
||||
end
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
end
|
||||
|
||||
result.port = port
|
||||
@ -1300,7 +1353,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.hysteria_auth_password = params.auth
|
||||
result.tls_serverName = params.peer or params.sni or ""
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
result.alpn = params.alpn
|
||||
result.hysteria_up_mbps = params.upmbps
|
||||
result.hysteria_down_mbps = params.downmbps
|
||||
@ -1346,12 +1399,12 @@ local function processData(szType, content, add_mode, group)
|
||||
result.tls_CertSha = params.pcs
|
||||
result.tls_CertByName = params.vcn
|
||||
local insecure = params.allowinsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
result.hysteria2_tls_pinSHA256 = params.pinSHA256
|
||||
result.hysteria2_hop = params.mport
|
||||
|
||||
if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then
|
||||
local is_singbox = hysteria2_type_default == "sing-box" and has_singbox
|
||||
if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then
|
||||
local is_singbox = default_hysteria2_type == "sing-box" and has_singbox
|
||||
result.type = is_singbox and 'sing-box' or 'Xray'
|
||||
result.protocol = "hysteria2"
|
||||
if params["obfs-password"] or params["obfs_password"] then
|
||||
@ -1426,7 +1479,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.tuic_congestion_control = params.congestion_control or "cubic"
|
||||
result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
|
||||
local insecure = params.allowinsecure or params.insecure or params.allow_insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
elseif szType == "anytls" then
|
||||
if has_singbox then
|
||||
result.type = 'sing-box'
|
||||
@ -1490,7 +1543,7 @@ local function processData(szType, content, add_mode, group)
|
||||
end
|
||||
result.port = port
|
||||
local insecure = params.allowinsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
end
|
||||
elseif szType == 'naive+https' or szType == 'naive+quic' then
|
||||
if has_singbox then
|
||||
@ -1829,6 +1882,30 @@ local function update_node(manual)
|
||||
for _, v in ipairs(nodeResult) do
|
||||
local remark = v["remark"]
|
||||
local list = v["list"]
|
||||
local sub_cfg = v["sub_cfg"]
|
||||
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
|
||||
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
|
||||
-- Subscription Group Chain Agent
|
||||
local function valid_chain_node(node)
|
||||
if not node then return "" end
|
||||
local cp = uci:get(appname, node, "chain_proxy") or ""
|
||||
local am = uci:get(appname, node, "add_mode") or "0"
|
||||
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
|
||||
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
|
||||
chain_node_type = ""
|
||||
return ""
|
||||
end
|
||||
return node
|
||||
end
|
||||
if sub_cfg then
|
||||
domain_resolver = sub_cfg.domain_resolver
|
||||
domain_resolver_dns = sub_cfg.domain_resolver_dns
|
||||
domain_resolver_dns_https = sub_cfg.domain_resolver_dns_https
|
||||
local map = { UseIPv4v6 = 1, UseIPv6v4 = 1, UseIPv4 = 1, UseIPv6 = 1 }
|
||||
domain_strategy = (sub_cfg.domain_strategy and map[sub_cfg.domain_strategy]) and sub_cfg.domain_strategy or nil
|
||||
preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or ""
|
||||
to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or ""
|
||||
end
|
||||
for _, vv in ipairs(list) do
|
||||
local cfgid = uci:section(appname, "nodes", api.gen_short_uuid())
|
||||
for kkk, vvv in pairs(vv) do
|
||||
@ -1916,8 +1993,12 @@ local function update_node(manual)
|
||||
end
|
||||
end
|
||||
|
||||
local function parse_link(raw, add_mode, group, cfgid)
|
||||
local function parse_link(raw, add_mode, group, sub_cfg)
|
||||
if raw and #raw > 0 then
|
||||
local cfgid
|
||||
if sub_cfg then
|
||||
cfgid = sub_cfg[".name"]
|
||||
end
|
||||
local nodes, szType
|
||||
local node_list = {}
|
||||
-- SSD 似乎是这种格式 ssd:// 开头的
|
||||
@ -1952,17 +2033,17 @@ local function parse_link(raw, add_mode, group, cfgid)
|
||||
xpcall(function ()
|
||||
local result
|
||||
if szType == 'ssd' then
|
||||
result = processData(szType, v, add_mode, group)
|
||||
result = processData(szType, v, add_mode, group, sub_cfg)
|
||||
elseif not szType then
|
||||
local node = api.trim(v)
|
||||
local dat = split(node, "://")
|
||||
if dat and dat[1] and dat[2] then
|
||||
if dat[1] == 'vmess' or dat[1] == 'ssr' then
|
||||
local link = api.trim(dat[2]:gsub("#.*$", ""))
|
||||
result = processData(dat[1], base64Decode(link), add_mode, group)
|
||||
result = processData(dat[1], base64Decode(link), add_mode, group, sub_cfg)
|
||||
else
|
||||
local link = dat[2]:gsub("&", "&"):gsub("%s*#%s*", "#") -- 一些奇葩的链接用"&"当做"&","#"前后带空格
|
||||
result = processData(dat[1], link, add_mode, group)
|
||||
result = processData(dat[1], link, add_mode, group, sub_cfg)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -1974,7 +2055,7 @@ local function parse_link(raw, add_mode, group, cfgid)
|
||||
log('丢弃节点:' .. result.remarks .. " ,原因:" .. result.error_msg)
|
||||
elseif not result.type then
|
||||
log('丢弃节点:' .. result.remarks .. " ,找不到可使用二进制。")
|
||||
elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
|
||||
elseif (add_mode == "2" and is_filter_keyword(default_filter_keyword_mode, default_filter_keyword_discard_list, default_filter_keyword_keep_list, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
|
||||
(not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then
|
||||
log('丢弃过滤节点:' .. result.type .. ' 节点,' .. result.remarks)
|
||||
else
|
||||
@ -1994,7 +2075,8 @@ local function parse_link(raw, add_mode, group, cfgid)
|
||||
if #node_list > 0 then
|
||||
nodeResult[#nodeResult + 1] = {
|
||||
remark = group,
|
||||
list = node_list
|
||||
list = node_list,
|
||||
sub_cfg = sub_cfg
|
||||
}
|
||||
end
|
||||
log('成功解析【' .. group .. '】节点数量:' .. #node_list)
|
||||
@ -2025,78 +2107,26 @@ local execute = function()
|
||||
local cfgid = value[".name"]
|
||||
local remark = value.remark or ""
|
||||
local url = value.url or ""
|
||||
if value.allowInsecure and value.allowInsecure == "1" then
|
||||
allowInsecure_default = true
|
||||
end
|
||||
local filter_keyword_mode = value.filter_keyword_mode or "5"
|
||||
if filter_keyword_mode == "0" then
|
||||
filter_keyword_mode_default = "0"
|
||||
elseif filter_keyword_mode == "1" then
|
||||
filter_keyword_mode_default = "1"
|
||||
filter_keyword_discard_list_default = value.filter_discard_list or {}
|
||||
elseif filter_keyword_mode == "2" then
|
||||
filter_keyword_mode_default = "2"
|
||||
filter_keyword_keep_list_default = value.filter_keep_list or {}
|
||||
elseif filter_keyword_mode == "3" then
|
||||
filter_keyword_mode_default = "3"
|
||||
filter_keyword_keep_list_default = value.filter_keep_list or {}
|
||||
filter_keyword_discard_list_default = value.filter_discard_list or {}
|
||||
elseif filter_keyword_mode == "4" then
|
||||
filter_keyword_mode_default = "4"
|
||||
filter_keyword_keep_list_default = value.filter_keep_list or {}
|
||||
filter_keyword_discard_list_default = value.filter_discard_list or {}
|
||||
end
|
||||
local ss_type = value.ss_type or "global"
|
||||
if ss_type ~= "global" and core_has[ss_type] then
|
||||
ss_type_default = ss_type
|
||||
end
|
||||
local trojan_type = value.trojan_type or "global"
|
||||
if trojan_type ~= "global" and core_has[trojan_type] then
|
||||
trojan_type_default = trojan_type
|
||||
end
|
||||
local vmess_type = value.vmess_type or "global"
|
||||
if vmess_type ~= "global" and core_has[vmess_type] then
|
||||
vmess_type_default = vmess_type
|
||||
end
|
||||
local vless_type = value.vless_type or "global"
|
||||
if vless_type ~= "global" and core_has[vless_type] then
|
||||
vless_type_default = vless_type
|
||||
end
|
||||
local hysteria2_type = value.hysteria2_type or "global"
|
||||
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
|
||||
hysteria2_type_default = hysteria2_type
|
||||
end
|
||||
|
||||
domain_resolver = value.domain_resolver
|
||||
domain_resolver_dns = value.domain_resolver_dns
|
||||
domain_resolver_dns_https = value.domain_resolver_dns_https
|
||||
local map = { UseIPv4v6 = 1, UseIPv6v4 = 1, UseIPv4 = 1, UseIPv6 = 1 }
|
||||
domain_strategy = (value.domain_strategy and map[value.domain_strategy]) and value.domain_strategy or nil
|
||||
|
||||
-- 订阅组链式代理
|
||||
local function valid_chain_node(node)
|
||||
if not node then return "" end
|
||||
local cp = uci:get(appname, node, "chain_proxy") or ""
|
||||
local am = uci:get(appname, node, "add_mode") or "0"
|
||||
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
|
||||
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
|
||||
chain_node_type = ""
|
||||
return ""
|
||||
end
|
||||
return node
|
||||
end
|
||||
preproxy_node_group = (value.chain_proxy == "1") and valid_chain_node(value.preproxy_node) or ""
|
||||
to_node_group = (value.chain_proxy == "2") and valid_chain_node(value.to_node) or ""
|
||||
|
||||
local ua = value.user_agent
|
||||
local access_mode = value.access_mode
|
||||
local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连访问" or (access_mode == "proxy" and "通过代理" or "自动"))
|
||||
log('正在订阅:【' .. remark .. '】' .. url .. ' [' .. result .. ']')
|
||||
local tmp_file = "/tmp/" .. cfgid
|
||||
value.http_code = curl(url, tmp_file, ua, access_mode)
|
||||
if value.http_code ~= 200 then
|
||||
fail_list[#fail_list + 1] = value
|
||||
local url_is_local
|
||||
if fs.access(url) then
|
||||
-- debug, reads local files.
|
||||
log('正在订阅:【' .. remark .. '】' .. url)
|
||||
url_is_local = true
|
||||
tmp_file = url
|
||||
else
|
||||
local ua = value.user_agent
|
||||
local access_mode = value.access_mode
|
||||
local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连" or (access_mode == "proxy" and "代理" or "自动"))
|
||||
log('正在订阅:【' .. remark .. '】' .. url .. ' [' .. result .. ']')
|
||||
tmp_file = "/tmp/" .. cfgid
|
||||
value.http_code = curl(url, tmp_file, ua, access_mode)
|
||||
if value.http_code ~= 200 then
|
||||
fail_list[#fail_list + 1] = value
|
||||
luci.sys.call("rm -f " .. tmp_file)
|
||||
end
|
||||
end
|
||||
if fs.access(tmp_file) then
|
||||
if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then
|
||||
local f = io.open(tmp_file, "r")
|
||||
local stdout = f:read("*all")
|
||||
@ -2107,33 +2137,18 @@ local execute = function()
|
||||
if not manual_sub and old_md5 == new_md5 then
|
||||
log('订阅:【' .. remark .. '】没有变化,无需更新。')
|
||||
else
|
||||
parse_link(raw_data, "2", remark, cfgid)
|
||||
parse_link(raw_data, "2", remark, value)
|
||||
uci:set(appname, cfgid, "md5", new_md5)
|
||||
end
|
||||
else
|
||||
fail_list[#fail_list + 1] = value
|
||||
end
|
||||
if url_is_local then
|
||||
value.http_code = 0
|
||||
else
|
||||
luci.sys.call("rm -f " .. tmp_file)
|
||||
end
|
||||
end
|
||||
luci.sys.call("rm -f " .. tmp_file)
|
||||
allowInsecure_default = nil
|
||||
filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
|
||||
filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
|
||||
filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
|
||||
|
||||
ss_type = uci:get(appname, "@global_subscribe[0]", "ss_type") or ""
|
||||
ss_type_default = core_has[ss_type] and ss_type or ss_type_default
|
||||
|
||||
trojan_type = uci:get(appname, "@global_subscribe[0]", "trojan_type") or ""
|
||||
trojan_type_default = core_has[trojan_type] and trojan_type or trojan_type_default
|
||||
|
||||
vmess_type = uci:get(appname, "@global_subscribe[0]", "vmess_type") or ""
|
||||
vmess_type_default = core_has[vmess_type] and vmess_type or vmess_type_default
|
||||
|
||||
vless_type = uci:get(appname, "@global_subscribe[0]", "vless_type") or ""
|
||||
vless_type_default = core_has[vless_type] and vless_type or vless_type_default
|
||||
|
||||
hysteria2_type = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or ""
|
||||
hysteria2_type_default = core_has[hysteria2_type] and hysteria2_type or hysteria2_type_default
|
||||
end
|
||||
|
||||
if #fail_list > 0 then
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=luci-app-passwall2
|
||||
PKG_VERSION:=26.4.10
|
||||
PKG_RELEASE:=37
|
||||
PKG_VERSION:=26.4.20
|
||||
PKG_RELEASE:=38
|
||||
PKG_PO_VERSION:=$(PKG_VERSION)
|
||||
|
||||
PKG_CONFIG_DEPENDS:= \
|
||||
|
||||
@ -268,8 +268,8 @@ o = s:taboption("DNS", Flag, "dns_redirect", translate("DNS Redirect"), translat
|
||||
o.default = "1"
|
||||
o.rmempty = false
|
||||
|
||||
local use_nft = m:get("@global_forwarding[0]", "use_nft") == "1"
|
||||
local set_title = api.i18n.translate(use_nft and "Clear NFTSET" or "Clear IPSET")
|
||||
local prefer_nft = m:get("@global_forwarding[0]", "prefer_nft") == "1"
|
||||
local set_title = api.i18n.translate(prefer_nft and "Clear NFTSET" or "Clear IPSET")
|
||||
o = s:taboption("DNS", DummyValue, "clear_ipset", set_title, translate("Try this feature if the rule modification does not take effect."))
|
||||
o.rawhtml = true
|
||||
function o.cfgvalue(self, section)
|
||||
|
||||
@ -48,15 +48,23 @@ o.default = ""
|
||||
o:depends("console_auth", true)
|
||||
|
||||
---- Console Port
|
||||
o = s:option(Value, "console_port", translate("Console Port"), translate(
|
||||
"In the browser input routing IP plus port access, such as:192.168.1.1:1188"))
|
||||
o.default = "1188"
|
||||
o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188"))
|
||||
o.datatype = "port"
|
||||
o.default = 1188
|
||||
o:depends("balancing_enable", true)
|
||||
|
||||
o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
|
||||
o.default = "0"
|
||||
o:depends("balancing_enable", true)
|
||||
|
||||
o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy"))
|
||||
o:value("roundrobin", "roundRobin")
|
||||
o:value("random", "random")
|
||||
o:value("leastconn", "leastConn")
|
||||
o:value("source", "source")
|
||||
o.default = "roundrobin"
|
||||
o:depends("balancing_enable", true)
|
||||
|
||||
---- Health Check Type
|
||||
o = s:option(ListValue, "health_check_type", translate("Health Check Type"))
|
||||
o.default = "passwall_logic"
|
||||
@ -77,9 +85,11 @@ o.description = translate("The URL used to detect the connection status.")
|
||||
o:depends("health_check_type", "passwall_logic")
|
||||
|
||||
---- Health Check Inter
|
||||
o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds"))
|
||||
o.default = "60"
|
||||
o = s:option(Value, "health_check_inter", translate("Health Check Inter"))
|
||||
o.datatype = "min(20)"
|
||||
o.default = 60
|
||||
o:depends("balancing_enable", true)
|
||||
o.description = translate("Units:seconds") .. " ( ≥ 20s )"
|
||||
|
||||
o = s:option(DummyValue, "health_check_tips", " ")
|
||||
o.rawhtml = true
|
||||
@ -89,11 +99,10 @@ end
|
||||
o:depends("health_check_type", "passwall_logic")
|
||||
|
||||
-- [[ Balancing Settings ]]--
|
||||
s = m:section(TypedSection, "haproxy_config", translate("Node List"),
|
||||
"<font color='red'>" ..
|
||||
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") ..
|
||||
"\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") ..
|
||||
"</font>")
|
||||
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
|
||||
s.description = "<font color='red'>" ..
|
||||
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" ..
|
||||
translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>"
|
||||
s.template = "cbi/tblsection"
|
||||
s.sortable = true
|
||||
s.anonymous = true
|
||||
@ -148,7 +157,7 @@ o.rmempty = false
|
||||
|
||||
---- Node Weight
|
||||
o = s:option(Value, "lbweight", translate("Node Weight"))
|
||||
o.datatype = "uinteger"
|
||||
o.datatype = "min(1)"
|
||||
o.default = 5
|
||||
o.rmempty = false
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ if not arg[1] or not m:get(arg[1]) then
|
||||
luci.http.redirect(api.url())
|
||||
end
|
||||
|
||||
m:append(Template(appname .. "/cbi/nodes_multivalue_com"))
|
||||
m:append(Template(appname .. "/cbi/nodes_dynamiclist_com"))
|
||||
m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
|
||||
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
@ -95,41 +95,28 @@ o.datatype = "min(1)"
|
||||
o.default = 1
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
o = s:option(MultiValue, "autoswitch_backup_node", translate("List of backup nodes"))
|
||||
o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes"))
|
||||
o:depends("enable_autoswitch", true)
|
||||
o.widget = "checkbox"
|
||||
o.template = appname .. "/cbi/nodes_multivalue"
|
||||
o.template = appname .. "/cbi/nodes_dynamiclist"
|
||||
o.group = {}
|
||||
o.write = function(self, section, value)
|
||||
local n = s.fields["node"]:formvalue(section)
|
||||
local v = type(value) == "table" and value or { value }
|
||||
local t = {}
|
||||
for _, x in ipairs(v) do
|
||||
if x and x ~= n then t[#t+1] = x end
|
||||
end
|
||||
return DynamicList.write(self, section, t)
|
||||
end
|
||||
for i, v in pairs(nodes_table) do
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = v.group or ""
|
||||
if v.protocol ~= "_shunt" then
|
||||
o:value(v.id, v.remark)
|
||||
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
s.fields["enable_autoswitch"]:depends({ node = v.id })
|
||||
end
|
||||
socks_node:value(v.id, v["remark"])
|
||||
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
|
||||
end
|
||||
-- Reading the old DynamicList
|
||||
function o.cfgvalue(self, section)
|
||||
return m.uci:get_list(appname, section, "autoswitch_backup_node") or {}
|
||||
end
|
||||
-- Write-and-hold DynamicList
|
||||
function o.write(self, section, value)
|
||||
local old = m.uci:get_list(appname, section, "autoswitch_backup_node") or {}
|
||||
local new, set = {}, {}
|
||||
for v in value:gmatch("%S+") do
|
||||
new[#new + 1] = v
|
||||
set[v] = 1
|
||||
end
|
||||
for _, v in ipairs(old) do
|
||||
if not set[v] then
|
||||
m.uci:set_list(appname, section, "autoswitch_backup_node", new)
|
||||
return
|
||||
end
|
||||
set[v] = nil
|
||||
end
|
||||
for _ in pairs(set) do
|
||||
m.uci:set_list(appname, section, "autoswitch_backup_node", new)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node."))
|
||||
o:depends("enable_autoswitch", true)
|
||||
|
||||
@ -506,33 +506,6 @@ o:depends({ [_n("transport")] = "mkcp" })
|
||||
o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
|
||||
o:depends({ [_n("mkcp_guise")] = "dns" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU"))
|
||||
o.default = "1350"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_tti"), translate("KCP TTI"))
|
||||
o.default = "20"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity"))
|
||||
o.default = "5"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity"))
|
||||
o.default = "20"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Flag, _n("mkcp_congestion"), translate("KCP Congestion"))
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_readBufferSize"), translate("KCP readBufferSize"))
|
||||
o.default = "1"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_writeBufferSize"), translate("KCP writeBufferSize"))
|
||||
o.default = "1"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed"))
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
@ -645,6 +618,14 @@ end
|
||||
|
||||
-- [[ User-Agent ]]--
|
||||
o = s:option(Value, _n("user_agent"), translate("User-Agent"))
|
||||
o.default = ""
|
||||
o:value("", translate("default"))
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome")
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
|
||||
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge")
|
||||
o:value("Go-http-client/1.1", "golang")
|
||||
o:value("curl/7.68.0", "curl")
|
||||
o:depends({ [_n("tcp_guise")] = "http" })
|
||||
o:depends({ [_n("transport")] = "ws" })
|
||||
o:depends({ [_n("transport")] = "httpupgrade" })
|
||||
|
||||
@ -416,8 +416,14 @@ if singbox_tags:find("with_quic") then
|
||||
end
|
||||
|
||||
-- [[ SSH config start ]] --
|
||||
o = s:option(Value, _n("ssh_priv_key"), translate("Private Key"))
|
||||
o = s:option(TextValue, _n("ssh_priv_key"), translate("Private Key"))
|
||||
o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ [_n("protocol")] = "ssh" })
|
||||
o.validate = function(self, value)
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value
|
||||
end
|
||||
|
||||
o = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase"))
|
||||
o.password = true
|
||||
@ -509,11 +515,7 @@ o.rows = 5
|
||||
o.wrap = "off"
|
||||
o:depends({ [_n("ech")] = true })
|
||||
o.validate = function(self, value)
|
||||
value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n")
|
||||
value = value:gsub("^%s*\n", "")
|
||||
if value:sub(-1) == "\n" then
|
||||
value = value:sub(1, -2)
|
||||
end
|
||||
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
|
||||
return value
|
||||
end
|
||||
|
||||
@ -681,6 +683,14 @@ o:depends({ [_n("grpc_health_check")] = true })
|
||||
|
||||
-- [[ User-Agent ]]--
|
||||
o = s:option(Value, _n("user_agent"), translate("User-Agent"))
|
||||
o.default = ""
|
||||
o:value("", translate("default"))
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome")
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
|
||||
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
|
||||
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge")
|
||||
o:value("Go-http-client/1.1", "golang")
|
||||
o:value("curl/7.68.0", "curl")
|
||||
o:depends({ [_n("tcp_guise")] = "http" })
|
||||
o:depends({ [_n("transport")] = "http" })
|
||||
o:depends({ [_n("transport")] = "ws" })
|
||||
|
||||
@ -356,33 +356,6 @@ o:depends({ [_n("transport")] = "mkcp" })
|
||||
o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
|
||||
o:depends({ [_n("mkcp_guise")] = "dns" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU"))
|
||||
o.default = "1350"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_tti"), translate("KCP TTI"))
|
||||
o.default = "20"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity"))
|
||||
o.default = "5"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity"))
|
||||
o.default = "20"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Flag, _n("mkcp_congestion"), translate("KCP Congestion"))
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_readBufferSize"), translate("KCP readBufferSize"))
|
||||
o.default = "1"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_writeBufferSize"), translate("KCP writeBufferSize"))
|
||||
o.default = "1"
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed"))
|
||||
o:depends({ [_n("transport")] = "mkcp" })
|
||||
|
||||
|
||||
@ -217,13 +217,12 @@ function gen_outbound(flag, node, tag, proxy_table)
|
||||
}
|
||||
} or nil,
|
||||
kcpSettings = (node.transport == "mkcp") and {
|
||||
mtu = tonumber(node.mkcp_mtu),
|
||||
tti = tonumber(node.mkcp_tti),
|
||||
uplinkCapacity = tonumber(node.mkcp_uplinkCapacity),
|
||||
downlinkCapacity = tonumber(node.mkcp_downlinkCapacity),
|
||||
congestion = (node.mkcp_congestion == "1") and true or false,
|
||||
readBufferSize = tonumber(node.mkcp_readBufferSize),
|
||||
writeBufferSize = tonumber(node.mkcp_writeBufferSize)
|
||||
mtu = 1350,
|
||||
tti = 50,
|
||||
uplinkCapacity = 12,
|
||||
downlinkCapacity = 100,
|
||||
CwndMultiplier = 1,
|
||||
MaxSendingWindow = 2 * 1024 * 1024
|
||||
} or nil,
|
||||
wsSettings = (node.transport == "ws") and {
|
||||
path = node.ws_path or "/",
|
||||
@ -675,13 +674,12 @@ function gen_config_server(node)
|
||||
}
|
||||
} or nil,
|
||||
kcpSettings = (node.transport == "mkcp") and {
|
||||
mtu = tonumber(node.mkcp_mtu),
|
||||
tti = tonumber(node.mkcp_tti),
|
||||
uplinkCapacity = tonumber(node.mkcp_uplinkCapacity),
|
||||
downlinkCapacity = tonumber(node.mkcp_downlinkCapacity),
|
||||
congestion = (node.mkcp_congestion == "1") and true or false,
|
||||
readBufferSize = tonumber(node.mkcp_readBufferSize),
|
||||
writeBufferSize = tonumber(node.mkcp_writeBufferSize)
|
||||
mtu = 1350,
|
||||
tti = 50,
|
||||
uplinkCapacity = 12,
|
||||
downlinkCapacity = 100,
|
||||
CwndMultiplier = 1,
|
||||
MaxSendingWindow = 2 * 1024 * 1024
|
||||
} or nil,
|
||||
wsSettings = (node.transport == "ws") and {
|
||||
host = node.ws_host or nil,
|
||||
|
||||
@ -0,0 +1,149 @@
|
||||
<%+cbi/valueheader%>
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
|
||||
local json = require "luci.jsonc"
|
||||
|
||||
-- 读取值(keylist/vallist/group)
|
||||
local values = {}
|
||||
for i, key in pairs(self.keylist) do
|
||||
values[#values + 1] = {
|
||||
key = key,
|
||||
label = self.vallist[i] or key,
|
||||
group = self.group and self.group[i] or nil
|
||||
}
|
||||
end
|
||||
|
||||
-- 获取当前配置值
|
||||
local current_key = {}
|
||||
local cval = self:cfgvalue(section)
|
||||
if type(cval) == "table" then
|
||||
for _, v in ipairs(cval) do
|
||||
for item in string.gmatch(v, "[^,]+") do
|
||||
current_key[#current_key+1] = item
|
||||
end
|
||||
end
|
||||
elseif type(cval) == "string" then
|
||||
for item in string.gmatch(cval, "[^,]+") do
|
||||
current_key[#current_key+1] = item
|
||||
end
|
||||
end
|
||||
|
||||
-- 分离无组节点(ungrouped)与有组节点(grouped),并保持原有顺序
|
||||
local ungrouped = {}
|
||||
local groups = {}
|
||||
local group_order = {}
|
||||
for _, item in ipairs(values) do
|
||||
if not item.group or item.group == "" then
|
||||
table.insert(ungrouped, item)
|
||||
else
|
||||
local g = item.group
|
||||
if not groups[g] then
|
||||
groups[g] = {}
|
||||
table.insert(group_order, g)
|
||||
end
|
||||
table.insert(groups[g], item)
|
||||
end
|
||||
end
|
||||
|
||||
-- 打包数据给 JS
|
||||
local dropdown_data = {
|
||||
current_key = current_key,
|
||||
ungrouped = ungrouped,
|
||||
groups = groups,
|
||||
group_order = group_order,
|
||||
cbid = cbid,
|
||||
}
|
||||
%>
|
||||
<script>
|
||||
window.dl_dropdown_data = window.dl_dropdown_data || {};
|
||||
window.dl_dropdown_data["<%=cbid%>"] = <%=json.stringify(dropdown_data)%>;
|
||||
</script>
|
||||
|
||||
<div id="<%=cbid%>.main" class="dl-dropdown-container">
|
||||
<select id="<%=cbid%>.ref" class="cbi-input-select" style="display:none !important;">
|
||||
<option value>placeholder</option>
|
||||
</select>
|
||||
|
||||
<!-- 已选节点显示区 -->
|
||||
<div id="<%=cbid%>.selected" class="dl-selected-list"></div>
|
||||
|
||||
<!-- 模拟 listvalue 控件外观 -->
|
||||
<div class="cbi-input-value cbi-input-select dl-dropdown-display" id="<%=cbid%>.display" tabindex="0">
|
||||
<span id="<%=cbid%>.label" class="dl-dropdown-label">
|
||||
<%=translate("-- Please choose --")%>
|
||||
</span>
|
||||
<span class="dl-arrow-down"></span>
|
||||
</div>
|
||||
<!-- 下拉面板 -->
|
||||
<div id="<%=cbid%>.panel" class="cbi-dynamiclist-panel dl-dropdown-panel" style="display:none;">
|
||||
<!-- 搜索框 -->
|
||||
<div style="padding:8px;border-bottom:1px solid #f0f0f0;">
|
||||
<input id="<%=cbid%>.search" class="cbi-input-text dl-dropdown-search" type="text" placeholder="🔍 <%:Search nodes...%>" inputmode="search" enterkeyhint="done" />
|
||||
</div>
|
||||
<!-- 列表容器 -->
|
||||
<div id="<%=cbid%>.list" style="padding:8px;">
|
||||
<!-- 首次点击 display 时由 JS 填充 -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
(function(){
|
||||
const cbid = "<%=cbid%>";
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const labelSpan = document.getElementById(cbid + ".label");
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const listContainer = document.getElementById(cbid + ".list");
|
||||
|
||||
function init(){
|
||||
const hidden = document.getElementById(cbid + ".hidden");
|
||||
const data = window.dl_dropdown_data[cbid];
|
||||
let arr = data.current_key;
|
||||
if (!arr || arr.length === 0) return;
|
||||
arr.forEach(key=>{
|
||||
let label = "";
|
||||
data.ungrouped.concat(...Object.values(data.groups)).forEach(v=>{
|
||||
if (v.key == key) label = v.label;
|
||||
});
|
||||
if (label)
|
||||
dl_add_item(cbid, key, label);
|
||||
});
|
||||
}
|
||||
|
||||
// 点击 display
|
||||
display.addEventListener("click", function(e){
|
||||
e.stopPropagation();
|
||||
dl_render_dropdown_list(cbid,panel,listContainer,labelSpan,searchInput,display);
|
||||
document.querySelectorAll(".cbi-dynamiclist-panel").forEach(p=>{
|
||||
if (p !== panel) p.style.display = "none";
|
||||
});
|
||||
if (panel.style.display !== "none") {
|
||||
dl_closePanel(cbid,panel,listContainer,searchInput);
|
||||
} else {
|
||||
dl_openPanel(cbid,display,panel,listContainer,searchInput);
|
||||
}
|
||||
});
|
||||
|
||||
dl_registerAdaptive(cbid);
|
||||
|
||||
init();
|
||||
|
||||
new Sortable(document.getElementById(cbid + ".selected"), {
|
||||
animation: 150,
|
||||
ghostClass: "dragging-row",
|
||||
onEnd: function(){
|
||||
dl_update_hidden(cbid);
|
||||
}
|
||||
});
|
||||
})();
|
||||
//]]>
|
||||
</script>
|
||||
|
||||
<%+cbi/valuefooter%>
|
||||
@ -0,0 +1,654 @@
|
||||
<%
|
||||
-- Template Developers:
|
||||
-- - lwb1978
|
||||
-- - snowie2000
|
||||
-- Copyright: copyright(c)2025–2027
|
||||
-- Description: Passwall(2) UI template
|
||||
-- It is the common part of the template and cannot be used independently
|
||||
%>
|
||||
<script src="<%=resource%>/view/passwall2/Sortable.min.js"></script>
|
||||
<style>
|
||||
/* 主下拉按钮的下箭头 */
|
||||
.dl-arrow-down {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 5px solid transparent;
|
||||
border-right: 5px solid transparent;
|
||||
border-top: 6px solid #666;
|
||||
margin-left: 6px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的右箭头(折叠) */
|
||||
.dl-arrow-right {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-top: 4px solid transparent;
|
||||
border-bottom: 4px solid transparent;
|
||||
border-left: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 组标题的下箭头(展开) */
|
||||
.dl-arrow-down-small {
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-left: 4px solid transparent;
|
||||
border-right: 4px solid transparent;
|
||||
border-top: 5px solid #555;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
/* 基础列表项样式 */
|
||||
.cbi-dynamiclist-panel li[data-key] {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
/* 鼠标悬停效果 - 使用透明度避免覆盖问题 */
|
||||
.cbi-dynamiclist-panel li[data-key]:hover {
|
||||
background-color: rgba(0, 123, 255, 0.1);
|
||||
}
|
||||
|
||||
.dl-dropdown-container {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
min-width: 220px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 1152px) {
|
||||
.dl-dropdown-container {
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.dl-dropdown-container {
|
||||
display: block;
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.dl-dropdown-display {
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.dl-dropdown-label,
|
||||
.dl-selected-label {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
.dl-dropdown-panel {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2147483647;
|
||||
border: 1px solid #dcdcdc;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 6px 18px rgba(0,0,0,0.08);
|
||||
max-height: 50vh;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
.dl-dropdown-search {
|
||||
width: 100%;
|
||||
max-width: 100% !important;
|
||||
min-width: 0 !important;
|
||||
box-sizing: border-box;
|
||||
padding: 6px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.dl-group-title {
|
||||
cursor: pointer;
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.dl-group-list {
|
||||
list-style: none;
|
||||
margin: 6px 0 0 8px;
|
||||
padding: 0;
|
||||
}
|
||||
.dl-group-item {
|
||||
padding: 6px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: left !important;
|
||||
}
|
||||
.dl-selected-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.dl-remove {
|
||||
margin-left: 8px;
|
||||
cursor: pointer;
|
||||
color: red;
|
||||
}
|
||||
.dragging-row {
|
||||
background-color: rgba(131, 191, 255, 0.7) !important;
|
||||
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script type="text/javascript">
|
||||
//<![CDATA[
|
||||
// css helper functions
|
||||
function dl_camelToKebab(str) {
|
||||
return str.replace(/([a-z0-9]|(?=[A-Z]))([A-Z])/g, '$1-$2').toLowerCase()
|
||||
}
|
||||
|
||||
function dl_style2Css(styleDeclaration, properties) {
|
||||
const cssRules = properties.map(prop => {
|
||||
const kebabCaseProp = dl_camelToKebab(prop);[1, 5]
|
||||
const value = styleDeclaration[prop]
|
||||
if (value) {
|
||||
return `${kebabCaseProp}: ${value};`
|
||||
}
|
||||
return ''
|
||||
})
|
||||
// Filter out any empty strings and join the rules
|
||||
return cssRules.filter(Boolean).join(' ')
|
||||
}
|
||||
|
||||
const dl_parseColorToRgba = (function() {
|
||||
// Create canvas and context once (Closure)
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||||
return function(colorStr) {
|
||||
if (!colorStr)
|
||||
return null;
|
||||
ctx.clearRect(0, 0, 1, 1);
|
||||
// 2. Apply the color
|
||||
ctx.fillStyle = colorStr;
|
||||
// 3. Fill a single pixel
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
// 4. Extract pixel data [R, G, B, A]
|
||||
const data = ctx.getImageData(0, 0, 1, 1).data;
|
||||
return {
|
||||
r: data[0],
|
||||
g: data[1],
|
||||
b: data[2],
|
||||
// Convert alpha from 0-255 to 0-1 (rounded to 3 decimal places)
|
||||
a: Math.round((data[3] / 255) * 1000) / 1000
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
// Helper to convert back to Hex (for output consistency)
|
||||
function dl_rgbToHex(r, g, b) {
|
||||
const toHex = (n) => {
|
||||
const hex = Math.max(0, Math.min(255, n)).toString(16)
|
||||
return hex.length === 1 ? '0' + hex : hex
|
||||
}
|
||||
return `#${toHex(r)}${toHex(g)}${toHex(b)}`
|
||||
}
|
||||
|
||||
function dl_isTransparent(color) {
|
||||
const cleanColor = dl_parseColorToRgba(color);
|
||||
// check #RRGGBBAA for transparency
|
||||
return !cleanColor || (cleanColor.a !== undefined && !cleanColor.a);
|
||||
}
|
||||
|
||||
function dl_getColorSchema(color) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return 'unknown'; // Handle invalid colors
|
||||
// Calculate YIQ brightness (human eye perception)
|
||||
const brightness = ((rgb.r * 299) + (rgb.g * 587) + (rgb.b * 114)) / 1000;
|
||||
|
||||
return brightness > 128 ? 'light' : 'dark';
|
||||
}
|
||||
|
||||
function dl_lighter(color, amount) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Add amount to each channel
|
||||
const r = rgb.r + amount;
|
||||
const g = rgb.g + amount;
|
||||
const b = rgb.b + amount;
|
||||
// Convert back to Hex (clamping happens inside rgbToHex)
|
||||
return dl_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
function dl_darker(color, amount) {
|
||||
const rgb = dl_parseColorToRgba(color);
|
||||
if (!rgb) return color;
|
||||
// Subtract amount from each channel
|
||||
const r = rgb.r - amount;
|
||||
const g = rgb.g - amount;
|
||||
const b = rgb.b - amount;
|
||||
|
||||
return dl_rgbToHex(r, g, b);
|
||||
}
|
||||
|
||||
// copy select styles
|
||||
function dl_adaptiveStyle(cbid) {
|
||||
const display = document.getElementById(cbid + ".display");
|
||||
const hiddenRef = document.getElementById(cbid + ".ref");
|
||||
const panel = document.getElementById(cbid + ".panel");
|
||||
if (hiddenRef && display) {
|
||||
const elOption = hiddenRef.getElementsByTagName("option")[0]
|
||||
const styleSelect = window.getComputedStyle(hiddenRef)
|
||||
const styleOption = window.getComputedStyle(elOption)
|
||||
const styleBody = window.getComputedStyle(document.body)
|
||||
|
||||
const styleNode = document.createElement('style')
|
||||
const styleNames = ["width", "color", "height", "padding", "margin", "lineHeight", "border", "borderRadius", "minWidth", "minHeight"]
|
||||
if (styleSelect.borderBottomStyle !== "none") {
|
||||
styleNames.push("borderBottomWidth", "borderBottomStyle", "borderBottomColor");
|
||||
}
|
||||
document.head.appendChild(styleNode)
|
||||
// trace back from option -> select -> body for background color
|
||||
const panelRadius = styleSelect.borderRadius;
|
||||
const optionColor = !dl_isTransparent(styleOption.backgroundColor) ? styleOption.backgroundColor : !dl_isTransparent(styleSelect.backgroundColor) ? styleSelect.backgroundColor : styleBody.backgroundColor
|
||||
const titleColor = dl_getColorSchema(optionColor) === "light" ? dl_darker(optionColor, 30) : dl_lighter(optionColor, 30)
|
||||
const selectStyleCSS = [`#${CSS.escape(cbid + ".display")}, .dl-selected-item {`, dl_style2Css(styleSelect, styleNames), dl_style2Css(styleSelect, ["backgroundColor"]), "}"]
|
||||
const optionStyleCSS = [`#${CSS.escape(cbid + ".panel")} {`, dl_style2Css(styleOption, styleNames), `background-color: ${optionColor};`, `border-radius: ${panelRadius};`, "}"]
|
||||
const titleStyleCSS = [`#${CSS.escape(cbid + ".panel")} .dl-group-title {`, `background-color: ${titleColor} !important;`, "}"]
|
||||
styleNode.textContent = [].concat(selectStyleCSS, optionStyleCSS, titleStyleCSS).join("\n")
|
||||
}
|
||||
}
|
||||
|
||||
function dl_idSafe(id) {
|
||||
return id
|
||||
.trim()
|
||||
.replace(/\s+/g, "-")
|
||||
.replace(/[\x00-\x1F\x7F]/g, "");
|
||||
}
|
||||
|
||||
// 更新组内选中计数
|
||||
function dl_updateGroupCounts(cbid, listContainer, searchInput) {
|
||||
const groups = listContainer.querySelectorAll(".dl-group");
|
||||
const isSearching = searchInput.value.trim() !== "";
|
||||
groups.forEach(group => {
|
||||
const gname = group.getAttribute("data-group");
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
const span = document.getElementById("group-count-" + cbid + "-" + gname);
|
||||
if (!span) return;
|
||||
if (isSearching) {
|
||||
// 搜索状态:显示匹配数量
|
||||
let matchCount = 0;
|
||||
items.forEach(li => {
|
||||
if (li.style.display !== "none") matchCount++;
|
||||
});
|
||||
span.textContent = "(" + matchCount + "/" + items.length + ")";
|
||||
if (matchCount > 0) {
|
||||
span.style.color = "#28a745";
|
||||
span.style.fontWeight = "600";
|
||||
} else {
|
||||
span.style.color = "#dc3545";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
} else {
|
||||
// 默认状态
|
||||
span.textContent = "(" + items.length + ")";
|
||||
span.style.color = "";
|
||||
span.style.fontWeight = "normal";
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//搜索过滤器:按 name 或 label 做模糊匹配,搜索时自动展开所有组并隐藏不匹配条目
|
||||
function dl_filterList(keyword, cbid, listContainer, searchInput) {
|
||||
keyword = (keyword || "").toLowerCase().trim();
|
||||
const topItems = listContainer.querySelectorAll("ul li[data-key]");
|
||||
topItems.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
const groups = listContainer.querySelectorAll(".dl-group");
|
||||
groups.forEach(group=>{
|
||||
const items = group.querySelectorAll("li[data-key]");
|
||||
let matchCount = 0;
|
||||
items.forEach(li=>{
|
||||
const name = (li.getAttribute("data-node-name") || "").toLowerCase();
|
||||
if (!keyword || name.indexOf(keyword) !== -1) {
|
||||
li.style.display = "block";
|
||||
matchCount++;
|
||||
} else {
|
||||
li.style.display = "none";
|
||||
}
|
||||
});
|
||||
group.style.display = (matchCount === 0 && keyword !== "") ? "none" : "block";
|
||||
const ul = group.querySelector(".dl-group-list");
|
||||
const gname = group.getAttribute("data-group");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + gname);
|
||||
if (keyword) {
|
||||
if (ul) ul.style.display = (matchCount > 0 ? "block" : "none");
|
||||
if (arrow) arrow.className = (matchCount > 0 ? "dl-arrow-down-small" : "dl-arrow-right");
|
||||
} else {
|
||||
if (ul) ul.style.display = "none";
|
||||
if (arrow) arrow.className = "dl-arrow-right";
|
||||
}
|
||||
});
|
||||
dl_updateGroupCounts(cbid, listContainer, searchInput);
|
||||
}
|
||||
|
||||
// 切换单个组(点击组标题)
|
||||
function dl_toggleGroup(listContainer, cbid, g) {
|
||||
g = dl_idSafe(g);
|
||||
const group = listContainer.querySelector(".dl-group[data-group='" + g + "']");
|
||||
if (!group) return;
|
||||
const ul = group.querySelector(".dl-group-list");
|
||||
const arrow = document.getElementById("arrow-" + cbid + "-" + g);
|
||||
if (!ul) return;
|
||||
const searchInput = document.getElementById(cbid + ".search");
|
||||
const isSearching = searchInput?.value.trim() !== "";
|
||||
const isExpanded = ul.style.display !== "none";
|
||||
|
||||
if (isExpanded) {
|
||||
ul.style.display = "none";
|
||||
if (arrow) arrow.className = "dl-arrow-right";
|
||||
} else {
|
||||
ul.style.display = "block";
|
||||
if (arrow) arrow.className = "dl-arrow-down-small";
|
||||
|
||||
if (!isSearching) {
|
||||
const allGroups = listContainer.querySelectorAll(".dl-group");
|
||||
allGroups.forEach(otherGroup => {
|
||||
if (otherGroup !== group) {
|
||||
const otherUl = otherGroup.querySelector(".dl-group-list");
|
||||
const otherGname = otherGroup.getAttribute("data-group");
|
||||
const otherArrow = document.getElementById("arrow-" + cbid + "-" + otherGname);
|
||||
if (otherUl) otherUl.style.display = "none";
|
||||
if (otherArrow) otherArrow.className = "dl-arrow-right";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 计算panel位置
|
||||
function dl_repositionPanel(panel, display) {
|
||||
if (!panel || panel.style.display === "none") return;
|
||||
const rect = display.getBoundingClientRect();
|
||||
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
||||
panel.style.visibility = "hidden";
|
||||
panel.style.display = "block";
|
||||
panel.style.minHeight = "100px";
|
||||
panel.style.maxHeight = Math.min(0.5*viewportHeight, 550) + "px";
|
||||
const panelHeight = panel.offsetHeight;
|
||||
const spaceBelow = viewportHeight - rect.bottom;
|
||||
const spaceAbove = rect.top;
|
||||
let top, isUp = false;
|
||||
if (spaceBelow >= panelHeight) {
|
||||
top = rect.bottom + 2;
|
||||
isUp = false;
|
||||
} else if (spaceAbove >= panelHeight) {
|
||||
top = rect.top - panelHeight - 2;
|
||||
isUp = true;
|
||||
} else {
|
||||
if (spaceBelow >= spaceAbove) {
|
||||
top = Math.max(rect.bottom - 2, viewportHeight - panelHeight - 2);
|
||||
isUp = false;
|
||||
} else {
|
||||
top = Math.min(rect.top - panelHeight + 2, 2);
|
||||
isUp = true;
|
||||
}
|
||||
}
|
||||
panel.style.left = rect.left + "px";
|
||||
panel.style.top = top + "px";
|
||||
const panelRect = panel.getBoundingClientRect();
|
||||
const displayWidth = rect.width;
|
||||
const remainingWidth = window.innerWidth - panelRect.left - 12;
|
||||
const maxWidth = Math.max(displayWidth, Math.floor(remainingWidth));
|
||||
panel.style.maxWidth = maxWidth + "px";
|
||||
panel.style.minWidth = displayWidth + "px";
|
||||
panel.style.width = "auto";
|
||||
panel.style.visibility = "";
|
||||
}
|
||||
|
||||
// 打开/关闭面板
|
||||
function dl_openPanel(cbid, display, panel, listContainer, searchInput) {
|
||||
if (!panel._moved) {
|
||||
document.body.appendChild(panel);
|
||||
panel._moved = true;
|
||||
}
|
||||
panel.style.display = "block";
|
||||
dl_repositionPanel(panel, display);
|
||||
// 失焦监听
|
||||
const handler = function(e){
|
||||
const target = e.target;
|
||||
if (panel.style.display !== "none") {
|
||||
if (!panel.contains(target) && !display.contains(target)) {
|
||||
dl_closePanel(cbid, panel, listContainer, searchInput, display);
|
||||
}
|
||||
}
|
||||
}
|
||||
panel._docClickHandler = handler;
|
||||
document.addEventListener("click", handler);
|
||||
// 滚动 / resize 自动 reposition
|
||||
let ticking = false;
|
||||
const repositionHandler = function () {
|
||||
if (ticking) return;
|
||||
ticking = true;
|
||||
requestAnimationFrame(function () {
|
||||
ticking = false;
|
||||
dl_repositionPanel(panel, display);
|
||||
});
|
||||
};
|
||||
panel._repositionHandler = repositionHandler;
|
||||
window.addEventListener("scroll", repositionHandler, true);
|
||||
window.addEventListener("resize", repositionHandler);
|
||||
}
|
||||
function dl_closePanel(cbid, panel, listContainer, searchInput) {
|
||||
panel.style.display = "none";
|
||||
searchInput.value = "";
|
||||
dl_filterList("", cbid, listContainer, searchInput);
|
||||
// document click
|
||||
if (panel._docClickHandler) {
|
||||
document.removeEventListener("click", panel._docClickHandler);
|
||||
panel._docClickHandler = null;
|
||||
}
|
||||
// scroll / resize
|
||||
if (panel._repositionHandler) {
|
||||
window.removeEventListener("scroll", panel._repositionHandler, true);
|
||||
window.removeEventListener("resize", panel._repositionHandler);
|
||||
panel._repositionHandler = null;
|
||||
}
|
||||
}
|
||||
|
||||
function dl_add_item(cbid, key, label) {
|
||||
const container = document.getElementById(cbid + ".selected");
|
||||
const hidden = document.getElementById(cbid + ".hidden");
|
||||
if (container.querySelector(`[data-key="${key}"]`)) return;
|
||||
const item = document.createElement("div");
|
||||
item.className = "cbi-input-value cbi-input-select dl-dropdown-display dl-selected-item";
|
||||
item.setAttribute("data-key", key);
|
||||
item.innerHTML = `
|
||||
<span class="dl-selected-label" style="cursor:grab;" title="${label}">${label}</span>
|
||||
<span class="dl-remove">×</span>
|
||||
`;
|
||||
item.querySelector(".dl-remove").onclick = function(){
|
||||
item.remove();
|
||||
dl_update_hidden(cbid);
|
||||
};
|
||||
container.appendChild(item);
|
||||
dl_update_hidden(cbid);
|
||||
}
|
||||
|
||||
function dl_update_hidden(cbid) {
|
||||
const main = document.getElementById(cbid + ".main");
|
||||
const container = document.getElementById(cbid + ".selected");
|
||||
main.querySelectorAll(`input[type="hidden"][name="${cbid}"]`).forEach(el => el.remove());
|
||||
let items = container.querySelectorAll(".dl-selected-item");
|
||||
items.forEach(el => {
|
||||
let key = el.getAttribute("data-key");
|
||||
let input = document.createElement("input");
|
||||
input.type = "hidden";
|
||||
input.name = cbid;
|
||||
input.value = key;
|
||||
main.insertBefore(input, container);
|
||||
});
|
||||
}
|
||||
|
||||
// 动态生成下拉框
|
||||
window.dl_dropdown_rendered = {};
|
||||
function dl_escape_html(s) {
|
||||
return s.replace(/[&<>"']/g, c => ({
|
||||
"&":"&", "<":"<", ">":">", '"':""", "'":"'"
|
||||
}[c]));
|
||||
}
|
||||
|
||||
function dl_render_dropdown_list(cbid, panel, listContainer, labelSpan, searchInput, display) {
|
||||
if (window.dl_dropdown_rendered[cbid]) return;
|
||||
const data = window.dl_dropdown_data[cbid];
|
||||
if (!data) return;
|
||||
if (!listContainer) return;
|
||||
|
||||
let html = "";
|
||||
|
||||
// 无组项
|
||||
if (data.ungrouped && data.ungrouped.length > 0) {
|
||||
html += `<ul style="list-style:none;padding:0;margin:0 0 8px 0;">`;
|
||||
|
||||
data.ungrouped.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${dl_escape_html(item.label.toLowerCase())}"
|
||||
class="list-item">
|
||||
<span class="dl-item-label" style="margin-left:12px;">
|
||||
${dl_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `</ul>`;
|
||||
}
|
||||
|
||||
// 分组项
|
||||
data.group_order.forEach(gname => {
|
||||
const items = data.groups[gname];
|
||||
|
||||
html += `
|
||||
<div class="dl-group" data-group="${dl_idSafe(gname)}" style="margin-bottom:8px;">
|
||||
<div class="dl-group-title" data-group-name="${dl_idSafe(gname)}">
|
||||
<span class="dl-arrow-right" id="arrow-${cbid}-${dl_idSafe(gname)}"></span>
|
||||
<b style="margin-left:6px;">${dl_escape_html(gname)}</b>
|
||||
<span id="group-count-${cbid}-${dl_idSafe(gname)}"
|
||||
style="margin-left:8px;">(${items.length})</span>
|
||||
</div>
|
||||
<ul id="group-${cbid}-${dl_idSafe(gname)}" class="dl-group-list" style="display:none">
|
||||
`;
|
||||
|
||||
items.forEach(item => {
|
||||
html += `
|
||||
<li data-key="${item.key}"
|
||||
data-node-name="${dl_escape_html(item.label.toLowerCase())}"
|
||||
class="dl-group-item">
|
||||
<span class="dl-item-label" title="${dl_escape_html(item.label)}">
|
||||
${dl_escape_html(item.label)}
|
||||
</span>
|
||||
</li>`;
|
||||
});
|
||||
|
||||
html += `
|
||||
</ul>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
listContainer.innerHTML = html;
|
||||
|
||||
window.dl_dropdown_rendered[cbid] = true;
|
||||
|
||||
dl_adaptiveStyle(cbid);
|
||||
|
||||
// 点击项(无组与组内项都使用 li[data-key])
|
||||
listContainer.addEventListener("click", function(e){
|
||||
let li = e.target;
|
||||
while(li && li !== listContainer && !li.hasAttribute('data-key')) li = li.parentNode;
|
||||
if(!li || li === listContainer) return;
|
||||
const key = li.getAttribute('data-key') || "";
|
||||
const text = li.querySelector(".dl-item-label")?.textContent || li.textContent || key;
|
||||
//改值
|
||||
dl_add_item(cbid, key, text);
|
||||
labelSpan.innerText = "<%=translate("-- Please choose --")%>";
|
||||
dl_closePanel(cbid,panel,listContainer,searchInput);
|
||||
});
|
||||
|
||||
// 搜索功能
|
||||
searchInput.addEventListener("input", function() {
|
||||
dl_filterList(this.value, cbid, listContainer, searchInput);
|
||||
dl_repositionPanel(panel, display);
|
||||
});
|
||||
searchInput.addEventListener('keydown', function(e) {
|
||||
const isEnter = e.key === "Enter" || e.keyCode === 13;
|
||||
if (!isEnter) return;
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
searchInput.blur();
|
||||
});
|
||||
|
||||
// 切换组
|
||||
listContainer.querySelectorAll(".dl-group-title").forEach(title => {
|
||||
title.addEventListener("click", function() {
|
||||
const g = this.closest(".dl-group")?.getAttribute("data-group");
|
||||
if (g) {
|
||||
dl_toggleGroup(listContainer, cbid, g);
|
||||
dl_repositionPanel(panel, display);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 防止 panel 惯性滚动穿透
|
||||
panel.addEventListener('wheel', function (e) {
|
||||
const deltaY = e.deltaY;
|
||||
const scrollTop = panel.scrollTop;
|
||||
const scrollHeight = panel.scrollHeight;
|
||||
const clientHeight = panel.clientHeight;
|
||||
const isAtTop = scrollTop === 0;
|
||||
const isAtBottom = scrollTop + clientHeight >= scrollHeight;
|
||||
if (deltaY < 0 && isAtTop) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (deltaY > 0 && isAtBottom) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
}, { passive: false });
|
||||
}
|
||||
|
||||
const dl_adaptiveControls = new Set();
|
||||
function dl_registerAdaptive(cbid) {
|
||||
dl_adaptiveControls.add(cbid);
|
||||
dl_adaptiveStyle(cbid);
|
||||
}
|
||||
let dl_adaptiveTicking = false;
|
||||
window.addEventListener("resize", () => {
|
||||
if (!dl_adaptiveTicking) {
|
||||
dl_adaptiveTicking = true;
|
||||
requestAnimationFrame(() => {
|
||||
dl_adaptiveControls.forEach(cbid => {
|
||||
dl_adaptiveStyle(cbid);
|
||||
});
|
||||
dl_adaptiveTicking = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
//]]>
|
||||
</script>
|
||||
@ -1,11 +1,23 @@
|
||||
#!/bin/sh
|
||||
|
||||
[[ "$ACTION" == "ifup" && $(uci get "passwall2.@global[0].enabled") == "1" ]] && [ -f /var/lock/passwall2_ready.lock ] && {
|
||||
default_device=$(ip route | grep default | awk -F 'dev ' '{print $2}' | awk '{print $1}')
|
||||
[ "$default_device" == "$DEVICE" ] && {
|
||||
LOCK_FILE_DIR=/var/lock
|
||||
[ ! -d ${LOCK_FILE_DIR} ] && mkdir -p ${LOCK_FILE_DIR}
|
||||
LOCK_FILE="${LOCK_FILE_DIR}/passwall2_ifup.lock"
|
||||
[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0
|
||||
|
||||
. /usr/share/passwall2/utils.sh
|
||||
|
||||
([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
|
||||
|
||||
default_device=$(ip route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
|
||||
default6_device=$(ip -6 route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
|
||||
( [ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] ) && {
|
||||
|
||||
if [ "$ACTION" = "ifupdate" ]; then
|
||||
USE_TABLES=$(get_cache_var "USE_TABLES")
|
||||
[ -n "$USE_TABLES" ] && ${APP_PATH}/${USE_TABLES}.sh update_wan_sets >/dev/null 2>&1 &
|
||||
exit 0
|
||||
fi
|
||||
|
||||
[ ! -d ${LOCK_PATH} ] && mkdir -p ${LOCK_PATH}
|
||||
LOCK_FILE="${LOCK_PATH}/${CONFIG}_ifup.lock"
|
||||
if [ -s ${LOCK_FILE} ]; then
|
||||
SPID=$(cat ${LOCK_FILE})
|
||||
if [ -e /proc/${SPID}/status ]; then
|
||||
@ -15,8 +27,8 @@
|
||||
fi
|
||||
echo $$ > ${LOCK_FILE}
|
||||
|
||||
/etc/init.d/passwall2 restart >/dev/null 2>&1 &
|
||||
logger -p notice -t network -s "passwall2: restart when $INTERFACE ifup"
|
||||
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
|
||||
logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup"
|
||||
|
||||
rm -rf ${LOCK_FILE}
|
||||
}
|
||||
|
||||
@ -933,8 +933,8 @@ start_haproxy() {
|
||||
[ "$(config_t_get global_haproxy balancing_enable 0)" != "1" ] && return
|
||||
haproxy_path=$TMP_PATH/haproxy
|
||||
haproxy_conf="config.cfg"
|
||||
lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} -dns ${LOCAL_DNS:-${AUTO_DNS}}
|
||||
ln_run ${QUEUE_RUN} "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}"
|
||||
lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf}
|
||||
ln_run 0 "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}"
|
||||
}
|
||||
|
||||
run_copy_dnsmasq() {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
#!/usr/bin/lua
|
||||
|
||||
local api = require ("luci.passwall2.api")
|
||||
local appname = "passwall2"
|
||||
local appname = api.appname
|
||||
local fs = api.fs
|
||||
local jsonc = api.jsonc
|
||||
local uci = api.uci
|
||||
@ -18,11 +18,12 @@ end
|
||||
local var = api.get_args(arg)
|
||||
local haproxy_path = var["-path"]
|
||||
local haproxy_conf = var["-conf"]
|
||||
local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53"
|
||||
local haproxy_dns = "127.0.0.1"
|
||||
|
||||
local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1"
|
||||
local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp"
|
||||
local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "10"
|
||||
local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "20"
|
||||
local balancingStrategy = uci:get(appname, "@global_haproxy[0]", "balancingStrategy") or "roundrobin"
|
||||
local console_port = uci:get(appname, "@global_haproxy[0]", "console_port")
|
||||
local bind_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0"
|
||||
local bind_address = "0.0.0.0"
|
||||
@ -38,35 +39,37 @@ local f_out = io.open(haproxy_file, "a")
|
||||
local haproxy_config = [[
|
||||
global
|
||||
daemon
|
||||
log 127.0.0.1 local2
|
||||
maxconn 60000
|
||||
stats socket {{path}}/haproxy.sock
|
||||
log 127.0.0.1 local2
|
||||
maxconn 60000
|
||||
stats socket {{path}}/haproxy.sock
|
||||
nbthread {{nbthread}}
|
||||
external-check
|
||||
insecure-fork-wanted
|
||||
spread-checks 50
|
||||
tune.max-checks-per-thread 5
|
||||
|
||||
defaults
|
||||
mode tcp
|
||||
log global
|
||||
option tcplog
|
||||
option dontlognull
|
||||
mode tcp
|
||||
log global
|
||||
option tcplog
|
||||
option dontlognull
|
||||
option http-server-close
|
||||
#option forwardfor except 127.0.0.0/8
|
||||
option redispatch
|
||||
retries 2
|
||||
timeout http-request 10s
|
||||
timeout queue 1m
|
||||
timeout connect 10s
|
||||
timeout client 1m
|
||||
timeout server 1m
|
||||
#option forwardfor except 127.0.0.0/8
|
||||
option redispatch
|
||||
retries 2
|
||||
timeout http-request 10s
|
||||
timeout queue 1m
|
||||
timeout connect 10s
|
||||
timeout client 1m
|
||||
timeout server 1m
|
||||
timeout http-keep-alive 10s
|
||||
timeout check 10s
|
||||
maxconn 3000
|
||||
|
||||
timeout check 10s
|
||||
maxconn 3000
|
||||
|
||||
resolvers mydns
|
||||
resolve_retries 1
|
||||
timeout resolve 5s
|
||||
hold valid 600s
|
||||
resolve_retries 1
|
||||
timeout resolve 5s
|
||||
hold valid 600s
|
||||
{{dns}}
|
||||
]]
|
||||
|
||||
@ -97,10 +100,12 @@ uci:foreach(appname, "haproxy_config", function(t)
|
||||
local lbss = t.lbss
|
||||
local listen_port = tonumber(t.haproxy_port) or 0
|
||||
local server_node = uci:get_all(appname, lbss)
|
||||
if server_node and server_node.address and server_node.port then
|
||||
server_remark = server_node.address .. ":" .. server_node.port
|
||||
local hop = (health_check_type == "passwall_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil
|
||||
hop = hop and hop:gsub(":", "-") or nil
|
||||
if server_node and server_node.address and (server_node.port or hop) then
|
||||
server_remark = server_node.address .. ":" .. (server_node.port or hop)
|
||||
server_address = server_node.address
|
||||
server_port = server_node.port
|
||||
server_port = server_node.port or hop
|
||||
t.origin_address = server_address
|
||||
t.origin_port = server_port
|
||||
if health_check_type == "passwall_logic" then
|
||||
@ -143,6 +148,10 @@ uci:foreach(appname, "haproxy_config", function(t)
|
||||
end
|
||||
end)
|
||||
|
||||
local inter = tonumber((health_check_inter or "20"):match("^%d+"))
|
||||
inter = (inter and inter >= 20) and inter or 20
|
||||
local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s"
|
||||
|
||||
local sortTable = {}
|
||||
for i in pairs(listens) do
|
||||
if i ~= nil then
|
||||
@ -158,14 +167,14 @@ for i, port in pairs(sortTable) do
|
||||
listen %s
|
||||
bind %s:%s
|
||||
mode tcp
|
||||
balance roundrobin
|
||||
]], port, bind_address, port))
|
||||
balance %s
|
||||
]], port, bind_address, port, balancingStrategy))
|
||||
|
||||
if health_check_type == "passwall_logic" then
|
||||
f_out:write(string.format([[
|
||||
option external-check
|
||||
external-check command "/usr/share/passwall2/haproxy_check.sh"
|
||||
]], port, port))
|
||||
external-check command "/usr/share/%s/haproxy_check.sh"
|
||||
]], appname))
|
||||
end
|
||||
|
||||
local count_M, count_B = 1, 1
|
||||
@ -180,7 +189,7 @@ listen %s
|
||||
count_B = count_B + 1
|
||||
end
|
||||
local server = o.server_address .. ":" .. o.server_port
|
||||
local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check inter {{inter}} rise 1 fall 3 {{backup}}"
|
||||
local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}"
|
||||
server_conf = server_conf:gsub("{{remark}}", remark)
|
||||
server_conf = server_conf:gsub("{{server}}", server)
|
||||
server_conf = server_conf:gsub("{{weight}}", o.lbweight)
|
||||
@ -189,13 +198,13 @@ listen %s
|
||||
resolvers = ""
|
||||
end
|
||||
server_conf = server_conf:gsub("{{resolvers}}", resolvers)
|
||||
server_conf = server_conf:gsub("{{inter}}", tonumber(health_check_inter) .. "s")
|
||||
server_conf = server_conf:gsub("{{inter_arg}}", inter_arg)
|
||||
server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "")
|
||||
|
||||
f_out:write(" " .. server_conf .. "\n")
|
||||
f_out:write(" " .. api.trim(server_conf) .. "\n")
|
||||
|
||||
if o.export ~= "0" then
|
||||
sys.call(string.format(". /usr/share/passwall2/utils.sh ; add_ip2route %s %s", o.origin_address, o.export))
|
||||
sys.call(string.format(". /usr/share/%s/utils.sh ; add_ip2route %s %s", appname, o.origin_address, o.export))
|
||||
end
|
||||
|
||||
log(2, string.format(api.i18n.translatef("Node: %s:%s, Weight: %s", o.origin_address, o.origin_port, o.lbweight)))
|
||||
|
||||
@ -24,7 +24,7 @@ if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
|
||||
extra_params="${extra_params} --retry-all-errors"
|
||||
fi
|
||||
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 2 --max-time 10 -w "%{http_code}" "${probeUrl}")
|
||||
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}")
|
||||
|
||||
case "$status" in
|
||||
200|204)
|
||||
|
||||
@ -617,6 +617,24 @@ filter_direct_node_list() {
|
||||
done
|
||||
}
|
||||
|
||||
update_wan_sets() {
|
||||
local WAN_IP=$(get_wan_ips ip4)
|
||||
[ -n "$WAN_IP" ] && {
|
||||
ipset -F "$IPSET_WAN"
|
||||
for wan_ip in $WAN_IP; do
|
||||
ipset -! add "$IPSET_WAN" "$wan_ip"
|
||||
done
|
||||
}
|
||||
|
||||
local WAN6_IP=$(get_wan_ips ip6)
|
||||
[ -n "$WAN6_IP" ] && {
|
||||
ipset -F "$IPSET_WAN6"
|
||||
for wan6_ip in $WAN6_IP; do
|
||||
ipset -! add "$IPSET_WAN6" "$wan6_ip"
|
||||
done
|
||||
}
|
||||
}
|
||||
|
||||
add_firewall_rule() {
|
||||
log_i18n 0 "Starting to load %s firewall rules..." "iptables"
|
||||
|
||||
@ -665,6 +683,8 @@ add_firewall_rule() {
|
||||
EOF
|
||||
}
|
||||
|
||||
update_wan_sets
|
||||
|
||||
[ -n "$ISP_DNS" ] && {
|
||||
for ispip in $ISP_DNS; do
|
||||
ipset -! add $IPSET_LAN $ispip
|
||||
@ -716,6 +736,7 @@ add_firewall_rule() {
|
||||
$ipt_n -N PSW2
|
||||
$ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN
|
||||
$ipt_n -A PSW2 $(dst $IPSET_VPS) -j RETURN
|
||||
$ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
|
||||
|
||||
[ "$accept_icmp" = "1" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2"
|
||||
[ -z "${is_tproxy}" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2"
|
||||
@ -747,19 +768,9 @@ add_firewall_rule() {
|
||||
$ipt_m -N PSW2
|
||||
$ipt_m -A PSW2 $(dst $IPSET_LAN) -j RETURN
|
||||
$ipt_m -A PSW2 $(dst $IPSET_VPS) -j RETURN
|
||||
$ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
|
||||
$ipt_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN
|
||||
|
||||
WAN_IP=$(get_wan_ips ip4)
|
||||
[ -n "${WAN_IP}" ] && {
|
||||
ipset -F $IPSET_WAN
|
||||
for wan_ip in $WAN_IP; do
|
||||
ipset -! add $IPSET_WAN ${wan_ip}
|
||||
done
|
||||
$ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
|
||||
$ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
|
||||
}
|
||||
unset WAN_IP wan_ip
|
||||
|
||||
insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2"
|
||||
# Only TCP, UDP Invalid.
|
||||
insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT"
|
||||
@ -807,17 +818,8 @@ add_firewall_rule() {
|
||||
$ip6t_m -N PSW2
|
||||
$ip6t_m -A PSW2 $(dst $IPSET_LAN6) -j RETURN
|
||||
$ip6t_m -A PSW2 $(dst $IPSET_VPS6) -j RETURN
|
||||
$ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN
|
||||
$ip6t_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN
|
||||
|
||||
WAN6_IP=$(get_wan_ips ip6)
|
||||
[ -n "${WAN6_IP}" ] && {
|
||||
ipset -F $IPSET_WAN6
|
||||
for wan6_ip in $WAN6_IP; do
|
||||
ipset -! add $IPSET_WAN6 ${wan6_ip}
|
||||
done
|
||||
$ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN
|
||||
}
|
||||
unset WAN6_IP wan6_ip
|
||||
|
||||
insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2"
|
||||
# Only TCP, UDP Invalid.
|
||||
@ -835,7 +837,7 @@ add_firewall_rule() {
|
||||
local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}')
|
||||
if [[ "$dns_address" == *::* ]]; then
|
||||
$ip6t_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN
|
||||
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "${dns_address}:${dns_port:-53}")"
|
||||
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "[${dns_address}]:${dns_port:-53}")"
|
||||
else
|
||||
$ipt_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN
|
||||
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "iptables" "${dns_address}:${dns_port:-53}")"
|
||||
@ -1050,7 +1052,7 @@ gen_include() {
|
||||
local __ipt=""
|
||||
[ -n "${ipt}" ] && {
|
||||
__ipt=$(cat <<- EOF
|
||||
. $UTILS_PATH
|
||||
${MY_PATH} update_wan_sets
|
||||
$ipt-save -c | grep -v "PSW2" | $ipt-restore -c
|
||||
$ipt-restore -n <<-EOT
|
||||
$(extract_rules 4 nat)
|
||||
@ -1062,21 +1064,13 @@ gen_include() {
|
||||
|
||||
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2")
|
||||
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT")
|
||||
|
||||
WAN_IP=\$(get_wan_ips ip4)
|
||||
[ ! -z "\${WAN_IP}" ] && {
|
||||
ipset -F $IPSET_WAN
|
||||
for wan_ip in \$WAN_IP; do
|
||||
ipset -! add $IPSET_WAN \${wan_ip}
|
||||
done
|
||||
}
|
||||
EOF
|
||||
)
|
||||
}
|
||||
local __ip6t=""
|
||||
[ -n "${ip6t}" ] && {
|
||||
__ip6t=$(cat <<- EOF
|
||||
. $UTILS_PATH
|
||||
${MY_PATH} update_wan_sets
|
||||
$ip6t-save -c | grep -v "PSW2" | $ip6t-restore -c
|
||||
$ip6t-restore -n <<-EOT
|
||||
$(extract_rules 6 nat)
|
||||
@ -1087,20 +1081,12 @@ gen_include() {
|
||||
|
||||
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2")
|
||||
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT")
|
||||
|
||||
WAN6_IP=\$(get_wan_ips ip6)
|
||||
[ ! -z "\${WAN6_IP}" ] && {
|
||||
ipset -F $IPSET_WAN6
|
||||
for wan6_ip in \$WAN6_IP; do
|
||||
ipset -! add $IPSET_WAN6 \${wan6_ip}
|
||||
done
|
||||
}
|
||||
EOF
|
||||
)
|
||||
}
|
||||
cat <<-EOF >> $FWI
|
||||
${__ipt}
|
||||
|
||||
|
||||
${__ip6t}
|
||||
|
||||
return 0
|
||||
@ -1158,6 +1144,9 @@ get_ip6t_bin)
|
||||
filter_direct_node_list)
|
||||
filter_direct_node_list
|
||||
;;
|
||||
update_wan_sets)
|
||||
update_wan_sets "$@"
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
|
||||
@ -680,6 +680,20 @@ mwan3_start() {
|
||||
nft list chain ip mangle mwan3_hook >/dev/null 2>&1 && nft insert rule ip mangle mwan3_hook ct mark ${FWMARK} counter return >/dev/null 2>&1
|
||||
}
|
||||
|
||||
update_wan_sets() {
|
||||
local WAN_IP=$(get_wan_ips ip4)
|
||||
[ -n "$WAN_IP" ] && {
|
||||
nft flush set $NFTABLE_NAME $NFTSET_WAN
|
||||
echo "$WAN_IP" | insert_nftset $NFTSET_WAN "-1"
|
||||
}
|
||||
|
||||
local WAN6_IP=$(get_wan_ips ip6)
|
||||
[ -n "${WAN6_IP}" ] && {
|
||||
nft flush set $NFTABLE_NAME $NFTSET_WAN6
|
||||
echo "$WAN6_IP" | insert_nftset $NFTSET_WAN6 "-1"
|
||||
}
|
||||
}
|
||||
|
||||
add_firewall_rule() {
|
||||
log_i18n 0 "Starting to load %s firewall rules..." "nftables"
|
||||
gen_nft_tables
|
||||
@ -714,6 +728,8 @@ add_firewall_rule() {
|
||||
[ -n "$lan_ip6" ] && echo $lan_ip6 | insert_nftset $NFTSET_LAN6 "-1"
|
||||
}
|
||||
|
||||
update_wan_sets
|
||||
|
||||
[ -n "$ISP_DNS" ] && {
|
||||
echo "$ISP_DNS" | insert_nftset $NFTSET_LAN 0
|
||||
for ispip in $ISP_DNS; do
|
||||
@ -835,14 +851,9 @@ add_firewall_rule() {
|
||||
nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT"
|
||||
fi
|
||||
|
||||
WAN_IP=$(get_wan_ips ip4)
|
||||
[ -n "${WAN_IP}" ] && {
|
||||
nft flush set $NFTABLE_NAME $NFTSET_WAN
|
||||
echo $WAN_IP | insert_nftset $NFTSET_WAN "-1"
|
||||
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
|
||||
}
|
||||
unset WAN_IP
|
||||
#ipv4 wan_ip
|
||||
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
|
||||
|
||||
ip rule add fwmark ${FWMARK} table 999 priority 999
|
||||
ip route add local 0.0.0.0/0 dev lo table 999
|
||||
@ -867,10 +878,11 @@ add_firewall_rule() {
|
||||
local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}')
|
||||
if [[ "$dns_address" == *::* ]]; then
|
||||
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return"
|
||||
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "[${dns_address}]:${dns_port:-53}")"
|
||||
else
|
||||
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return"
|
||||
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")"
|
||||
fi
|
||||
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")"
|
||||
done
|
||||
}
|
||||
|
||||
@ -879,13 +891,7 @@ add_firewall_rule() {
|
||||
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto {ipv6} counter jump PSW2_MANGLE_V6"
|
||||
nft "add rule $NFTABLE_NAME mangle_output meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\""
|
||||
|
||||
WAN6_IP=$(get_wan_ips ip6)
|
||||
[ -n "${WAN6_IP}" ] && {
|
||||
nft flush set $NFTABLE_NAME $NFTSET_WAN6
|
||||
echo $WAN6_IP | insert_nftset $NFTSET_WAN6 "-1"
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
|
||||
}
|
||||
unset WAN6_IP
|
||||
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
|
||||
|
||||
ip -6 rule add fwmark ${FWMARK} table 999 priority 999
|
||||
ip -6 route add local ::/0 dev lo table 999
|
||||
@ -894,7 +900,7 @@ add_firewall_rule() {
|
||||
[ "$ENABLED_DEFAULT_ACL" == 1 ] && {
|
||||
TCP_LOCALHOST_PROXY=$LOCALHOST_PROXY
|
||||
UDP_LOCALHOST_PROXY=$LOCALHOST_PROXY
|
||||
|
||||
|
||||
msg="$(i18n "[Local],")"
|
||||
[ "$TCP_NO_REDIR_PORTS" != "disable" ] && {
|
||||
nft "add rule $NFTABLE_NAME $nft_output_chain ip protocol tcp $(factor $TCP_NO_REDIR_PORTS "tcp dport") counter return"
|
||||
@ -906,7 +912,7 @@ add_firewall_rule() {
|
||||
log 1 "${msg}$(i18n "not proxy all %s" "TCP")"
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
[ "$UDP_NO_REDIR_PORTS" != "disable" ] && {
|
||||
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return"
|
||||
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return"
|
||||
@ -1088,20 +1094,9 @@ gen_include() {
|
||||
|
||||
local __nft=" "
|
||||
__nft=$(cat <<- EOF
|
||||
. $UTILS_PATH
|
||||
[ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW2)" ] && nft -f ${nft_chain_file}
|
||||
WAN_IP=\$(get_wan_ips ip4)
|
||||
[ ! -z "\${WAN_IP}" ] && {
|
||||
nft flush set $NFTABLE_NAME $NFTSET_WAN
|
||||
echo "\${WAN_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1"
|
||||
}
|
||||
[ "$PROXY_IPV6" == "1" ] && {
|
||||
WAN6_IP=\$(get_wan_ips ip6)
|
||||
[ ! -z "\${WAN6_IP}" ] && {
|
||||
nft flush set $NFTABLE_NAME $NFTSET_WAN6
|
||||
echo "\${WAN6_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1"
|
||||
}
|
||||
}
|
||||
|
||||
${MY_PATH} update_wan_sets
|
||||
EOF
|
||||
)
|
||||
|
||||
@ -1150,6 +1145,9 @@ mwan3_start)
|
||||
mwan3_stop)
|
||||
mwan3_stop
|
||||
;;
|
||||
update_wan_sets)
|
||||
update_wan_sets "$@"
|
||||
;;
|
||||
stop)
|
||||
stop
|
||||
;;
|
||||
|
||||
@ -31,59 +31,60 @@ local has_ssr = api.is_finded("ssr-local") and api.is_finded("ssr-redir")
|
||||
local has_singbox = api.finded_com("sing-box")
|
||||
local has_xray = api.finded_com("xray")
|
||||
local has_hysteria2 = api.finded_com("hysteria")
|
||||
local allowInsecure_default = true
|
||||
local DEFAULT_ALLOWINSECURE = true
|
||||
local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
|
||||
local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
|
||||
local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
|
||||
-- Nodes should be retrieved using the core type (if not set on the node subscription page, the default type will be used automatically).
|
||||
local ss_type_default = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local trojan_type_default = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local vmess_type_default = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local vless_type_default = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local hysteria2_type_default = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
|
||||
local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
|
||||
local core_has = {
|
||||
["xray"] = has_xray, ["sing-box"] = has_singbox, ["shadowsocks-libev"] = has_ss,["shadowsocks-rust"] = has_ss_rust,
|
||||
["xray"] = has_xray,
|
||||
["sing-box"] = has_singbox,
|
||||
["shadowsocks-libev"] = has_ss,
|
||||
["shadowsocks-rust"] = has_ss_rust,
|
||||
["hysteria2"] = has_hysteria2
|
||||
}
|
||||
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
|
||||
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
|
||||
-- Determine whether to filter node keywords
|
||||
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
|
||||
local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
|
||||
local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
|
||||
local function is_filter_keyword(value)
|
||||
if filter_keyword_mode_default == "1" then
|
||||
for k,v in ipairs(filter_keyword_discard_list_default) do
|
||||
local function is_filter_keyword(mode, discard_list, keep_list, value)
|
||||
if mode == "1" then
|
||||
for k,v in ipairs(discard_list) do
|
||||
if value:find(v, 1, true) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
elseif filter_keyword_mode_default == "2" then
|
||||
elseif mode == "2" then
|
||||
local result = true
|
||||
for k,v in ipairs(filter_keyword_keep_list_default) do
|
||||
for k,v in ipairs(keep_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = false
|
||||
end
|
||||
end
|
||||
return result
|
||||
elseif filter_keyword_mode_default == "3" then
|
||||
elseif mode == "3" then
|
||||
local result = false
|
||||
for k,v in ipairs(filter_keyword_discard_list_default) do
|
||||
for k,v in ipairs(discard_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = true
|
||||
end
|
||||
end
|
||||
for k,v in ipairs(filter_keyword_keep_list_default) do
|
||||
for k,v in ipairs(keep_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = false
|
||||
end
|
||||
end
|
||||
return result
|
||||
elseif filter_keyword_mode_default == "4" then
|
||||
elseif mode == "4" then
|
||||
local result = true
|
||||
for k,v in ipairs(filter_keyword_keep_list_default) do
|
||||
for k,v in ipairs(keep_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = false
|
||||
end
|
||||
end
|
||||
for k,v in ipairs(filter_keyword_discard_list_default) do
|
||||
for k,v in ipairs(discard_list) do
|
||||
if value:find(v, 1, true) then
|
||||
result = true
|
||||
end
|
||||
@ -425,16 +426,16 @@ local function get_subscribe_info(cfgid, value)
|
||||
end
|
||||
|
||||
-- Configure the SS protocol implementation type
|
||||
local function set_ss_implementation(result)
|
||||
if ss_type_default == "shadowsocks-libev" and has_ss then
|
||||
local function set_ss_implementation(ss_type, result)
|
||||
if ss_type == "shadowsocks-libev" and has_ss then
|
||||
result.type = "SS"
|
||||
elseif ss_type_default == "shadowsocks-rust" and has_ss_rust then
|
||||
elseif ss_type == "shadowsocks-rust" and has_ss_rust then
|
||||
result.type = 'SS-Rust'
|
||||
elseif ss_type_default == "xray" and has_xray then
|
||||
elseif ss_type == "xray" and has_xray then
|
||||
result.type = 'Xray'
|
||||
result.protocol = 'shadowsocks'
|
||||
result.transport = 'raw'
|
||||
elseif ss_type_default == "sing-box" and has_singbox then
|
||||
elseif ss_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
result.protocol = 'shadowsocks'
|
||||
else
|
||||
@ -445,8 +446,60 @@ local function set_ss_implementation(result)
|
||||
end
|
||||
|
||||
-- Processing data
|
||||
local function processData(szType, content, add_mode, group)
|
||||
local function processData(szType, content, add_mode, group, sub_cfg)
|
||||
--log(2, content, add_mode, group)
|
||||
local default_allowinsecure = DEFAULT_ALLOWINSECURE
|
||||
local default_filter_keyword_mode = DEFAULT_FILTER_KEYWORD_MODE
|
||||
local default_filter_keyword_discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST
|
||||
local default_filter_keyword_keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST
|
||||
local default_ss_type = DEFAULT_SS_TYPE
|
||||
local default_trojan_type = DEFAULT_TROJAN_TYPE
|
||||
local default_vmess_type = DEFAULT_VMESS_TYPE
|
||||
local default_vless_type = DEFAULT_VLESS_TYPE
|
||||
local default_hysteria2_type = DEFAULT_HYSTERIA2_TYPE
|
||||
if sub_cfg then
|
||||
if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then
|
||||
default_allowinsecure = nil
|
||||
end
|
||||
local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global
|
||||
if filter_keyword_mode == "0" then
|
||||
default_filter_keyword_mode = "0"
|
||||
elseif filter_keyword_mode == "1" then
|
||||
default_filter_keyword_mode = "1"
|
||||
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
|
||||
elseif filter_keyword_mode == "2" then
|
||||
default_filter_keyword_mode = "2"
|
||||
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
|
||||
elseif filter_keyword_mode == "3" then
|
||||
default_filter_keyword_mode = "3"
|
||||
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
|
||||
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
|
||||
elseif filter_keyword_mode == "4" then
|
||||
default_filter_keyword_mode = "4"
|
||||
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
|
||||
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
|
||||
end
|
||||
local ss_type = sub_cfg.ss_type or "global"
|
||||
if ss_type ~= "global" and core_has[ss_type] then
|
||||
default_ss_type = ss_type
|
||||
end
|
||||
local trojan_type = sub_cfg.trojan_type or "global"
|
||||
if trojan_type ~= "global" and core_has[trojan_type] then
|
||||
default_trojan_type = trojan_type
|
||||
end
|
||||
local vmess_type = sub_cfg.vmess_type or "global"
|
||||
if vmess_type ~= "global" and core_has[vmess_type] then
|
||||
default_vmess_type = vmess_type
|
||||
end
|
||||
local vless_type = sub_cfg.vless_type or "global"
|
||||
if vless_type ~= "global" and core_has[vless_type] then
|
||||
default_vless_type = vless_type
|
||||
end
|
||||
local hysteria2_type = sub_cfg.hysteria2_type or "global"
|
||||
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
|
||||
default_hysteria2_type = hysteria2_type
|
||||
end
|
||||
end
|
||||
local result = {
|
||||
timeout = 60,
|
||||
add_mode = add_mode, -- `0` for manual configuration, `1` for import, `2` for subscription
|
||||
@ -486,9 +539,9 @@ local function processData(szType, content, add_mode, group)
|
||||
result.remarks = base64Decode(params.remarks)
|
||||
elseif szType == 'vmess' then
|
||||
local info = jsonParse(content)
|
||||
if vmess_type_default == "sing-box" and has_singbox then
|
||||
if default_vmess_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
elseif vmess_type_default == "xray" and has_xray then
|
||||
elseif default_vmess_type == "xray" and has_xray then
|
||||
result.type = "Xray"
|
||||
else
|
||||
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VMess", "VMess"))
|
||||
@ -559,12 +612,7 @@ local function processData(szType, content, add_mode, group)
|
||||
if info.net == 'kcp' or info.net == 'mkcp' then
|
||||
info.net = "mkcp"
|
||||
result.mkcp_guise = info.type
|
||||
result.mkcp_mtu = 1350
|
||||
result.mkcp_tti = 50
|
||||
result.mkcp_uplinkCapacity = 5
|
||||
result.mkcp_downlinkCapacity = 20
|
||||
result.mkcp_readBufferSize = 2
|
||||
result.mkcp_writeBufferSize = 2
|
||||
result.mkcp_seed = info.seed
|
||||
end
|
||||
if info.net == 'quic' then
|
||||
result.quic_guise = info.type
|
||||
@ -599,7 +647,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.tls_CertSha = info.pcs
|
||||
result.tls_CertByName = info.vcn
|
||||
local insecure = info.allowinsecure or info.allowInsecure or info.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
else
|
||||
result.tls = "0"
|
||||
end
|
||||
@ -615,7 +663,7 @@ local function processData(szType, content, add_mode, group)
|
||||
return nil
|
||||
end
|
||||
elseif szType == "ss" then
|
||||
result = set_ss_implementation(result)
|
||||
result = set_ss_implementation(default_ss_type, result)
|
||||
if not result then return nil end
|
||||
|
||||
--SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ]
|
||||
@ -825,12 +873,6 @@ local function processData(szType, content, add_mode, group)
|
||||
if params.type == 'kcp' or params.type == 'mkcp' then
|
||||
result.transport = "mkcp"
|
||||
result.mkcp_guise = params.headerType or "none"
|
||||
result.mkcp_mtu = 1350
|
||||
result.mkcp_tti = 50
|
||||
result.mkcp_uplinkCapacity = 5
|
||||
result.mkcp_downlinkCapacity = 20
|
||||
result.mkcp_readBufferSize = 2
|
||||
result.mkcp_writeBufferSize = 2
|
||||
result.mkcp_seed = params.seed
|
||||
end
|
||||
if params.type == 'quic' then
|
||||
@ -884,7 +926,7 @@ local function processData(szType, content, add_mode, group)
|
||||
end
|
||||
end
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
result.uot = params.udp
|
||||
elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then
|
||||
result.error_msg = i18n.translatef("Please replace Xray or Sing-Box to support more transmission methods in Shadowsocks.")
|
||||
@ -893,7 +935,7 @@ local function processData(szType, content, add_mode, group)
|
||||
|
||||
if params["shadow-tls"] then
|
||||
if result.type ~= "sing-box" and result.type ~= "SS-Rust" then
|
||||
result.error_msg = ss_type_default .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls")
|
||||
result.error_msg = default_ss_type .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls")
|
||||
else
|
||||
-- Parsing SS Shadow-TLS plugin parameters
|
||||
local function parseShadowTLSParams(b64str, out)
|
||||
@ -935,10 +977,10 @@ local function processData(szType, content, add_mode, group)
|
||||
end
|
||||
end
|
||||
elseif szType == "trojan" then
|
||||
if trojan_type_default == "sing-box" and has_singbox then
|
||||
if default_trojan_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
result.protocol = 'trojan'
|
||||
elseif trojan_type_default == "xray" and has_xray then
|
||||
elseif default_trojan_type == "xray" and has_xray then
|
||||
result.type = 'Xray'
|
||||
result.protocol = 'trojan'
|
||||
else
|
||||
@ -988,7 +1030,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.tls_CertSha = params.pcs
|
||||
result.tls_CertByName = params.vcn
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
|
||||
if not params.type then params.type = "tcp" end
|
||||
params.type = string.lower(params.type)
|
||||
@ -1042,12 +1084,6 @@ local function processData(szType, content, add_mode, group)
|
||||
if params.type == 'kcp' or params.type == 'mkcp' then
|
||||
result.transport = "mkcp"
|
||||
result.mkcp_guise = params.headerType or "none"
|
||||
result.mkcp_mtu = 1350
|
||||
result.mkcp_tti = 50
|
||||
result.mkcp_uplinkCapacity = 5
|
||||
result.mkcp_downlinkCapacity = 20
|
||||
result.mkcp_readBufferSize = 2
|
||||
result.mkcp_writeBufferSize = 2
|
||||
result.mkcp_seed = params.seed
|
||||
end
|
||||
if params.type == 'quic' then
|
||||
@ -1079,7 +1115,7 @@ local function processData(szType, content, add_mode, group)
|
||||
end
|
||||
end
|
||||
elseif szType == "ssd" then
|
||||
result = set_ss_implementation(result)
|
||||
result = set_ss_implementation(default_ss_type, result)
|
||||
if not result then return nil end
|
||||
result.address = content.server
|
||||
result.port = content.port
|
||||
@ -1090,9 +1126,9 @@ local function processData(szType, content, add_mode, group)
|
||||
result.group = content.airport
|
||||
result.remarks = content.remarks
|
||||
elseif szType == "vless" then
|
||||
if vless_type_default == "sing-box" and has_singbox then
|
||||
if default_vless_type == "sing-box" and has_singbox then
|
||||
result.type = 'sing-box'
|
||||
elseif vless_type_default == "xray" and has_xray then
|
||||
elseif default_vless_type == "xray" and has_xray then
|
||||
result.type = "Xray"
|
||||
else
|
||||
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VLESS", "VLESS"))
|
||||
@ -1189,12 +1225,7 @@ local function processData(szType, content, add_mode, group)
|
||||
if params.type == 'kcp' or params.type == 'mkcp' then
|
||||
result.transport = "mkcp"
|
||||
result.mkcp_guise = params.headerType or "none"
|
||||
result.mkcp_mtu = 1350
|
||||
result.mkcp_tti = 50
|
||||
result.mkcp_uplinkCapacity = 5
|
||||
result.mkcp_downlinkCapacity = 20
|
||||
result.mkcp_readBufferSize = 2
|
||||
result.mkcp_writeBufferSize = 2
|
||||
result.mkcp_seed = params.seed
|
||||
end
|
||||
if params.type == 'quic' then
|
||||
result.quic_guise = params.headerType or "none"
|
||||
@ -1250,7 +1281,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.reality_mldsa65Verify = params.pqv or nil
|
||||
end
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
end
|
||||
|
||||
result.port = port
|
||||
@ -1307,7 +1338,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.hysteria_auth_password = params.auth
|
||||
result.tls_serverName = params.peer or params.sni or ""
|
||||
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
result.alpn = params.alpn
|
||||
result.hysteria_up_mbps = params.upmbps
|
||||
result.hysteria_down_mbps = params.downmbps
|
||||
@ -1353,12 +1384,12 @@ local function processData(szType, content, add_mode, group)
|
||||
result.tls_CertSha = params.pcs
|
||||
result.tls_CertByName = params.vcn
|
||||
local insecure = params.allowinsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
result.hysteria2_tls_pinSHA256 = params.pinSHA256
|
||||
result.hysteria2_hop = params.mport
|
||||
|
||||
if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then
|
||||
local is_singbox = hysteria2_type_default == "sing-box" and has_singbox
|
||||
if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then
|
||||
local is_singbox = default_hysteria2_type == "sing-box" and has_singbox
|
||||
result.type = is_singbox and 'sing-box' or 'Xray'
|
||||
result.protocol = "hysteria2"
|
||||
if params["obfs-password"] or params["obfs_password"] then
|
||||
@ -1433,7 +1464,7 @@ local function processData(szType, content, add_mode, group)
|
||||
result.tuic_congestion_control = params.congestion_control or "cubic"
|
||||
result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
|
||||
local insecure = params.allowinsecure or params.insecure or params.allow_insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
elseif szType == "anytls" then
|
||||
if has_singbox then
|
||||
result.type = 'sing-box'
|
||||
@ -1497,7 +1528,7 @@ local function processData(szType, content, add_mode, group)
|
||||
end
|
||||
result.port = port
|
||||
local insecure = params.allowinsecure or params.insecure
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
|
||||
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
|
||||
end
|
||||
elseif szType == 'naive+https' or szType == 'naive+quic' then
|
||||
if has_singbox then
|
||||
@ -1840,6 +1871,29 @@ local function update_node(manual)
|
||||
for _, v in ipairs(nodeResult) do
|
||||
local remark = v["remark"]
|
||||
local list = v["list"]
|
||||
local sub_cfg = v["sub_cfg"]
|
||||
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
|
||||
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
|
||||
-- Subscription Group Chain Agent
|
||||
local function valid_chain_node(node)
|
||||
if not node then return "" end
|
||||
local cp = uci:get(appname, node, "chain_proxy") or ""
|
||||
local am = uci:get(appname, node, "add_mode") or "0"
|
||||
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
|
||||
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
|
||||
chain_node_type = ""
|
||||
return ""
|
||||
end
|
||||
return node
|
||||
end
|
||||
if sub_cfg then
|
||||
domain_resolver = sub_cfg.domain_resolver
|
||||
domain_resolver_dns = sub_cfg.domain_resolver_dns
|
||||
domain_resolver_dns_https = sub_cfg.domain_resolver_dns_https
|
||||
domain_strategy = (sub_cfg.domain_strategy == "UseIPv4" or sub_cfg.domain_strategy == "UseIPv6") and sub_cfg.domain_strategy or nil
|
||||
preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or ""
|
||||
to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or ""
|
||||
end
|
||||
for _, vv in ipairs(list) do
|
||||
local cfgid = uci:section(appname, "nodes", api.gen_short_uuid())
|
||||
for kkk, vvv in pairs(vv) do
|
||||
@ -1929,8 +1983,12 @@ local function update_node(manual)
|
||||
end
|
||||
end
|
||||
|
||||
local function parse_link(raw, add_mode, group, cfgid)
|
||||
local function parse_link(raw, add_mode, group, sub_cfg)
|
||||
if raw and #raw > 0 then
|
||||
local cfgid
|
||||
if sub_cfg then
|
||||
cfgid = sub_cfg[".name"]
|
||||
end
|
||||
local nodes, szType
|
||||
local node_list = {}
|
||||
-- ssd appear to be in this format, starting with ssd://.
|
||||
@ -1965,17 +2023,17 @@ local function parse_link(raw, add_mode, group, cfgid)
|
||||
xpcall(function ()
|
||||
local result
|
||||
if szType == 'ssd' then
|
||||
result = processData(szType, v, add_mode, group)
|
||||
result = processData(szType, v, add_mode, group, sub_cfg)
|
||||
elseif not szType then
|
||||
local node = api.trim(v)
|
||||
local dat = split(node, "://")
|
||||
if dat and dat[1] and dat[2] then
|
||||
if dat[1] == 'vmess' or dat[1] == 'ssr' then
|
||||
local link = api.trim(dat[2]:gsub("#.*$", ""))
|
||||
result = processData(dat[1], base64Decode(link), add_mode, group)
|
||||
result = processData(dat[1], base64Decode(link), add_mode, group, sub_cfg)
|
||||
else
|
||||
local link = dat[2]:gsub("&", "&"):gsub("%s*#%s*", "#") -- Some odd links use "&" as "&", and include spaces before and after "#".
|
||||
result = processData(dat[1], link, add_mode, group)
|
||||
result = processData(dat[1], link, add_mode, group, sub_cfg)
|
||||
end
|
||||
end
|
||||
else
|
||||
@ -1987,7 +2045,7 @@ local function parse_link(raw, add_mode, group, cfgid)
|
||||
log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. result.error_msg)
|
||||
elseif not result.type then
|
||||
log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. i18n.translatef("No usable binary was found."))
|
||||
elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
|
||||
elseif (add_mode == "2" and is_filter_keyword(default_filter_keyword_mode, default_filter_keyword_discard_list, default_filter_keyword_keep_list, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
|
||||
(not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then
|
||||
log(2, i18n.translatef("Discard filter nodes: %s type node %s", result.type, result.remarks))
|
||||
else
|
||||
@ -2007,7 +2065,8 @@ local function parse_link(raw, add_mode, group, cfgid)
|
||||
if #node_list > 0 then
|
||||
nodeResult[#nodeResult + 1] = {
|
||||
remark = group,
|
||||
list = node_list
|
||||
list = node_list,
|
||||
sub_cfg = sub_cfg
|
||||
}
|
||||
end
|
||||
log(2, i18n.translatef("Successfully resolved the [%s] node, number: %s", group, #node_list))
|
||||
@ -2038,77 +2097,26 @@ local execute = function()
|
||||
local cfgid = value[".name"]
|
||||
local remark = value.remark or ""
|
||||
local url = value.url or ""
|
||||
if value.allowInsecure and value.allowInsecure ~= "1" then
|
||||
allowInsecure_default = nil
|
||||
end
|
||||
local filter_keyword_mode = value.filter_keyword_mode or "5"
|
||||
if filter_keyword_mode == "0" then
|
||||
filter_keyword_mode_default = "0"
|
||||
elseif filter_keyword_mode == "1" then
|
||||
filter_keyword_mode_default = "1"
|
||||
filter_keyword_discard_list_default = value.filter_discard_list or {}
|
||||
elseif filter_keyword_mode == "2" then
|
||||
filter_keyword_mode_default = "2"
|
||||
filter_keyword_keep_list_default = value.filter_keep_list or {}
|
||||
elseif filter_keyword_mode == "3" then
|
||||
filter_keyword_mode_default = "3"
|
||||
filter_keyword_keep_list_default = value.filter_keep_list or {}
|
||||
filter_keyword_discard_list_default = value.filter_discard_list or {}
|
||||
elseif filter_keyword_mode == "4" then
|
||||
filter_keyword_mode_default = "4"
|
||||
filter_keyword_keep_list_default = value.filter_keep_list or {}
|
||||
filter_keyword_discard_list_default = value.filter_discard_list or {}
|
||||
end
|
||||
local ss_type = value.ss_type or "global"
|
||||
if ss_type ~= "global" and core_has[ss_type] then
|
||||
ss_type_default = ss_type
|
||||
end
|
||||
local trojan_type = value.trojan_type or "global"
|
||||
if trojan_type ~= "global" and core_has[trojan_type] then
|
||||
trojan_type_default = trojan_type
|
||||
end
|
||||
local vmess_type = value.vmess_type or "global"
|
||||
if vmess_type ~= "global" and core_has[vmess_type] then
|
||||
vmess_type_default = vmess_type
|
||||
end
|
||||
local vless_type = value.vless_type or "global"
|
||||
if vless_type ~= "global" and core_has[vless_type] then
|
||||
vless_type_default = vless_type
|
||||
end
|
||||
local hysteria2_type = value.hysteria2_type or "global"
|
||||
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
|
||||
hysteria2_type_default = hysteria2_type
|
||||
end
|
||||
|
||||
domain_resolver = value.domain_resolver
|
||||
domain_resolver_dns = value.domain_resolver_dns
|
||||
domain_resolver_dns_https = value.domain_resolver_dns_https
|
||||
domain_strategy = (value.domain_strategy == "UseIPv4" or value.domain_strategy == "UseIPv6") and value.domain_strategy or nil
|
||||
|
||||
-- Subscription Group Chain Agent
|
||||
local function valid_chain_node(node)
|
||||
if not node then return "" end
|
||||
local cp = uci:get(appname, node, "chain_proxy") or ""
|
||||
local am = uci:get(appname, node, "add_mode") or "0"
|
||||
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
|
||||
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
|
||||
chain_node_type = ""
|
||||
return ""
|
||||
end
|
||||
return node
|
||||
end
|
||||
preproxy_node_group = (value.chain_proxy == "1") and valid_chain_node(value.preproxy_node) or ""
|
||||
to_node_group = (value.chain_proxy == "2") and valid_chain_node(value.to_node) or ""
|
||||
|
||||
local ua = value.user_agent
|
||||
local access_mode = value.access_mode
|
||||
local result = (not access_mode) and i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto")))
|
||||
log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url .. ' [' .. result .. ']'))
|
||||
local tmp_file = "/tmp/" .. cfgid
|
||||
value.http_code = curl(url, tmp_file, ua, access_mode)
|
||||
if value.http_code ~= 200 then
|
||||
fail_list[#fail_list + 1] = value
|
||||
local url_is_local
|
||||
if fs.access(url) then
|
||||
-- debug, reads local files.
|
||||
log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url))
|
||||
url_is_local = true
|
||||
tmp_file = url
|
||||
else
|
||||
local ua = value.user_agent
|
||||
local access_mode = value.access_mode
|
||||
local result = (not access_mode) and i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto")))
|
||||
log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url .. ' [' .. result .. ']'))
|
||||
tmp_file = "/tmp/" .. cfgid
|
||||
value.http_code = curl(url, tmp_file, ua, access_mode)
|
||||
if value.http_code ~= 200 then
|
||||
fail_list[#fail_list + 1] = value
|
||||
luci.sys.call("rm -f " .. tmp_file)
|
||||
end
|
||||
end
|
||||
if fs.access(tmp_file) then
|
||||
if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then
|
||||
local f = io.open(tmp_file, "r")
|
||||
local stdout = f:read("*all")
|
||||
@ -2119,33 +2127,18 @@ local execute = function()
|
||||
if not manual_sub and old_md5 == new_md5 then
|
||||
log(1, i18n.translatef("Subscription: [%s] No changes, no update required.", remark))
|
||||
else
|
||||
parse_link(raw_data, "2", remark, cfgid)
|
||||
parse_link(raw_data, "2", remark, value)
|
||||
uci:set(appname, cfgid, "md5", new_md5)
|
||||
end
|
||||
else
|
||||
fail_list[#fail_list + 1] = value
|
||||
end
|
||||
if url_is_local then
|
||||
value.http_code = 0
|
||||
else
|
||||
luci.sys.call("rm -f " .. tmp_file)
|
||||
end
|
||||
end
|
||||
allowInsecure_default = true
|
||||
luci.sys.call("rm -f " .. tmp_file)
|
||||
filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
|
||||
filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
|
||||
filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
|
||||
|
||||
ss_type = uci:get(appname, "@global_subscribe[0]", "ss_type") or ""
|
||||
ss_type_default = core_has[ss_type] and ss_type or ss_type_default
|
||||
|
||||
trojan_type = uci:get(appname, "@global_subscribe[0]", "trojan_type") or ""
|
||||
trojan_type_default = core_has[trojan_type] and trojan_type or trojan_type_default
|
||||
|
||||
vmess_type = uci:get(appname, "@global_subscribe[0]", "vmess_type") or ""
|
||||
vmess_type_default = core_has[vmess_type] and vmess_type or vmess_type_default
|
||||
|
||||
vless_type = uci:get(appname, "@global_subscribe[0]", "vless_type") or ""
|
||||
vless_type_default = core_has[vless_type] and vless_type or vless_type_default
|
||||
|
||||
hysteria2_type = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or ""
|
||||
hysteria2_type_default = core_has[hysteria2_type] and hysteria2_type or hysteria2_type_default
|
||||
end
|
||||
|
||||
if #fail_list > 0 then
|
||||
|
||||
Loading…
Reference in New Issue
Block a user