🌴 Sync 2026-04-20 08:30:53

This commit is contained in:
github-actions[bot] committed 2026-04-20 08:30:53 +08:00
1 parent 24fc7da92a
commit ce3c712fa3
24 files changed
+1811 -709

No files matched your search

+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_VERSION:=0.1.10 PKG_VERSION:=0.1.11
PKG_RELEASE:=12 PKG_RELEASE:=13
LUCI_TITLE:=LuCI support for OpenClaw Launcher LUCI_TITLE:=LuCI support for OpenClaw Launcher
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
@@ -828,10 +828,15 @@
.oclm-dialog { .oclm-dialog {
position: fixed; position: fixed;
inset: 0; inset: 0;
width: 100vw;
min-width: 100vw;
height: 100vh;
min-height: 100vh;
z-index: 9998; z-index: 9998;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-sizing: border-box;
padding: 20px; padding: 20px;
background: rgba(15, 23, 42, 0.42); background: rgba(15, 23, 42, 0.42);
} }
@@ -932,10 +937,15 @@
.oclm-log-modal { .oclm-log-modal {
position: fixed; position: fixed;
inset: 0; inset: 0;
width: 100vw;
min-width: 100vw;
height: 100vh;
min-height: 100vh;
z-index: 9999; z-index: 9999;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
box-sizing: border-box;
padding: 20px; padding: 20px;
background: rgba(15, 23, 42, 0.42); background: rgba(15, 23, 42, 0.42);
} }
@@ -65,6 +65,14 @@
} }
}; };
var styleText = ""; 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) { function request(url, options) {
options = options || {}; options = options || {};
@@ -197,6 +205,358 @@
return state.security; 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() { function securityDataDirectory() {
var formBaseDir = state.form && state.form.base_dir ? String(state.form.base_dir) : ""; 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) : ""; var statusBaseDir = state.status && state.status.base_dir ? String(state.status.base_dir) : "";
@@ -204,6 +564,19 @@
return formBaseDir || statusBaseDir || suggestedBaseDir || ""; 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) { function validateProtectedDirectory(path) {
var security = getSecurityState(); var security = getSecurityState();
var value = String(path || "").replace(/^\s+|\s+$/g, ""); var value = String(path || "").replace(/^\s+|\s+$/g, "");
@@ -443,7 +816,7 @@
function showTaskLog(taskId) { function showTaskLog(taskId) {
taskId = taskId || "openclawmgr"; taskId = taskId || "openclawmgr";
if (!taskWindowAvailable()) { if (!taskWindowAvailable()) {
window.alert("任务窗口不可用(未加载 taskd/xterm)。请安装 luci-lib-taskd 和 luci-lib-xterm,然后强制刷新页面。"); openNoticeDialog("任务窗口不可用(未加载 taskd/xterm)。请安装 luci-lib-taskd 和 luci-lib-xterm,然后强制刷新页面。");
return; return;
} }
var now = Date.now ? Date.now() : (+new Date()); var now = Date.now ? Date.now() : (+new Date());
@@ -548,6 +921,7 @@
function render() { function render() {
var status = state.status || {}; var status = state.status || {};
var form = state.form || {}; var form = state.form || {};
var formErrors = getFormErrors();
var options = state.options || { base_dir_choices: [] }; var options = state.options || { base_dir_choices: [] };
var security = getSecurityState(); var security = getSecurityState();
var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : []; var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : [];
@@ -590,9 +964,6 @@
'</div>' + '</div>' +
'</div>'; '</div>';
}).join(""); }).join("");
var addDialogOpen = !!security.pendingAddPath;
var deleteDialogOpen = security.deleteTargetId != null;
var canStartService = !status.running && !status.reachable; var canStartService = !status.running && !status.reachable;
var canStopService = !!status.running; var canStopService = !!status.running;
@@ -648,7 +1019,8 @@
["loopback", "仅本机"], ["loopback", "仅本机"],
["auto", "自动"] ["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">' + '<div class="oclm-section-submit">' +
'<button class="oclm-button oclm-button-primary" type="button" id="oclm-save-basic"' + (savingBasic ? ' disabled' : '') + '>' + (savingBasic ? '应用中…' : '保存并应用') + '</button>' + '<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') + '">' + '<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="uninstall">卸载</button>' +
'<button class="oclm-button oclm-button-danger" type="button" data-op="purge">彻底清理</button>' + '<button class="oclm-button oclm-button-danger" type="button" data-op="purge">彻底清理</button>' +
'</div>' + '</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>' + '</section>' +
'</div></div></div>'; '</div></div></div>';
syncActionDialog();
bindEvents(); bindEvents();
} }
@@ -1036,7 +1373,7 @@
showTaskLog(rv.running_task_id); showTaskLog(rv.running_task_id);
return; return;
} }
window.alert((rv && rv.error) || "操作失败"); openNoticeDialog((rv && rv.error) || "操作失败");
return; return;
} }
state.lastTaskRunning = true; state.lastTaskRunning = true;
@@ -1049,12 +1386,12 @@
Array.prototype.forEach.call(root.querySelectorAll("[data-open-console]"), function(el) { Array.prototype.forEach.call(root.querySelectorAll("[data-open-console]"), function(el) {
el.onclick = function() { el.onclick = function() {
if (!state.consoleReady) { if (!state.consoleReady) {
window.alert("控制台正在启动,请稍候再试。"); openNoticeDialog("控制台正在启动,请稍候再试。");
return; return;
} }
var url = (state.status && state.status.token_url) || ""; var url = (state.status && state.status.token_url) || "";
if (!url) { if (!url) {
window.alert("控制台地址不可用"); openNoticeDialog("控制台地址不可用");
return; return;
} }
window.open(url, "_blank", "noreferrer"); window.open(url, "_blank", "noreferrer");
@@ -1068,7 +1405,7 @@
var baseDirEl = root.getElementById("oclm-base-dir"); var baseDirEl = root.getElementById("oclm-base-dir");
var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : ""; var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : "";
if (!baseDir) { if (!baseDir) {
window.alert("请先选择数据目录并保存应用,再执行安装。"); openNoticeDialog("请先选择数据目录并保存应用,再执行安装。");
if (baseDirEl) baseDirEl.focus(); if (baseDirEl) baseDirEl.focus();
return; return;
} }
@@ -1083,7 +1420,7 @@
if (!cfgRv || !cfgRv.ok) { if (!cfgRv || !cfgRv.ok) {
state.status.installing = false; state.status.installing = false;
render(); render();
window.alert((cfgRv && cfgRv.error) || "保存安装选项失败"); openNoticeDialog((cfgRv && cfgRv.error) || "保存安装选项失败");
return; return;
} }
state.form.install_accelerated = accelerated; state.form.install_accelerated = accelerated;
@@ -1099,7 +1436,7 @@
} }
state.status.installing = false; state.status.installing = false;
render(); render();
window.alert((rv && rv.error) || "启动安装失败"); openNoticeDialog((rv && rv.error) || "启动安装失败");
return; return;
} }
state.lastTaskRunning = true; state.lastTaskRunning = true;
@@ -1108,7 +1445,7 @@
}).catch(function() { }).catch(function() {
state.status.installing = false; state.status.installing = false;
render(); render();
window.alert("启动安装失败"); openNoticeDialog("启动安装失败");
}); });
}; };
}); });
@@ -1121,7 +1458,7 @@
var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : ""; var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : "";
var op = (state.status && state.status.installed) ? "upgrade" : "install"; var op = (state.status && state.status.installed) ? "upgrade" : "install";
if (!baseDir) { if (!baseDir) {
window.alert("请先选择数据目录并保存应用,再执行安装。"); openNoticeDialog("请先选择数据目录并保存应用,再执行安装。");
if (baseDirEl) baseDirEl.focus(); if (baseDirEl) baseDirEl.focus();
return; return;
} }
@@ -1138,7 +1475,7 @@
state.status.task_running = false; state.status.task_running = false;
state.status.task_op = ""; state.status.task_op = "";
render(); render();
window.alert((cfgRv && cfgRv.error) || "保存安装选项失败"); openNoticeDialog((cfgRv && cfgRv.error) || "保存安装选项失败");
return; return;
} }
state.form.install_accelerated = accelerated; state.form.install_accelerated = accelerated;
@@ -1156,7 +1493,7 @@
state.status.task_running = false; state.status.task_running = false;
state.status.task_op = ""; state.status.task_op = "";
render(); render();
window.alert((rv && rv.error) || "启动安装失败"); openNoticeDialog((rv && rv.error) || "启动安装失败");
return; return;
} }
state.lastTaskRunning = true; state.lastTaskRunning = true;
@@ -1167,7 +1504,7 @@
state.status.task_running = false; state.status.task_running = false;
state.status.task_op = ""; state.status.task_op = "";
render(); render();
window.alert("启动安装失败"); openNoticeDialog("启动安装失败");
}); });
}; };
}); });
@@ -1191,7 +1528,7 @@
updateCheck.localVersion = String((rv && rv.local_version) || (state.status && state.status.openclaw_version) || ""); updateCheck.localVersion = String((rv && rv.local_version) || (state.status && state.status.openclaw_version) || "");
updateCheck.remoteVersion = String((rv && rv.remote_version) || ""); updateCheck.remoteVersion = String((rv && rv.remote_version) || "");
render(); render();
window.alert(updateCheck.error); openNoticeDialog(updateCheck.error);
return; return;
} }
updateCheck.checked = true; updateCheck.checked = true;
@@ -1206,7 +1543,7 @@
updateCheck.hasUpdate = false; updateCheck.hasUpdate = false;
updateCheck.error = "检测更新失败"; updateCheck.error = "检测更新失败";
render(); 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) { Array.prototype.forEach.call(root.querySelectorAll("[data-security-recheck]"), function(el) {
el.onclick = function() { el.onclick = function() {
var id = el.getAttribute("data-security-recheck"); var id = el.getAttribute("data-security-recheck");
if (!id) return; if (!id) return;
postJson(config.securityRecheckUrl, { id: id }).then(function(rv) { postJson(config.securityRecheckUrl, { id: id }).then(function(rv) {
if (!rv || !rv.ok) { if (!rv || !rv.ok) {
window.alert((rv && rv.error) || "重新检测失败"); openNoticeDialog((rv && rv.error) || "重新检测失败");
return; return;
} }
loadSecurityData(); loadSecurityData();
}).catch(function() { }).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 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) { if (saveBasic) {
saveBasic.onclick = function() { 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 = { var payload = {
port: root.getElementById("oclm-port").value, port: root.getElementById("oclm-port").value,
bind: root.getElementById("oclm-bind").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), install_accelerated: !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked),
}; };
state.savingSection = "basic"; state.savingSection = "basic";
@@ -1482,7 +1752,7 @@
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "basic"); }).catch(function() { postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "basic"); }).catch(function() {
state.savingSection = ""; state.savingSection = "";
render(); render();
window.alert("保存失败"); openNoticeDialog("保存失败");
}); });
}; };
} }
@@ -1494,7 +1764,7 @@
var modelValue = normalizeModelValue(root.getElementById("oclm-model").value, defaultModelForAgent(agentValue)); var modelValue = normalizeModelValue(root.getElementById("oclm-model").value, defaultModelForAgent(agentValue));
var baseUrlValue = root.getElementById("oclm-base-url").value; var baseUrlValue = root.getElementById("oclm-base-url").value;
if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) { if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) {
window.alert("自定义供应商必须填写服务地址"); openNoticeDialog("自定义供应商必须填写服务地址");
return; return;
} }
var payload = { var payload = {
@@ -1510,7 +1780,7 @@
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "ai"); }).catch(function() { postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "ai"); }).catch(function() {
state.savingSection = ""; state.savingSection = "";
render(); render();
window.alert("保存失败"); openNoticeDialog("保存失败");
}); });
}; };
} }
@@ -1529,7 +1799,7 @@
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "access"); }).catch(function() { postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "access"); }).catch(function() {
state.savingSection = ""; state.savingSection = "";
render(); render();
window.alert("保存失败"); openNoticeDialog("保存失败");
}); });
}; };
} }
@@ -1537,16 +1807,25 @@
function handleSaveResult(rv, section) { function handleSaveResult(rv, section) {
if (!rv || !rv.ok) { if (!rv || !rv.ok) {
var formErrors = getFormErrors();
state.savingSection = ""; state.savingSection = "";
if (section === "basic" && isBaseDirErrorMessage(rv && rv.error)) {
formErrors.base_dir = (rv && rv.error) || "数据目录错误";
render();
return;
}
render(); render();
window.alert((rv && rv.error) || "保存失败"); openNoticeDialog((rv && rv.error) || "保存失败");
return; return;
} }
if (section === "basic") {
getFormErrors().base_dir = "";
}
postForm(config.applyUrl, {}).then(function(applyRv) { postForm(config.applyUrl, {}).then(function(applyRv) {
state.savingSection = ""; state.savingSection = "";
if (!applyRv || !applyRv.ok) { if (!applyRv || !applyRv.ok) {
render(); render();
window.alert((applyRv && applyRv.error) || "应用配置失败"); openNoticeDialog((applyRv && applyRv.error) || "应用配置失败");
loadConfig(); loadConfig();
scheduleStatusRefresh(3, 600); scheduleStatusRefresh(3, 600);
return; return;
@@ -1558,7 +1837,7 @@
}).catch(function() { }).catch(function() {
state.savingSection = ""; state.savingSection = "";
render(); render();
window.alert("应用配置失败"); openNoticeDialog("应用配置失败");
loadConfig(); loadConfig();
scheduleStatusRefresh(3, 600); scheduleStatusRefresh(3, 600);
}); });
@@ -211,6 +211,37 @@ local function trim(v)
return tostring(v or ""):gsub("^%s+", ""):gsub("%s+$", "") return tostring(v or ""):gsub("^%s+", ""):gsub("%s+$", "")
end 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 function split_dots(v)
local parts = {} local parts = {}
for part in tostring(v or ""):gmatch("[^%.]+") do for part in tostring(v or ""):gmatch("[^%.]+") do
@@ -1231,11 +1262,13 @@ function action_config_data()
end end
if has("base_dir") then if has("base_dir") then
local base_dir = tostring(body.base_dir or "") local ok, err, normalized = validate_base_dir_input(body.base_dir or "")
if base_dir == "" then if not ok then
write_json({ ok = false, error = "base_dir required" }) write_json({ ok = false, error = err })
return return
end end
body.base_dir = normalized
local base_dir = normalized
uci:set("openclawmgr", section, "base_dir", base_dir) uci:set("openclawmgr", section, "base_dir", base_dir)
end end
@@ -24,6 +24,9 @@ validate_base_dir() {
*) return 1 ;; *) return 1 ;;
esac esac
[ "$dir" != "/" ] || return 1 [ "$dir" != "/" ] || return 1
case "$dir" in
/root|/root/*) return 1 ;;
esac
case "$dir" in case "$dir" in
*"/../"*|*"/.."|*"/./"*|*"/.") return 1 ;; *"/../"*|*"/.."|*"/./"*|*"/.") return 1 ;;
esac esac
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.4.15 PKG_VERSION:=26.4.15
PKG_RELEASE:=83 PKG_RELEASE:=84
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ 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_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria") 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类型(节点订阅页面未设置时,自动取默认) -- 取节点使用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 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 trojan_type_default = api.get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{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 vmess_type_default = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local DEFAULT_VMESS_TYPE = 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 DEFAULT_VLESS_TYPE = 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_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local core_has = { local core_has = {
["xray"] = has_xray, ["sing-box"] = has_singbox, ["shadowsocks-libev"] = has_ss,["shadowsocks-rust"] = has_ss_rust, ["xray"] = has_xray,
["trojan-plus"] = has_trojan_plus, ["hysteria2"] = has_hysteria2 ["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 function is_filter_keyword(mode, discard_list, keep_list, value)
local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} if mode == "1" then
local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} for k,v in ipairs(discard_list) do
local function is_filter_keyword(value)
if filter_keyword_mode_default == "1" then
for k,v in ipairs(filter_keyword_discard_list_default) do
if value:find(v, 1, true) then if value:find(v, 1, true) then
return true return true
end end
end end
elseif filter_keyword_mode_default == "2" then elseif mode == "2" then
local result = true 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 if value:find(v, 1, true) then
result = false result = false
end end
end end
return result return result
elseif filter_keyword_mode_default == "3" then elseif mode == "3" then
local result = false 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 if value:find(v, 1, true) then
result = true result = true
end end
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 if value:find(v, 1, true) then
result = false result = false
end end
end end
return result return result
elseif filter_keyword_mode_default == "4" then elseif mode == "4" then
local result = true 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 if value:find(v, 1, true) then
result = false result = false
end end
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 if value:find(v, 1, true) then
result = true result = true
end end
@@ -448,16 +449,16 @@ local function get_subscribe_info(cfgid, value)
end end
-- 设置 ss 协议实现类型 -- 设置 ss 协议实现类型
local function set_ss_implementation(result) local function set_ss_implementation(ss_type, result)
if ss_type_default == "shadowsocks-libev" and has_ss then if ss_type == "shadowsocks-libev" and has_ss then
result.type = "SS" 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' 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.type = 'Xray'
result.protocol = 'shadowsocks' result.protocol = 'shadowsocks'
result.transport = 'raw' 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.type = 'sing-box'
result.protocol = 'shadowsocks' result.protocol = 'shadowsocks'
else else
@@ -468,8 +469,60 @@ local function set_ss_implementation(result)
end end
-- 处理数据 -- 处理数据
local function processData(szType, content, add_mode, group) local function processData(szType, content, add_mode, group, sub_cfg)
--log(content, add_mode, group) --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 = { local result = {
timeout = 60, timeout = 60,
add_mode = add_mode, --0为手动配置,1为导入,2为订阅 add_mode = add_mode, --0为手动配置,1为导入,2为订阅
@@ -509,9 +562,9 @@ local function processData(szType, content, add_mode, group)
result.remarks = base64Decode(params.remarks) result.remarks = base64Decode(params.remarks)
elseif szType == 'vmess' then elseif szType == 'vmess' then
local info = jsonParse(content) 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' 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" result.type = "Xray"
else else
log("跳过 VMess 节点,因未适配到 VMess 核心程序,或未正确设置节点使用类型。") log("跳过 VMess 节点,因未适配到 VMess 核心程序,或未正确设置节点使用类型。")
@@ -606,7 +659,7 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = info.pcs result.tls_CertSha = info.pcs
result.tls_CertByName = info.vcn result.tls_CertByName = info.vcn
local insecure = info.allowinsecure or info.allowInsecure or info.insecure 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 else
result.tls = "0" result.tls = "0"
end end
@@ -622,7 +675,7 @@ local function processData(szType, content, add_mode, group)
return nil return nil
end end
elseif szType == "ss" then elseif szType == "ss" then
result = set_ss_implementation(result) result = set_ss_implementation(default_ss_type, result)
if not result then return nil end if not result then return nil end
--SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ] --SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ]
@@ -886,7 +939,7 @@ local function processData(szType, content, add_mode, group)
end end
end end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure 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 result.uot = params.udp
elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then
result.error_msg = "请更换 Xray 或 Sing-Box 来支持 SS 更多的传输方式。" 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 params["shadow-tls"] then
if result.type ~= "sing-box" and result.type ~= "SS-Rust" 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 else
-- 解析SS Shadow-TLS 插件参数 -- 解析SS Shadow-TLS 插件参数
local function parseShadowTLSParams(b64str, out) local function parseShadowTLSParams(b64str, out)
@@ -937,12 +990,12 @@ local function processData(szType, content, add_mode, group)
end end
end end
elseif szType == "trojan" then 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" 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.type = 'sing-box'
result.protocol = 'trojan' 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.type = 'Xray'
result.protocol = 'trojan' result.protocol = 'trojan'
else else
@@ -991,7 +1044,7 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = params.pcs result.tls_CertSha = params.pcs
result.tls_CertByName = params.vcn result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.allowInsecure or params.insecure 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 if not params.type then params.type = "tcp" end
params.type = string.lower(params.type) params.type = string.lower(params.type)
@@ -1077,7 +1130,7 @@ local function processData(szType, content, add_mode, group)
end end
elseif szType == "ssd" then elseif szType == "ssd" then
result = set_ss_implementation(result) result = set_ss_implementation(default_ss_type, result)
if not result then return nil end if not result then return nil end
result.address = content.server result.address = content.server
result.port = content.port result.port = content.port
@@ -1088,9 +1141,9 @@ local function processData(szType, content, add_mode, group)
result.group = content.airport result.group = content.airport
result.remarks = content.remarks result.remarks = content.remarks
elseif szType == "vless" then 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' 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" result.type = "Xray"
else else
log("跳过 VLESS 节点,因未适配到 VLESS 核心程序,或未正确设置节点使用类型。") log("跳过 VLESS 节点,因未适配到 VLESS 核心程序,或未正确设置节点使用类型。")
@@ -1243,7 +1296,7 @@ local function processData(szType, content, add_mode, group)
result.reality_mldsa65Verify = params.pqv or nil result.reality_mldsa65Verify = params.pqv or nil
end end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure 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 end
result.port = port result.port = port
@@ -1300,7 +1353,7 @@ local function processData(szType, content, add_mode, group)
result.hysteria_auth_password = params.auth result.hysteria_auth_password = params.auth
result.tls_serverName = params.peer or params.sni or "" result.tls_serverName = params.peer or params.sni or ""
local insecure = params.allowinsecure or params.allowInsecure or params.insecure 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.alpn = params.alpn
result.hysteria_up_mbps = params.upmbps result.hysteria_up_mbps = params.upmbps
result.hysteria_down_mbps = params.downmbps 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_CertSha = params.pcs
result.tls_CertByName = params.vcn result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.insecure 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_tls_pinSHA256 = params.pinSHA256
result.hysteria2_hop = params.mport result.hysteria2_hop = params.mport
if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then
local is_singbox = hysteria2_type_default == "sing-box" and has_singbox local is_singbox = default_hysteria2_type == "sing-box" and has_singbox
result.type = is_singbox and 'sing-box' or 'Xray' result.type = is_singbox and 'sing-box' or 'Xray'
result.protocol = "hysteria2" result.protocol = "hysteria2"
if params["obfs-password"] or params["obfs_password"] then 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_congestion_control = params.congestion_control or "cubic"
result.tuic_udp_relay_mode = params.udp_relay_mode or "native" result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
local insecure = params.allowinsecure or params.insecure or params.allow_insecure 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 elseif szType == "anytls" then
if has_singbox then if has_singbox then
result.type = 'sing-box' result.type = 'sing-box'
@@ -1490,7 +1543,7 @@ local function processData(szType, content, add_mode, group)
end end
result.port = port result.port = port
local insecure = params.allowinsecure or params.insecure 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 end
elseif szType == 'naive+https' or szType == 'naive+quic' then elseif szType == 'naive+https' or szType == 'naive+quic' then
if has_singbox then if has_singbox then
@@ -1829,6 +1882,30 @@ local function update_node(manual)
for _, v in ipairs(nodeResult) do for _, v in ipairs(nodeResult) do
local remark = v["remark"] local remark = v["remark"]
local list = v["list"] 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 for _, vv in ipairs(list) do
local cfgid = uci:section(appname, "nodes", api.gen_short_uuid()) local cfgid = uci:section(appname, "nodes", api.gen_short_uuid())
for kkk, vvv in pairs(vv) do for kkk, vvv in pairs(vv) do
@@ -1916,8 +1993,12 @@ local function update_node(manual)
end end
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 if raw and #raw > 0 then
local cfgid
if sub_cfg then
cfgid = sub_cfg[".name"]
end
local nodes, szType local nodes, szType
local node_list = {} local node_list = {}
-- SSD 似乎是这种格式 ssd:// 开头的 -- SSD 似乎是这种格式 ssd:// 开头的
@@ -1952,17 +2033,17 @@ local function parse_link(raw, add_mode, group, cfgid)
xpcall(function () xpcall(function ()
local result local result
if szType == 'ssd' then if szType == 'ssd' then
result = processData(szType, v, add_mode, group) result = processData(szType, v, add_mode, group, sub_cfg)
elseif not szType then elseif not szType then
local node = api.trim(v) local node = api.trim(v)
local dat = split(node, "://") local dat = split(node, "://")
if dat and dat[1] and dat[2] then if dat and dat[1] and dat[2] then
if dat[1] == 'vmess' or dat[1] == 'ssr' then if dat[1] == 'vmess' or dat[1] == 'ssr' then
local link = api.trim(dat[2]:gsub("#.*$", "")) 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 else
local link = dat[2]:gsub("&amp;", "&"):gsub("%s*#%s*", "#") -- 一些奇葩的链接用"&amp;"当做"&","#"前后带空格 local link = dat[2]:gsub("&amp;", "&"):gsub("%s*#%s*", "#") -- 一些奇葩的链接用"&amp;"当做"&","#"前后带空格
result = processData(dat[1], link, add_mode, group) result = processData(dat[1], link, add_mode, group, sub_cfg)
end end
end end
else else
@@ -1974,7 +2055,7 @@ local function parse_link(raw, add_mode, group, cfgid)
log('丢弃节点:' .. result.remarks .. " ,原因:" .. result.error_msg) log('丢弃节点:' .. result.remarks .. " ,原因:" .. result.error_msg)
elseif not result.type then elseif not result.type then
log('丢弃节点:' .. result.remarks .. " ,找不到可使用二进制。") 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 (not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then
log('丢弃过滤节点:' .. result.type .. ' 节点,' .. result.remarks) log('丢弃过滤节点:' .. result.type .. ' 节点,' .. result.remarks)
else else
@@ -1994,7 +2075,8 @@ local function parse_link(raw, add_mode, group, cfgid)
if #node_list > 0 then if #node_list > 0 then
nodeResult[#nodeResult + 1] = { nodeResult[#nodeResult + 1] = {
remark = group, remark = group,
list = node_list list = node_list,
sub_cfg = sub_cfg
} }
end end
log('成功解析【' .. group .. '】节点数量:' .. #node_list) log('成功解析【' .. group .. '】节点数量:' .. #node_list)
@@ -2025,78 +2107,26 @@ local execute = function()
local cfgid = value[".name"] local cfgid = value[".name"]
local remark = value.remark or "" local remark = value.remark or ""
local url = value.url 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 local url_is_local
domain_resolver_dns = value.domain_resolver_dns if fs.access(url) then
domain_resolver_dns_https = value.domain_resolver_dns_https -- debug, reads local files.
local map = { UseIPv4v6 = 1, UseIPv6v4 = 1, UseIPv4 = 1, UseIPv6 = 1 } log('正在订阅:【' .. remark .. '】' .. url)
domain_strategy = (value.domain_strategy and map[value.domain_strategy]) and value.domain_strategy or nil url_is_local = true
tmp_file = url
-- 订阅组链式代理
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
else 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 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 f = io.open(tmp_file, "r")
local stdout = f:read("*all") local stdout = f:read("*all")
@@ -2107,33 +2137,18 @@ local execute = function()
if not manual_sub and old_md5 == new_md5 then if not manual_sub and old_md5 == new_md5 then
log('订阅:【' .. remark .. '】没有变化,无需更新。') log('订阅:【' .. remark .. '】没有变化,无需更新。')
else else
parse_link(raw_data, "2", remark, cfgid) parse_link(raw_data, "2", remark, value)
uci:set(appname, cfgid, "md5", new_md5) uci:set(appname, cfgid, "md5", new_md5)
end end
else else
fail_list[#fail_list + 1] = value fail_list[#fail_list + 1] = value
end end
if url_is_local then
value.http_code = 0
else
luci.sys.call("rm -f " .. tmp_file)
end
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 end
if #fail_list > 0 then if #fail_list > 0 then
+2 -2
View File
@@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.4.10 PKG_VERSION:=26.4.20
PKG_RELEASE:=37 PKG_RELEASE:=38
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -268,8 +268,8 @@ o = s:taboption("DNS", Flag, "dns_redirect", translate("DNS Redirect"), translat
o.default = "1" o.default = "1"
o.rmempty = false o.rmempty = false
local use_nft = m:get("@global_forwarding[0]", "use_nft") == "1" local prefer_nft = m:get("@global_forwarding[0]", "prefer_nft") == "1"
local set_title = api.i18n.translate(use_nft and "Clear NFTSET" or "Clear IPSET") 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 = s:taboption("DNS", DummyValue, "clear_ipset", set_title, translate("Try this feature if the rule modification does not take effect."))
o.rawhtml = true o.rawhtml = true
function o.cfgvalue(self, section) function o.cfgvalue(self, section)
@@ -48,15 +48,23 @@ o.default = ""
o:depends("console_auth", true) o:depends("console_auth", true)
---- Console Port ---- Console Port
o = s:option(Value, "console_port", translate("Console Port"), translate( 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"))
"In the browser input routing IP plus port access, such as:192.168.1.1:1188")) o.datatype = "port"
o.default = "1188" o.default = 1188
o:depends("balancing_enable", true) 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 = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0" o.default = "0"
o:depends("balancing_enable", true) 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 ---- Health Check Type
o = s:option(ListValue, "health_check_type", translate("Health Check Type")) o = s:option(ListValue, "health_check_type", translate("Health Check Type"))
o.default = "passwall_logic" 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") o:depends("health_check_type", "passwall_logic")
---- Health Check Inter ---- Health Check Inter
o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds")) o = s:option(Value, "health_check_inter", translate("Health Check Inter"))
o.default = "60" o.datatype = "min(20)"
o.default = 60
o:depends("balancing_enable", true) o:depends("balancing_enable", true)
o.description = translate("Units:seconds") .. " ( ≥ 20s )"
o = s:option(DummyValue, "health_check_tips", " ") o = s:option(DummyValue, "health_check_tips", " ")
o.rawhtml = true o.rawhtml = true
@@ -89,11 +99,10 @@ end
o:depends("health_check_type", "passwall_logic") o:depends("health_check_type", "passwall_logic")
-- [[ Balancing Settings ]]-- -- [[ Balancing Settings ]]--
s = m:section(TypedSection, "haproxy_config", translate("Node List"), s = m:section(TypedSection, "haproxy_config", translate("Node List"))
"<font color='red'>" .. 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.") .. 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>" ..
"\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!") .. 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>"
"</font>")
s.template = "cbi/tblsection" s.template = "cbi/tblsection"
s.sortable = true s.sortable = true
s.anonymous = true s.anonymous = true
@@ -148,7 +157,7 @@ o.rmempty = false
---- Node Weight ---- Node Weight
o = s:option(Value, "lbweight", translate("Node Weight")) o = s:option(Value, "lbweight", translate("Node Weight"))
o.datatype = "uinteger" o.datatype = "min(1)"
o.default = 5 o.default = 5
o.rmempty = false o.rmempty = false
@@ -8,7 +8,7 @@ if not arg[1] or not m:get(arg[1]) then
luci.http.redirect(api.url()) luci.http.redirect(api.url())
end 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")) m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
local has_singbox = api.finded_com("sing-box") local has_singbox = api.finded_com("sing-box")
@@ -95,41 +95,28 @@ o.datatype = "min(1)"
o.default = 1 o.default = 1
o:depends("enable_autoswitch", true) 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:depends("enable_autoswitch", true)
o.widget = "checkbox" o.template = appname .. "/cbi/nodes_dynamiclist"
o.template = appname .. "/cbi/nodes_multivalue"
o.group = {} 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 for i, v in pairs(nodes_table) do
o:value(v.id, v.remark) if v.protocol ~= "_shunt" then
o.group[#o.group+1] = v.group or "" 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:value(v.id, v["remark"])
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default") socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end 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 = 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) 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 = 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: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 = s:option(Value, _n("mkcp_seed"), translate("KCP Seed"))
o:depends({ [_n("transport")] = "mkcp" }) o:depends({ [_n("transport")] = "mkcp" })
@@ -645,6 +618,14 @@ end
-- [[ User-Agent ]]-- -- [[ User-Agent ]]--
o = s:option(Value, _n("user_agent"), translate("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("tcp_guise")] = "http" })
o:depends({ [_n("transport")] = "ws" }) o:depends({ [_n("transport")] = "ws" })
o:depends({ [_n("transport")] = "httpupgrade" }) o:depends({ [_n("transport")] = "httpupgrade" })
@@ -416,8 +416,14 @@ if singbox_tags:find("with_quic") then
end end
-- [[ SSH config start ]] -- -- [[ 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: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 = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase"))
o.password = true o.password = true
@@ -509,11 +515,7 @@ o.rows = 5
o.wrap = "off" o.wrap = "off"
o:depends({ [_n("ech")] = true }) o:depends({ [_n("ech")] = true })
o.validate = function(self, value) o.validate = function(self, value)
value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n") value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
value = value:gsub("^%s*\n", "")
if value:sub(-1) == "\n" then
value = value:sub(1, -2)
end
return value return value
end end
@@ -681,6 +683,14 @@ o:depends({ [_n("grpc_health_check")] = true })
-- [[ User-Agent ]]-- -- [[ User-Agent ]]--
o = s:option(Value, _n("user_agent"), translate("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("tcp_guise")] = "http" })
o:depends({ [_n("transport")] = "http" }) o:depends({ [_n("transport")] = "http" })
o:depends({ [_n("transport")] = "ws" }) 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 = 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: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 = s:option(Value, _n("mkcp_seed"), translate("KCP Seed"))
o:depends({ [_n("transport")] = "mkcp" }) o:depends({ [_n("transport")] = "mkcp" })
@@ -217,13 +217,12 @@ function gen_outbound(flag, node, tag, proxy_table)
} }
} or nil, } or nil,
kcpSettings = (node.transport == "mkcp") and { kcpSettings = (node.transport == "mkcp") and {
mtu = tonumber(node.mkcp_mtu), mtu = 1350,
tti = tonumber(node.mkcp_tti), tti = 50,
uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), uplinkCapacity = 12,
downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), downlinkCapacity = 100,
congestion = (node.mkcp_congestion == "1") and true or false, CwndMultiplier = 1,
readBufferSize = tonumber(node.mkcp_readBufferSize), MaxSendingWindow = 2 * 1024 * 1024
writeBufferSize = tonumber(node.mkcp_writeBufferSize)
} or nil, } or nil,
wsSettings = (node.transport == "ws") and { wsSettings = (node.transport == "ws") and {
path = node.ws_path or "/", path = node.ws_path or "/",
@@ -675,13 +674,12 @@ function gen_config_server(node)
} }
} or nil, } or nil,
kcpSettings = (node.transport == "mkcp") and { kcpSettings = (node.transport == "mkcp") and {
mtu = tonumber(node.mkcp_mtu), mtu = 1350,
tti = tonumber(node.mkcp_tti), tti = 50,
uplinkCapacity = tonumber(node.mkcp_uplinkCapacity), uplinkCapacity = 12,
downlinkCapacity = tonumber(node.mkcp_downlinkCapacity), downlinkCapacity = 100,
congestion = (node.mkcp_congestion == "1") and true or false, CwndMultiplier = 1,
readBufferSize = tonumber(node.mkcp_readBufferSize), MaxSendingWindow = 2 * 1024 * 1024
writeBufferSize = tonumber(node.mkcp_writeBufferSize)
} or nil, } or nil,
wsSettings = (node.transport == "ws") and { wsSettings = (node.transport == "ws") and {
host = node.ws_host or nil, 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 => ({
"&":"&amp;", "<":"&lt;", ">":"&gt;", '"':"&quot;", "'":"&#39;"
}[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 #!/bin/sh
[[ "$ACTION" == "ifup" && $(uci get "passwall2.@global[0].enabled") == "1" ]] && [ -f /var/lock/passwall2_ready.lock ] && { [ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0
default_device=$(ip route | grep default | awk -F 'dev ' '{print $2}' | awk '{print $1}')
[ "$default_device" == "$DEVICE" ] && { . /usr/share/passwall2/utils.sh
LOCK_FILE_DIR=/var/lock
[ ! -d ${LOCK_FILE_DIR} ] && mkdir -p ${LOCK_FILE_DIR} ([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
LOCK_FILE="${LOCK_FILE_DIR}/passwall2_ifup.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 if [ -s ${LOCK_FILE} ]; then
SPID=$(cat ${LOCK_FILE}) SPID=$(cat ${LOCK_FILE})
if [ -e /proc/${SPID}/status ]; then if [ -e /proc/${SPID}/status ]; then
@@ -15,8 +27,8 @@
fi fi
echo $$ > ${LOCK_FILE} echo $$ > ${LOCK_FILE}
/etc/init.d/passwall2 restart >/dev/null 2>&1 & /etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
logger -p notice -t network -s "passwall2: restart when $INTERFACE ifup" logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup"
rm -rf ${LOCK_FILE} rm -rf ${LOCK_FILE}
} }
@@ -933,8 +933,8 @@ start_haproxy() {
[ "$(config_t_get global_haproxy balancing_enable 0)" != "1" ] && return [ "$(config_t_get global_haproxy balancing_enable 0)" != "1" ] && return
haproxy_path=$TMP_PATH/haproxy haproxy_path=$TMP_PATH/haproxy
haproxy_conf="config.cfg" haproxy_conf="config.cfg"
lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} -dns ${LOCAL_DNS:-${AUTO_DNS}} lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf}
ln_run ${QUEUE_RUN} "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}" ln_run 0 "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}"
} }
run_copy_dnsmasq() { run_copy_dnsmasq() {
@@ -1,7 +1,7 @@
#!/usr/bin/lua #!/usr/bin/lua
local api = require ("luci.passwall2.api") local api = require ("luci.passwall2.api")
local appname = "passwall2" local appname = api.appname
local fs = api.fs local fs = api.fs
local jsonc = api.jsonc local jsonc = api.jsonc
local uci = api.uci local uci = api.uci
@@ -18,11 +18,12 @@ end
local var = api.get_args(arg) local var = api.get_args(arg)
local haproxy_path = var["-path"] local haproxy_path = var["-path"]
local haproxy_conf = var["-conf"] 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 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_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 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_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0"
local bind_address = "0.0.0.0" local bind_address = "0.0.0.0"
@@ -38,35 +39,37 @@ local f_out = io.open(haproxy_file, "a")
local haproxy_config = [[ local haproxy_config = [[
global global
daemon daemon
log 127.0.0.1 local2 log 127.0.0.1 local2
maxconn 60000 maxconn 60000
stats socket {{path}}/haproxy.sock stats socket {{path}}/haproxy.sock
nbthread {{nbthread}} nbthread {{nbthread}}
external-check external-check
insecure-fork-wanted insecure-fork-wanted
spread-checks 50
tune.max-checks-per-thread 5
defaults defaults
mode tcp mode tcp
log global log global
option tcplog option tcplog
option dontlognull option dontlognull
option http-server-close option http-server-close
#option forwardfor except 127.0.0.0/8 #option forwardfor except 127.0.0.0/8
option redispatch option redispatch
retries 2 retries 2
timeout http-request 10s timeout http-request 10s
timeout queue 1m timeout queue 1m
timeout connect 10s timeout connect 10s
timeout client 1m timeout client 1m
timeout server 1m timeout server 1m
timeout http-keep-alive 10s timeout http-keep-alive 10s
timeout check 10s timeout check 10s
maxconn 3000 maxconn 3000
resolvers mydns resolvers mydns
resolve_retries 1 resolve_retries 1
timeout resolve 5s timeout resolve 5s
hold valid 600s hold valid 600s
{{dns}} {{dns}}
]] ]]
@@ -97,10 +100,12 @@ uci:foreach(appname, "haproxy_config", function(t)
local lbss = t.lbss local lbss = t.lbss
local listen_port = tonumber(t.haproxy_port) or 0 local listen_port = tonumber(t.haproxy_port) or 0
local server_node = uci:get_all(appname, lbss) local server_node = uci:get_all(appname, lbss)
if server_node and server_node.address and server_node.port then local hop = (health_check_type == "passwall_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil
server_remark = server_node.address .. ":" .. server_node.port 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_address = server_node.address
server_port = server_node.port server_port = server_node.port or hop
t.origin_address = server_address t.origin_address = server_address
t.origin_port = server_port t.origin_port = server_port
if health_check_type == "passwall_logic" then if health_check_type == "passwall_logic" then
@@ -143,6 +148,10 @@ uci:foreach(appname, "haproxy_config", function(t)
end end
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 = {} local sortTable = {}
for i in pairs(listens) do for i in pairs(listens) do
if i ~= nil then if i ~= nil then
@@ -158,14 +167,14 @@ for i, port in pairs(sortTable) do
listen %s listen %s
bind %s:%s bind %s:%s
mode tcp mode tcp
balance roundrobin balance %s
]], port, bind_address, port)) ]], port, bind_address, port, balancingStrategy))
if health_check_type == "passwall_logic" then if health_check_type == "passwall_logic" then
f_out:write(string.format([[ f_out:write(string.format([[
option external-check option external-check
external-check command "/usr/share/passwall2/haproxy_check.sh" external-check command "/usr/share/%s/haproxy_check.sh"
]], port, port)) ]], appname))
end end
local count_M, count_B = 1, 1 local count_M, count_B = 1, 1
@@ -180,7 +189,7 @@ listen %s
count_B = count_B + 1 count_B = count_B + 1
end end
local server = o.server_address .. ":" .. o.server_port 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("{{remark}}", remark)
server_conf = server_conf:gsub("{{server}}", server) server_conf = server_conf:gsub("{{server}}", server)
server_conf = server_conf:gsub("{{weight}}", o.lbweight) server_conf = server_conf:gsub("{{weight}}", o.lbweight)
@@ -189,13 +198,13 @@ listen %s
resolvers = "" resolvers = ""
end end
server_conf = server_conf:gsub("{{resolvers}}", resolvers) 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 "") 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 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 end
log(2, string.format(api.i18n.translatef("Node: %s:%s, Weight: %s", o.origin_address, o.origin_port, o.lbweight))) 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" extra_params="${extra_params} --retry-all-errors"
fi 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 case "$status" in
200|204) 200|204)
@@ -617,6 +617,24 @@ filter_direct_node_list() {
done 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() { add_firewall_rule() {
log_i18n 0 "Starting to load %s firewall rules..." "iptables" log_i18n 0 "Starting to load %s firewall rules..." "iptables"
@@ -665,6 +683,8 @@ add_firewall_rule() {
EOF EOF
} }
update_wan_sets
[ -n "$ISP_DNS" ] && { [ -n "$ISP_DNS" ] && {
for ispip in $ISP_DNS; do for ispip in $ISP_DNS; do
ipset -! add $IPSET_LAN $ispip ipset -! add $IPSET_LAN $ispip
@@ -716,6 +736,7 @@ add_firewall_rule() {
$ipt_n -N PSW2 $ipt_n -N PSW2
$ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN $ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN
$ipt_n -A PSW2 $(dst $IPSET_VPS) -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" [ "$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" [ -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 -N PSW2
$ipt_m -A PSW2 $(dst $IPSET_LAN) -j RETURN $ipt_m -A PSW2 $(dst $IPSET_LAN) -j RETURN
$ipt_m -A PSW2 $(dst $IPSET_VPS) -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 $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" insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2"
# Only TCP, UDP Invalid. # Only TCP, UDP Invalid.
insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT" 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 -N PSW2
$ip6t_m -A PSW2 $(dst $IPSET_LAN6) -j RETURN $ip6t_m -A PSW2 $(dst $IPSET_LAN6) -j RETURN
$ip6t_m -A PSW2 $(dst $IPSET_VPS6) -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 $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" insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2"
# Only TCP, UDP Invalid. # Only TCP, UDP Invalid.
@@ -835,7 +837,7 @@ add_firewall_rule() {
local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}') local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}')
if [[ "$dns_address" == *::* ]]; then if [[ "$dns_address" == *::* ]]; then
$ip6t_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN $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 else
$ipt_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN $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}")" log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "iptables" "${dns_address}:${dns_port:-53}")"
@@ -1050,7 +1052,7 @@ gen_include() {
local __ipt="" local __ipt=""
[ -n "${ipt}" ] && { [ -n "${ipt}" ] && {
__ipt=$(cat <<- EOF __ipt=$(cat <<- EOF
. $UTILS_PATH ${MY_PATH} update_wan_sets
$ipt-save -c | grep -v "PSW2" | $ipt-restore -c $ipt-save -c | grep -v "PSW2" | $ipt-restore -c
$ipt-restore -n <<-EOT $ipt-restore -n <<-EOT
$(extract_rules 4 nat) $(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" "mwan3" "-j PSW2")
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT") \$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "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 EOF
) )
} }
local __ip6t="" local __ip6t=""
[ -n "${ip6t}" ] && { [ -n "${ip6t}" ] && {
__ip6t=$(cat <<- EOF __ip6t=$(cat <<- EOF
. $UTILS_PATH ${MY_PATH} update_wan_sets
$ip6t-save -c | grep -v "PSW2" | $ip6t-restore -c $ip6t-save -c | grep -v "PSW2" | $ip6t-restore -c
$ip6t-restore -n <<-EOT $ip6t-restore -n <<-EOT
$(extract_rules 6 nat) $(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" "mwan3" "-j PSW2")
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT") \$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "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 EOF
) )
} }
cat <<-EOF >> $FWI cat <<-EOF >> $FWI
${__ipt} ${__ipt}
${__ip6t} ${__ip6t}
return 0 return 0
@@ -1158,6 +1144,9 @@ get_ip6t_bin)
filter_direct_node_list) filter_direct_node_list)
filter_direct_node_list filter_direct_node_list
;; ;;
update_wan_sets)
update_wan_sets "$@"
;;
stop) stop)
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 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() { add_firewall_rule() {
log_i18n 0 "Starting to load %s firewall rules..." "nftables" log_i18n 0 "Starting to load %s firewall rules..." "nftables"
gen_nft_tables gen_nft_tables
@@ -714,6 +728,8 @@ add_firewall_rule() {
[ -n "$lan_ip6" ] && echo $lan_ip6 | insert_nftset $NFTSET_LAN6 "-1" [ -n "$lan_ip6" ] && echo $lan_ip6 | insert_nftset $NFTSET_LAN6 "-1"
} }
update_wan_sets
[ -n "$ISP_DNS" ] && { [ -n "$ISP_DNS" ] && {
echo "$ISP_DNS" | insert_nftset $NFTSET_LAN 0 echo "$ISP_DNS" | insert_nftset $NFTSET_LAN 0
for ispip in $ISP_DNS; do 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" nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT"
fi fi
WAN_IP=$(get_wan_ips ip4) #ipv4 wan_ip
[ -n "${WAN_IP}" ] && { [ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
nft flush set $NFTABLE_NAME $NFTSET_WAN nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
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
ip rule add fwmark ${FWMARK} table 999 priority 999 ip rule add fwmark ${FWMARK} table 999 priority 999
ip route add local 0.0.0.0/0 dev lo table 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}') local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}')
if [[ "$dns_address" == *::* ]]; then 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" 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 else
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return" 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 fi
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")"
done 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_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\"" 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) nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
[ -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
ip -6 rule add fwmark ${FWMARK} table 999 priority 999 ip -6 rule add fwmark ${FWMARK} table 999 priority 999
ip -6 route add local ::/0 dev lo table 999 ip -6 route add local ::/0 dev lo table 999
@@ -894,7 +900,7 @@ add_firewall_rule() {
[ "$ENABLED_DEFAULT_ACL" == 1 ] && { [ "$ENABLED_DEFAULT_ACL" == 1 ] && {
TCP_LOCALHOST_PROXY=$LOCALHOST_PROXY TCP_LOCALHOST_PROXY=$LOCALHOST_PROXY
UDP_LOCALHOST_PROXY=$LOCALHOST_PROXY UDP_LOCALHOST_PROXY=$LOCALHOST_PROXY
msg="$(i18n "[Local],")" msg="$(i18n "[Local],")"
[ "$TCP_NO_REDIR_PORTS" != "disable" ] && { [ "$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" 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")" log 1 "${msg}$(i18n "not proxy all %s" "TCP")"
fi fi
} }
[ "$UDP_NO_REDIR_PORTS" != "disable" ] && { [ "$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 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" 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=" " local __nft=" "
__nft=$(cat <<- EOF __nft=$(cat <<- EOF
. $UTILS_PATH
[ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW2)" ] && nft -f ${nft_chain_file} [ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW2)" ] && nft -f ${nft_chain_file}
WAN_IP=\$(get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && { ${MY_PATH} update_wan_sets
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"
}
}
EOF EOF
) )
@@ -1150,6 +1145,9 @@ mwan3_start)
mwan3_stop) mwan3_stop)
mwan3_stop mwan3_stop
;; ;;
update_wan_sets)
update_wan_sets "$@"
;;
stop) stop)
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_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray") local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria") 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). -- 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 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 trojan_type_default = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) local DEFAULT_TROJAN_TYPE = 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 DEFAULT_VMESS_TYPE = 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 DEFAULT_VLESS_TYPE = 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_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local core_has = { 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 ["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 -- Determine whether to filter node keywords
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0" local function is_filter_keyword(mode, discard_list, keep_list, value)
local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {} if mode == "1" then
local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {} for k,v in ipairs(discard_list) do
local function is_filter_keyword(value)
if filter_keyword_mode_default == "1" then
for k,v in ipairs(filter_keyword_discard_list_default) do
if value:find(v, 1, true) then if value:find(v, 1, true) then
return true return true
end end
end end
elseif filter_keyword_mode_default == "2" then elseif mode == "2" then
local result = true 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 if value:find(v, 1, true) then
result = false result = false
end end
end end
return result return result
elseif filter_keyword_mode_default == "3" then elseif mode == "3" then
local result = false 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 if value:find(v, 1, true) then
result = true result = true
end end
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 if value:find(v, 1, true) then
result = false result = false
end end
end end
return result return result
elseif filter_keyword_mode_default == "4" then elseif mode == "4" then
local result = true 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 if value:find(v, 1, true) then
result = false result = false
end end
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 if value:find(v, 1, true) then
result = true result = true
end end
@@ -425,16 +426,16 @@ local function get_subscribe_info(cfgid, value)
end end
-- Configure the SS protocol implementation type -- Configure the SS protocol implementation type
local function set_ss_implementation(result) local function set_ss_implementation(ss_type, result)
if ss_type_default == "shadowsocks-libev" and has_ss then if ss_type == "shadowsocks-libev" and has_ss then
result.type = "SS" 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' 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.type = 'Xray'
result.protocol = 'shadowsocks' result.protocol = 'shadowsocks'
result.transport = 'raw' 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.type = 'sing-box'
result.protocol = 'shadowsocks' result.protocol = 'shadowsocks'
else else
@@ -445,8 +446,60 @@ local function set_ss_implementation(result)
end end
-- Processing data -- 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) --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 = { local result = {
timeout = 60, timeout = 60,
add_mode = add_mode, -- `0` for manual configuration, `1` for import, `2` for subscription 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) result.remarks = base64Decode(params.remarks)
elseif szType == 'vmess' then elseif szType == 'vmess' then
local info = jsonParse(content) 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' 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" result.type = "Xray"
else 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")) 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 if info.net == 'kcp' or info.net == 'mkcp' then
info.net = "mkcp" info.net = "mkcp"
result.mkcp_guise = info.type result.mkcp_guise = info.type
result.mkcp_mtu = 1350 result.mkcp_seed = info.seed
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
end end
if info.net == 'quic' then if info.net == 'quic' then
result.quic_guise = info.type result.quic_guise = info.type
@@ -599,7 +647,7 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = info.pcs result.tls_CertSha = info.pcs
result.tls_CertByName = info.vcn result.tls_CertByName = info.vcn
local insecure = info.allowinsecure or info.allowInsecure or info.insecure 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 else
result.tls = "0" result.tls = "0"
end end
@@ -615,7 +663,7 @@ local function processData(szType, content, add_mode, group)
return nil return nil
end end
elseif szType == "ss" then elseif szType == "ss" then
result = set_ss_implementation(result) result = set_ss_implementation(default_ss_type, result)
if not result then return nil end if not result then return nil end
--SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ] --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 if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp" result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none" 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 result.mkcp_seed = params.seed
end end
if params.type == 'quic' then if params.type == 'quic' then
@@ -884,7 +926,7 @@ local function processData(szType, content, add_mode, group)
end end
end end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure 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 result.uot = params.udp
elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then 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.") 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 params["shadow-tls"] then
if result.type ~= "sing-box" and result.type ~= "SS-Rust" 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 else
-- Parsing SS Shadow-TLS plugin parameters -- Parsing SS Shadow-TLS plugin parameters
local function parseShadowTLSParams(b64str, out) local function parseShadowTLSParams(b64str, out)
@@ -935,10 +977,10 @@ local function processData(szType, content, add_mode, group)
end end
end end
elseif szType == "trojan" then 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.type = 'sing-box'
result.protocol = 'trojan' 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.type = 'Xray'
result.protocol = 'trojan' result.protocol = 'trojan'
else else
@@ -988,7 +1030,7 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = params.pcs result.tls_CertSha = params.pcs
result.tls_CertByName = params.vcn result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.allowInsecure or params.insecure 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 if not params.type then params.type = "tcp" end
params.type = string.lower(params.type) 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 if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp" result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none" 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 result.mkcp_seed = params.seed
end end
if params.type == 'quic' then if params.type == 'quic' then
@@ -1079,7 +1115,7 @@ local function processData(szType, content, add_mode, group)
end end
end end
elseif szType == "ssd" then elseif szType == "ssd" then
result = set_ss_implementation(result) result = set_ss_implementation(default_ss_type, result)
if not result then return nil end if not result then return nil end
result.address = content.server result.address = content.server
result.port = content.port result.port = content.port
@@ -1090,9 +1126,9 @@ local function processData(szType, content, add_mode, group)
result.group = content.airport result.group = content.airport
result.remarks = content.remarks result.remarks = content.remarks
elseif szType == "vless" then 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' 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" result.type = "Xray"
else 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")) 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 if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp" result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none" result.mkcp_guise = params.headerType or "none"
result.mkcp_mtu = 1350 result.mkcp_seed = params.seed
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
end end
if params.type == 'quic' then if params.type == 'quic' then
result.quic_guise = params.headerType or "none" 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 result.reality_mldsa65Verify = params.pqv or nil
end end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure 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 end
result.port = port result.port = port
@@ -1307,7 +1338,7 @@ local function processData(szType, content, add_mode, group)
result.hysteria_auth_password = params.auth result.hysteria_auth_password = params.auth
result.tls_serverName = params.peer or params.sni or "" result.tls_serverName = params.peer or params.sni or ""
local insecure = params.allowinsecure or params.allowInsecure or params.insecure 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.alpn = params.alpn
result.hysteria_up_mbps = params.upmbps result.hysteria_up_mbps = params.upmbps
result.hysteria_down_mbps = params.downmbps 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_CertSha = params.pcs
result.tls_CertByName = params.vcn result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.insecure 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_tls_pinSHA256 = params.pinSHA256
result.hysteria2_hop = params.mport result.hysteria2_hop = params.mport
if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then
local is_singbox = hysteria2_type_default == "sing-box" and has_singbox local is_singbox = default_hysteria2_type == "sing-box" and has_singbox
result.type = is_singbox and 'sing-box' or 'Xray' result.type = is_singbox and 'sing-box' or 'Xray'
result.protocol = "hysteria2" result.protocol = "hysteria2"
if params["obfs-password"] or params["obfs_password"] then 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_congestion_control = params.congestion_control or "cubic"
result.tuic_udp_relay_mode = params.udp_relay_mode or "native" result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
local insecure = params.allowinsecure or params.insecure or params.allow_insecure 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 elseif szType == "anytls" then
if has_singbox then if has_singbox then
result.type = 'sing-box' result.type = 'sing-box'
@@ -1497,7 +1528,7 @@ local function processData(szType, content, add_mode, group)
end end
result.port = port result.port = port
local insecure = params.allowinsecure or params.insecure 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 end
elseif szType == 'naive+https' or szType == 'naive+quic' then elseif szType == 'naive+https' or szType == 'naive+quic' then
if has_singbox then if has_singbox then
@@ -1840,6 +1871,29 @@ local function update_node(manual)
for _, v in ipairs(nodeResult) do for _, v in ipairs(nodeResult) do
local remark = v["remark"] local remark = v["remark"]
local list = v["list"] 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 for _, vv in ipairs(list) do
local cfgid = uci:section(appname, "nodes", api.gen_short_uuid()) local cfgid = uci:section(appname, "nodes", api.gen_short_uuid())
for kkk, vvv in pairs(vv) do for kkk, vvv in pairs(vv) do
@@ -1929,8 +1983,12 @@ local function update_node(manual)
end end
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 if raw and #raw > 0 then
local cfgid
if sub_cfg then
cfgid = sub_cfg[".name"]
end
local nodes, szType local nodes, szType
local node_list = {} local node_list = {}
-- ssd appear to be in this format, starting with ssd://. -- 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 () xpcall(function ()
local result local result
if szType == 'ssd' then if szType == 'ssd' then
result = processData(szType, v, add_mode, group) result = processData(szType, v, add_mode, group, sub_cfg)
elseif not szType then elseif not szType then
local node = api.trim(v) local node = api.trim(v)
local dat = split(node, "://") local dat = split(node, "://")
if dat and dat[1] and dat[2] then if dat and dat[1] and dat[2] then
if dat[1] == 'vmess' or dat[1] == 'ssr' then if dat[1] == 'vmess' or dat[1] == 'ssr' then
local link = api.trim(dat[2]:gsub("#.*$", "")) 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 else
local link = dat[2]:gsub("&amp;", "&"):gsub("%s*#%s*", "#") -- Some odd links use "&" as "&", and include spaces before and after "#". local link = dat[2]:gsub("&amp;", "&"):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
end end
else 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) log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. result.error_msg)
elseif not result.type then elseif not result.type then
log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. i18n.translatef("No usable binary was found.")) 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 (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)) log(2, i18n.translatef("Discard filter nodes: %s type node %s", result.type, result.remarks))
else else
@@ -2007,7 +2065,8 @@ local function parse_link(raw, add_mode, group, cfgid)
if #node_list > 0 then if #node_list > 0 then
nodeResult[#nodeResult + 1] = { nodeResult[#nodeResult + 1] = {
remark = group, remark = group,
list = node_list list = node_list,
sub_cfg = sub_cfg
} }
end end
log(2, i18n.translatef("Successfully resolved the [%s] node, number: %s", group, #node_list)) 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 cfgid = value[".name"]
local remark = value.remark or "" local remark = value.remark or ""
local url = value.url 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 local url_is_local
domain_resolver_dns = value.domain_resolver_dns if fs.access(url) then
domain_resolver_dns_https = value.domain_resolver_dns_https -- debug, reads local files.
domain_strategy = (value.domain_strategy == "UseIPv4" or value.domain_strategy == "UseIPv6") and value.domain_strategy or nil log(1, i18n.translatef("Start subscribing: %s", '【' .. remark .. '】' .. url))
url_is_local = true
-- Subscription Group Chain Agent tmp_file = url
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
else 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 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 f = io.open(tmp_file, "r")
local stdout = f:read("*all") local stdout = f:read("*all")
@@ -2119,33 +2127,18 @@ local execute = function()
if not manual_sub and old_md5 == new_md5 then if not manual_sub and old_md5 == new_md5 then
log(1, i18n.translatef("Subscription: [%s] No changes, no update required.", remark)) log(1, i18n.translatef("Subscription: [%s] No changes, no update required.", remark))
else else
parse_link(raw_data, "2", remark, cfgid) parse_link(raw_data, "2", remark, value)
uci:set(appname, cfgid, "md5", new_md5) uci:set(appname, cfgid, "md5", new_md5)
end end
else else
fail_list[#fail_list + 1] = value fail_list[#fail_list + 1] = value
end end
if url_is_local then
value.http_code = 0
else
luci.sys.call("rm -f " .. tmp_file)
end
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 end
if #fail_list > 0 then if #fail_list > 0 then