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

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

View File

@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_VERSION:=0.1.10
PKG_RELEASE:=12
PKG_VERSION:=0.1.11
PKG_RELEASE:=13
LUCI_TITLE:=LuCI support for OpenClaw Launcher
LUCI_PKGARCH:=all

View File

@ -828,10 +828,15 @@
.oclm-dialog {
position: fixed;
inset: 0;
width: 100vw;
min-width: 100vw;
height: 100vh;
min-height: 100vh;
z-index: 9998;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 20px;
background: rgba(15, 23, 42, 0.42);
}
@ -932,10 +937,15 @@
.oclm-log-modal {
position: fixed;
inset: 0;
width: 100vw;
min-width: 100vw;
height: 100vh;
min-height: 100vh;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
padding: 20px;
background: rgba(15, 23, 42, 0.42);
}

View File

@ -65,6 +65,14 @@
}
};
var styleText = "";
var noticeOverlay = null;
var noticeCard = null;
var noticeTitle = null;
var noticeMessage = null;
var noticeCloseButton = null;
var actionDialogOverlay = null;
var actionDialogCard = null;
state.formErrors = state.formErrors || { base_dir: "" };
function request(url, options) {
options = options || {};
@ -197,6 +205,358 @@
return state.security;
}
function ensureNoticeDialog() {
if (noticeOverlay && noticeOverlay.parentNode) {
return;
}
noticeOverlay = document.createElement("div");
noticeOverlay.style.position = "fixed";
noticeOverlay.style.inset = "0";
noticeOverlay.style.zIndex = "2147483647";
noticeOverlay.style.display = "none";
noticeOverlay.style.alignItems = "center";
noticeOverlay.style.justifyContent = "center";
noticeOverlay.style.padding = "20px";
noticeOverlay.style.boxSizing = "border-box";
noticeOverlay.style.background = "rgba(15, 23, 42, 0.42)";
noticeCard = document.createElement("div");
noticeCard.style.width = "min(560px, calc(100vw - 40px))";
noticeCard.style.borderRadius = "18px";
noticeCard.style.border = "1px solid rgba(217, 225, 238, 0.95)";
noticeCard.style.background = "#ffffff";
noticeCard.style.boxShadow = "0 10px 28px rgba(15, 23, 42, 0.18)";
noticeCard.style.padding = "20px";
noticeCard.style.boxSizing = "border-box";
noticeCard.style.color = "#1f2937";
noticeTitle = document.createElement("h3");
noticeTitle.style.margin = "0 0 10px";
noticeTitle.style.fontSize = "18px";
noticeTitle.style.fontWeight = "700";
noticeMessage = document.createElement("div");
noticeMessage.style.marginBottom = "16px";
noticeMessage.style.color = "#6b7280";
noticeMessage.style.lineHeight = "1.6";
var footer = document.createElement("div");
footer.style.display = "flex";
footer.style.justifyContent = "flex-end";
footer.style.gap = "14px";
noticeCloseButton = document.createElement("button");
noticeCloseButton.type = "button";
noticeCloseButton.textContent = "知道了";
noticeCloseButton.style.appearance = "none";
noticeCloseButton.style.display = "inline-flex";
noticeCloseButton.style.alignItems = "center";
noticeCloseButton.style.justifyContent = "center";
noticeCloseButton.style.border = "1px solid #d9e1ee";
noticeCloseButton.style.background = "#2f67f6";
noticeCloseButton.style.color = "#ffffff";
noticeCloseButton.style.borderRadius = "10px";
noticeCloseButton.style.height = "36px";
noticeCloseButton.style.minWidth = "96px";
noticeCloseButton.style.padding = "0 14px";
noticeCloseButton.style.fontSize = "13px";
noticeCloseButton.style.fontWeight = "600";
noticeCloseButton.style.cursor = "pointer";
noticeCloseButton.onclick = function() {
closeNoticeDialog();
};
footer.appendChild(noticeCloseButton);
noticeCard.appendChild(noticeTitle);
noticeCard.appendChild(noticeMessage);
noticeCard.appendChild(footer);
noticeOverlay.appendChild(noticeCard);
document.body.appendChild(noticeOverlay);
}
function openNoticeDialog(message, title) {
ensureNoticeDialog();
noticeTitle.textContent = String(title || "提示");
noticeMessage.textContent = String(message || "");
noticeOverlay.style.display = "flex";
}
function closeNoticeDialog() {
if (!noticeOverlay) {
return;
}
noticeOverlay.style.display = "none";
}
function ensureActionDialog() {
if (actionDialogOverlay && actionDialogOverlay.parentNode) {
return;
}
actionDialogOverlay = document.createElement("div");
actionDialogOverlay.style.position = "fixed";
actionDialogOverlay.style.inset = "0";
actionDialogOverlay.style.zIndex = "2147483646";
actionDialogOverlay.style.display = "none";
actionDialogOverlay.style.alignItems = "center";
actionDialogOverlay.style.justifyContent = "center";
actionDialogOverlay.style.padding = "20px";
actionDialogOverlay.style.boxSizing = "border-box";
actionDialogOverlay.style.background = "rgba(15, 23, 42, 0.42)";
actionDialogCard = document.createElement("div");
actionDialogCard.style.width = "min(560px, calc(100vw - 40px))";
actionDialogCard.style.borderRadius = "18px";
actionDialogCard.style.border = "1px solid rgba(217, 225, 238, 0.95)";
actionDialogCard.style.background = "#ffffff";
actionDialogCard.style.boxShadow = "0 10px 28px rgba(15, 23, 42, 0.18)";
actionDialogCard.style.padding = "20px";
actionDialogCard.style.boxSizing = "border-box";
actionDialogCard.style.color = "#1f2937";
actionDialogOverlay.appendChild(actionDialogCard);
document.body.appendChild(actionDialogOverlay);
}
function makeActionDialogText(text, muted) {
var el = document.createElement("div");
el.textContent = String(text || "");
el.style.marginBottom = "10px";
el.style.lineHeight = "1.6";
el.style.color = muted ? "#6b7280" : "#1f2937";
return el;
}
function makeActionDialogCode(text) {
var el = document.createElement("code");
el.textContent = String(text || "");
el.style.display = "block";
el.style.marginBottom = "14px";
el.style.padding = "10px 12px";
el.style.borderRadius = "12px";
el.style.background = "rgba(47, 103, 246, 0.08)";
el.style.color = "#1f2937";
el.style.overflowWrap = "anywhere";
return el;
}
function makeActionDialogFooter() {
var el = document.createElement("div");
el.style.display = "flex";
el.style.justifyContent = "flex-end";
el.style.gap = "14px";
el.style.marginTop = "12px";
return el;
}
function makeActionDialogButton(label, kind, onclick) {
var button = document.createElement("button");
button.type = "button";
button.textContent = String(label || "");
button.style.appearance = "none";
button.style.display = "inline-flex";
button.style.alignItems = "center";
button.style.justifyContent = "center";
button.style.border = "1px solid " + (kind === "danger" ? "#ef4444" : kind === "primary" ? "#2f67f6" : "#d9e1ee");
button.style.background = kind === "danger" ? "#ef4444" : kind === "primary" ? "#2f67f6" : "#ffffff";
button.style.color = kind === "default" ? "#1f2937" : "#ffffff";
button.style.borderRadius = "10px";
button.style.minWidth = "96px";
button.style.minHeight = "36px";
button.style.padding = "0 14px";
button.style.fontSize = "13px";
button.style.fontWeight = "600";
button.style.cursor = "pointer";
button.onclick = onclick;
return button;
}
function makeActionDialogOption(title, note, onclick) {
var button = document.createElement("button");
var titleEl = document.createElement("span");
var noteEl = document.createElement("span");
button.type = "button";
button.style.appearance = "none";
button.style.display = "flex";
button.style.flexDirection = "column";
button.style.alignItems = "flex-start";
button.style.justifyContent = "flex-start";
button.style.gap = "4px";
button.style.width = "100%";
button.style.padding = "12px 14px";
button.style.border = "1px solid #d9e1ee";
button.style.borderRadius = "12px";
button.style.background = "#ffffff";
button.style.cursor = "pointer";
button.style.textAlign = "left";
button.onclick = onclick;
titleEl.textContent = String(title || "");
titleEl.style.color = "#1f2937";
titleEl.style.fontWeight = "700";
noteEl.textContent = String(note || "");
noteEl.style.color = "#6b7280";
noteEl.style.fontSize = "12px";
noteEl.style.fontWeight = "500";
button.appendChild(titleEl);
button.appendChild(noteEl);
return button;
}
function hideActionDialog() {
if (actionDialogOverlay) {
actionDialogOverlay.style.display = "none";
}
}
function syncActionDialog() {
ensureActionDialog();
while (actionDialogCard.firstChild) {
actionDialogCard.removeChild(actionDialogCard.firstChild);
}
var security = getSecurityState();
var deleteDialogOpen = security.deleteTargetId != null;
var addDialogOpen = !!security.pendingAddPath;
if (!deleteDialogOpen && !addDialogOpen) {
hideActionDialog();
return;
}
var title = document.createElement("h3");
title.style.margin = "0 0 12px";
title.style.fontSize = "18px";
title.style.fontWeight = "700";
actionDialogCard.appendChild(title);
if (deleteDialogOpen) {
var actions = document.createElement("div");
title.textContent = "删除禁止访问目录";
actionDialogCard.appendChild(makeActionDialogText("确定要从保护列表中移除以下目录吗?", true));
actionDialogCard.appendChild(makeActionDialogCode(security.deleteTargetPath || ""));
actionDialogCard.appendChild(makeActionDialogText("请选择删除方式:", true));
actions.style.display = "grid";
actions.style.gap = "10px";
actions.appendChild(makeActionDialogOption("直接删除", "仅从列表移除,不改变目录权限", function() {
handleSecurityDelete("direct");
}));
actions.appendChild(makeActionDialogOption("恢复其他用户组可访问drwxr-xr-x", "移除并恢复权限", function() {
handleSecurityDelete("restore");
}));
actionDialogCard.appendChild(actions);
var deleteFooter = makeActionDialogFooter();
deleteFooter.appendChild(makeActionDialogButton("取消", "default", function() {
var nextSecurity = getSecurityState();
nextSecurity.deleteTargetId = null;
nextSecurity.deleteTargetPath = "";
render();
}));
actionDialogCard.appendChild(deleteFooter);
} else if (addDialogOpen) {
title.textContent = "确认添加禁止访问目录";
actionDialogCard.appendChild(makeActionDialogText("添加后将尝试修改该目录权限,限制 OpenClaw 对其访问:", true));
actionDialogCard.appendChild(makeActionDialogCode(security.pendingAddPath || ""));
var warning = document.createElement("div");
warning.style.marginTop = "4px";
warning.style.marginBottom = "14px";
warning.style.padding = "12px 14px";
warning.style.border = "1px solid rgba(245, 158, 11, 0.24)";
warning.style.borderRadius = "12px";
warning.style.background = "rgba(245, 158, 11, 0.08)";
var warningTitle = document.createElement("div");
warningTitle.textContent = "请确认你已知晓以下风险:";
warningTitle.style.marginBottom = "8px";
warningTitle.style.color = "#1f2937";
warningTitle.style.fontSize = "13px";
warningTitle.style.fontWeight = "700";
var warningList = document.createElement("ul");
warningList.style.margin = "0";
warningList.style.paddingLeft = "18px";
warningList.style.color = "#6b7280";
[
"该目录的 Linux 权限会被修改为仅允许 root 和 root 组访问。",
"这可能影响其他服务、用户或脚本对该目录的正常访问。",
"本功能不是容器或沙箱隔离,只是基于系统文件权限限制。",
"请勿将 OpenClaw 自身的数据目录、程序目录加入禁止访问列表。"
].forEach(function(text) {
var item = document.createElement("li");
item.textContent = text;
warningList.appendChild(item);
});
warning.appendChild(warningTitle);
warning.appendChild(warningList);
actionDialogCard.appendChild(warning);
var addFooter = makeActionDialogFooter();
addFooter.appendChild(makeActionDialogButton("取消", "default", function() {
var nextSecurity = getSecurityState();
nextSecurity.pendingAddPath = "";
render();
}));
addFooter.appendChild(makeActionDialogButton("确认添加", "danger", function() {
handleSecurityAddConfirm();
}));
actionDialogCard.appendChild(addFooter);
}
actionDialogOverlay.style.display = "flex";
}
function handleSecurityAddConfirm() {
var security = getSecurityState();
var path = security.pendingAddPath || "";
if (!path) {
render();
return;
}
postJson(config.securityAddUrl, { path: path }).then(function(rv) {
if (!rv || !rv.ok) {
security.pendingAddPath = "";
security.directoryError = (rv && rv.error) || "添加目录失败";
render();
return;
}
security.pendingAddPath = "";
security.newDirectoryPath = "";
security.directoryError = "";
loadSecurityData();
}).catch(function() {
security.pendingAddPath = "";
security.directoryError = "添加目录失败";
render();
});
}
function handleSecurityDelete(mode) {
var security = getSecurityState();
if (security.deleteTargetId == null) {
return;
}
postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: mode }).then(function(rv) {
if (!rv || !rv.ok) {
openNoticeDialog((rv && rv.error) || (mode === "restore" ? "恢复并删除失败" : "删除失败"));
return;
}
security.deleteTargetId = null;
security.deleteTargetPath = "";
loadSecurityData();
}).catch(function() {
openNoticeDialog(mode === "restore" ? "恢复并删除失败" : "删除失败");
});
}
function securityDataDirectory() {
var formBaseDir = state.form && state.form.base_dir ? String(state.form.base_dir) : "";
var statusBaseDir = state.status && state.status.base_dir ? String(state.status.base_dir) : "";
@ -204,6 +564,19 @@
return formBaseDir || statusBaseDir || suggestedBaseDir || "";
}
function getFormErrors() {
state.formErrors = state.formErrors || { base_dir: "" };
if (state.formErrors.base_dir == null) {
state.formErrors.base_dir = "";
}
return state.formErrors;
}
function isBaseDirErrorMessage(message) {
var text = String(message || "");
return text.indexOf("数据目录") >= 0 || text.indexOf("base_dir") >= 0;
}
function validateProtectedDirectory(path) {
var security = getSecurityState();
var value = String(path || "").replace(/^\s+|\s+$/g, "");
@ -443,7 +816,7 @@
function showTaskLog(taskId) {
taskId = taskId || "openclawmgr";
if (!taskWindowAvailable()) {
window.alert("任务窗口不可用(未加载 taskd/xterm。请安装 luci-lib-taskd 和 luci-lib-xterm然后强制刷新页面。");
openNoticeDialog("任务窗口不可用(未加载 taskd/xterm。请安装 luci-lib-taskd 和 luci-lib-xterm然后强制刷新页面。");
return;
}
var now = Date.now ? Date.now() : (+new Date());
@ -548,6 +921,7 @@
function render() {
var status = state.status || {};
var form = state.form || {};
var formErrors = getFormErrors();
var options = state.options || { base_dir_choices: [] };
var security = getSecurityState();
var allowedOrigins = Array.isArray(form.allowed_origins) ? form.allowed_origins : [];
@ -590,9 +964,6 @@
'</div>' +
'</div>';
}).join("");
var addDialogOpen = !!security.pendingAddPath;
var deleteDialogOpen = security.deleteTargetId != null;
var canStartService = !status.running && !status.reachable;
var canStopService = !!status.running;
@ -648,7 +1019,8 @@
["loopback", "仅本机"],
["auto", "自动"]
])) +
fieldInput("数据目录", '<input class="oclm-control" type="text" id="oclm-base-dir" list="oclm-base-dir-options" value="' + escapeAttr(form.base_dir || "") + '" /><datalist id="oclm-base-dir-options">' + baseDirOptions + '</datalist>') +
fieldInput("数据目录", '<input class="oclm-control' + (formErrors.base_dir ? ' oclm-control-danger' : '') + '" type="text" id="oclm-base-dir" list="oclm-base-dir-options" value="' + escapeAttr(form.base_dir || "") + '" /><datalist id="oclm-base-dir-options">' + baseDirOptions + '</datalist>' +
'<div id="oclm-base-dir-error" class="oclm-security-error' + (formErrors.base_dir ? '' : ' oclm-hidden') + '">' + escapeHtml(formErrors.base_dir || "") + '</div>') +
'<div class="oclm-section-submit">' +
'<button class="oclm-button oclm-button-primary" type="button" id="oclm-save-basic"' + (savingBasic ? ' disabled' : '') + '>' + (savingBasic ? '应用中…' : '保存并应用') + '</button>' +
'<span id="oclm-service-actions" class="oclm-inline-buttons' + (showServiceActions ? '' : ' oclm-hidden') + '">' +
@ -745,45 +1117,10 @@
'<button class="oclm-button oclm-button-danger" type="button" data-op="uninstall">卸载</button>' +
'<button class="oclm-button oclm-button-danger" type="button" data-op="purge">彻底清理</button>' +
'</div>' +
'</div>' +
'<div class="oclm-dialog' + (deleteDialogOpen ? '' : ' oclm-hidden') + '">' +
'<div class="oclm-dialog-card">' +
'<h3>删除禁止访问目录</h3>' +
'<div class="oclm-dialog-text">确定要从保护列表中移除以下目录吗?</div>' +
'<code class="oclm-dialog-code">' + escapeHtml(security.deleteTargetPath || "") + '</code>' +
'<div class="oclm-dialog-text">请选择删除方式:</div>' +
'<div class="oclm-dialog-actions">' +
'<button class="oclm-button oclm-dialog-option" type="button" id="oclm-security-delete-direct"><span class="oclm-dialog-option-title">直接删除</span><span class="oclm-dialog-option-note">仅从列表移除,不改变目录权限</span></button>' +
'<button class="oclm-button oclm-dialog-option" type="button" id="oclm-security-delete-restore"><span class="oclm-dialog-option-title">恢复其他用户组可访问drwxr-xr-x</span><span class="oclm-dialog-option-note">移除并恢复权限</span></button>' +
'</div>' +
'<div class="oclm-dialog-footer">' +
'<button class="oclm-button oclm-button-danger" type="button" id="oclm-security-delete-cancel">取消</button>' +
'</div>' +
'</div>' +
'</div>' +
'<div class="oclm-dialog' + (addDialogOpen ? '' : ' oclm-hidden') + '">' +
'<div class="oclm-dialog-card">' +
'<h3>确认添加禁止访问目录</h3>' +
'<div class="oclm-dialog-text">添加后将尝试修改该目录权限,限制 OpenClaw 对其访问:</div>' +
'<code class="oclm-dialog-code">' + escapeHtml(security.pendingAddPath || "") + '</code>' +
'<div class="oclm-dialog-warning">' +
'<div class="oclm-dialog-warning-title">请确认你已知晓以下风险:</div>' +
'<ul>' +
'<li>该目录的 Linux 权限会被修改为仅允许 root 和 root 组访问。</li>' +
'<li>这可能影响其他服务、用户或脚本对该目录的正常访问。</li>' +
'<li>本功能不是容器或沙箱隔离,只是基于系统文件权限限制。</li>' +
'<li>请勿将 OpenClaw 自身的数据目录、程序目录加入禁止访问列表。</li>' +
'</ul>' +
'</div>' +
'<div class="oclm-dialog-footer">' +
'<button class="oclm-button" type="button" id="oclm-security-add-cancel">取消</button>' +
'<button class="oclm-button oclm-button-danger" type="button" id="oclm-security-add-confirm">确认添加</button>' +
'</div>' +
'</div>' +
'</div>' +
'</section>' +
'</div></div></div>';
syncActionDialog();
bindEvents();
}
@ -1036,7 +1373,7 @@
showTaskLog(rv.running_task_id);
return;
}
window.alert((rv && rv.error) || "操作失败");
openNoticeDialog((rv && rv.error) || "操作失败");
return;
}
state.lastTaskRunning = true;
@ -1049,12 +1386,12 @@
Array.prototype.forEach.call(root.querySelectorAll("[data-open-console]"), function(el) {
el.onclick = function() {
if (!state.consoleReady) {
window.alert("控制台正在启动,请稍候再试。");
openNoticeDialog("控制台正在启动,请稍候再试。");
return;
}
var url = (state.status && state.status.token_url) || "";
if (!url) {
window.alert("控制台地址不可用");
openNoticeDialog("控制台地址不可用");
return;
}
window.open(url, "_blank", "noreferrer");
@ -1068,7 +1405,7 @@
var baseDirEl = root.getElementById("oclm-base-dir");
var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : "";
if (!baseDir) {
window.alert("请先选择数据目录并保存应用,再执行安装。");
openNoticeDialog("请先选择数据目录并保存应用,再执行安装。");
if (baseDirEl) baseDirEl.focus();
return;
}
@ -1083,7 +1420,7 @@
if (!cfgRv || !cfgRv.ok) {
state.status.installing = false;
render();
window.alert((cfgRv && cfgRv.error) || "保存安装选项失败");
openNoticeDialog((cfgRv && cfgRv.error) || "保存安装选项失败");
return;
}
state.form.install_accelerated = accelerated;
@ -1099,7 +1436,7 @@
}
state.status.installing = false;
render();
window.alert((rv && rv.error) || "启动安装失败");
openNoticeDialog((rv && rv.error) || "启动安装失败");
return;
}
state.lastTaskRunning = true;
@ -1108,7 +1445,7 @@
}).catch(function() {
state.status.installing = false;
render();
window.alert("启动安装失败");
openNoticeDialog("启动安装失败");
});
};
});
@ -1121,7 +1458,7 @@
var baseDir = baseDirEl && baseDirEl.value ? baseDirEl.value.trim() : "";
var op = (state.status && state.status.installed) ? "upgrade" : "install";
if (!baseDir) {
window.alert("请先选择数据目录并保存应用,再执行安装。");
openNoticeDialog("请先选择数据目录并保存应用,再执行安装。");
if (baseDirEl) baseDirEl.focus();
return;
}
@ -1138,7 +1475,7 @@
state.status.task_running = false;
state.status.task_op = "";
render();
window.alert((cfgRv && cfgRv.error) || "保存安装选项失败");
openNoticeDialog((cfgRv && cfgRv.error) || "保存安装选项失败");
return;
}
state.form.install_accelerated = accelerated;
@ -1156,7 +1493,7 @@
state.status.task_running = false;
state.status.task_op = "";
render();
window.alert((rv && rv.error) || "启动安装失败");
openNoticeDialog((rv && rv.error) || "启动安装失败");
return;
}
state.lastTaskRunning = true;
@ -1167,7 +1504,7 @@
state.status.task_running = false;
state.status.task_op = "";
render();
window.alert("启动安装失败");
openNoticeDialog("启动安装失败");
});
};
});
@ -1191,7 +1528,7 @@
updateCheck.localVersion = String((rv && rv.local_version) || (state.status && state.status.openclaw_version) || "");
updateCheck.remoteVersion = String((rv && rv.remote_version) || "");
render();
window.alert(updateCheck.error);
openNoticeDialog(updateCheck.error);
return;
}
updateCheck.checked = true;
@ -1206,7 +1543,7 @@
updateCheck.hasUpdate = false;
updateCheck.error = "检测更新失败";
render();
window.alert("检测更新失败");
openNoticeDialog("检测更新失败");
});
};
});
@ -1349,55 +1686,18 @@
};
}
var securityAddCancel = root.getElementById("oclm-security-add-cancel");
if (securityAddCancel) {
securityAddCancel.onclick = function() {
var security = getSecurityState();
security.pendingAddPath = "";
render();
};
}
var securityAddConfirm = root.getElementById("oclm-security-add-confirm");
if (securityAddConfirm) {
securityAddConfirm.onclick = function() {
var security = getSecurityState();
var path = security.pendingAddPath || "";
if (!path) {
render();
return;
}
postJson(config.securityAddUrl, { path: path }).then(function(rv) {
if (!rv || !rv.ok) {
security.pendingAddPath = "";
security.directoryError = (rv && rv.error) || "添加目录失败";
render();
return;
}
security.pendingAddPath = "";
security.newDirectoryPath = "";
security.directoryError = "";
loadSecurityData();
}).catch(function() {
security.pendingAddPath = "";
security.directoryError = "添加目录失败";
render();
});
};
}
Array.prototype.forEach.call(root.querySelectorAll("[data-security-recheck]"), function(el) {
el.onclick = function() {
var id = el.getAttribute("data-security-recheck");
if (!id) return;
postJson(config.securityRecheckUrl, { id: id }).then(function(rv) {
if (!rv || !rv.ok) {
window.alert((rv && rv.error) || "重新检测失败");
openNoticeDialog((rv && rv.error) || "重新检测失败");
return;
}
loadSecurityData();
}).catch(function() {
window.alert("重新检测失败");
openNoticeDialog("重新检测失败");
});
};
});
@ -1420,61 +1720,31 @@
};
});
var securityDeleteDirect = root.getElementById("oclm-security-delete-direct");
if (securityDeleteDirect) {
securityDeleteDirect.onclick = function() {
var security = getSecurityState();
if (security.deleteTargetId == null) return;
postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: "direct" }).then(function(rv) {
if (!rv || !rv.ok) {
window.alert((rv && rv.error) || "删除失败");
return;
}
security.deleteTargetId = null;
security.deleteTargetPath = "";
loadSecurityData();
}).catch(function() {
window.alert("删除失败");
});
};
}
var securityDeleteRestore = root.getElementById("oclm-security-delete-restore");
if (securityDeleteRestore) {
securityDeleteRestore.onclick = function() {
var security = getSecurityState();
if (security.deleteTargetId == null) return;
postJson(config.securityRemoveUrl, { id: security.deleteTargetId, mode: "restore" }).then(function(rv) {
if (!rv || !rv.ok) {
window.alert((rv && rv.error) || "恢复并删除失败");
return;
}
security.deleteTargetId = null;
security.deleteTargetPath = "";
loadSecurityData();
}).catch(function() {
window.alert("恢复并删除失败");
});
};
}
var securityDeleteCancel = root.getElementById("oclm-security-delete-cancel");
if (securityDeleteCancel) {
securityDeleteCancel.onclick = function() {
var security = getSecurityState();
security.deleteTargetId = null;
security.deleteTargetPath = "";
render();
};
}
var saveBasic = root.getElementById("oclm-save-basic");
var baseDirInput = root.getElementById("oclm-base-dir");
if (baseDirInput) {
baseDirInput.oninput = function() {
var formErrors = getFormErrors();
if (formErrors.base_dir) {
formErrors.base_dir = "";
render();
}
};
}
if (saveBasic) {
saveBasic.onclick = function() {
var formErrors = getFormErrors();
var baseDirValue = root.getElementById("oclm-base-dir").value;
if (!String(baseDirValue || "").replace(/^\s+|\s+$/g, "")) {
formErrors.base_dir = "数据目录不能为空";
render();
return;
}
formErrors.base_dir = "";
var payload = {
port: root.getElementById("oclm-port").value,
bind: root.getElementById("oclm-bind").value,
base_dir: root.getElementById("oclm-base-dir").value,
base_dir: baseDirValue,
install_accelerated: !!(root.getElementById("oclm-install-accelerated") && root.getElementById("oclm-install-accelerated").checked),
};
state.savingSection = "basic";
@ -1482,7 +1752,7 @@
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "basic"); }).catch(function() {
state.savingSection = "";
render();
window.alert("保存失败");
openNoticeDialog("保存失败");
});
};
}
@ -1494,7 +1764,7 @@
var modelValue = normalizeModelValue(root.getElementById("oclm-model").value, defaultModelForAgent(agentValue));
var baseUrlValue = root.getElementById("oclm-base-url").value;
if (agentValue === "custom-provider" && !String(baseUrlValue || "").replace(/^\s+|\s+$/g, "")) {
window.alert("自定义供应商必须填写服务地址");
openNoticeDialog("自定义供应商必须填写服务地址");
return;
}
var payload = {
@ -1510,7 +1780,7 @@
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "ai"); }).catch(function() {
state.savingSection = "";
render();
window.alert("保存失败");
openNoticeDialog("保存失败");
});
};
}
@ -1529,7 +1799,7 @@
postJson(config.configUrl, payload).then(function(rv) { handleSaveResult(rv, "access"); }).catch(function() {
state.savingSection = "";
render();
window.alert("保存失败");
openNoticeDialog("保存失败");
});
};
}
@ -1537,16 +1807,25 @@
function handleSaveResult(rv, section) {
if (!rv || !rv.ok) {
var formErrors = getFormErrors();
state.savingSection = "";
if (section === "basic" && isBaseDirErrorMessage(rv && rv.error)) {
formErrors.base_dir = (rv && rv.error) || "数据目录错误";
render();
return;
}
render();
window.alert((rv && rv.error) || "保存失败");
openNoticeDialog((rv && rv.error) || "保存失败");
return;
}
if (section === "basic") {
getFormErrors().base_dir = "";
}
postForm(config.applyUrl, {}).then(function(applyRv) {
state.savingSection = "";
if (!applyRv || !applyRv.ok) {
render();
window.alert((applyRv && applyRv.error) || "应用配置失败");
openNoticeDialog((applyRv && applyRv.error) || "应用配置失败");
loadConfig();
scheduleStatusRefresh(3, 600);
return;
@ -1558,7 +1837,7 @@
}).catch(function() {
state.savingSection = "";
render();
window.alert("应用配置失败");
openNoticeDialog("应用配置失败");
loadConfig();
scheduleStatusRefresh(3, 600);
});

View File

@ -211,6 +211,37 @@ local function trim(v)
return tostring(v or ""):gsub("^%s+", ""):gsub("%s+$", "")
end
local function normalize_dir_path(path)
path = trim(path)
if path == "" then
return ""
end
if path ~= "/" then
path = path:gsub("/+$", "")
if path == "" then
path = "/"
end
end
return path
end
local function validate_base_dir_input(path)
path = normalize_dir_path(path)
if path == "" then
return false, "数据目录不能为空"
end
if not path:match("^/") then
return false, "数据目录必须是绝对路径"
end
if path == "/" then
return false, "数据目录不能使用 / 根目录"
end
if path == "/root" or path:match("^/root/") then
return false, "数据目录不能使用 /root 开头的目录"
end
return true, nil, path
end
local function split_dots(v)
local parts = {}
for part in tostring(v or ""):gmatch("[^%.]+") do
@ -1231,11 +1262,13 @@ function action_config_data()
end
if has("base_dir") then
local base_dir = tostring(body.base_dir or "")
if base_dir == "" then
write_json({ ok = false, error = "base_dir required" })
local ok, err, normalized = validate_base_dir_input(body.base_dir or "")
if not ok then
write_json({ ok = false, error = err })
return
end
body.base_dir = normalized
local base_dir = normalized
uci:set("openclawmgr", section, "base_dir", base_dir)
end

View File

@ -24,6 +24,9 @@ validate_base_dir() {
*) return 1 ;;
esac
[ "$dir" != "/" ] || return 1
case "$dir" in
/root|/root/*) return 1 ;;
esac
case "$dir" in
*"/../"*|*"/.."|*"/./"*|*"/.") return 1 ;;
esac

View File

@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.4.15
PKG_RELEASE:=83
PKG_RELEASE:=84
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \

View File

@ -30,60 +30,61 @@ local has_trojan_plus = api.is_finded("trojan-plus")
local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria")
local allowInsecure_default = nil
local DEFAULT_ALLOWINSECURE = true
local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
-- 取节点使用core类型节点订阅页面未设置时自动取默认
local ss_type_default = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local trojan_type_default = api.get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local vmess_type_default = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local vless_type_default = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local hysteria2_type_default = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_trojan_plus,"trojan-plus"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local core_has = {
["xray"] = has_xray, ["sing-box"] = has_singbox, ["shadowsocks-libev"] = has_ss,["shadowsocks-rust"] = has_ss_rust,
["trojan-plus"] = has_trojan_plus, ["hysteria2"] = has_hysteria2
["xray"] = has_xray,
["sing-box"] = has_singbox,
["shadowsocks-libev"] = has_ss,
["shadowsocks-rust"] = has_ss_rust,
["trojan-plus"] = has_trojan_plus,
["hysteria2"] = has_hysteria2
}
----
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
-- 判断是否过滤节点关键字
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
local function is_filter_keyword(value)
if filter_keyword_mode_default == "1" then
for k,v in ipairs(filter_keyword_discard_list_default) do
local function is_filter_keyword(mode, discard_list, keep_list, value)
if mode == "1" then
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
return true
end
end
elseif filter_keyword_mode_default == "2" then
elseif mode == "2" then
local result = true
for k,v in ipairs(filter_keyword_keep_list_default) do
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif filter_keyword_mode_default == "3" then
elseif mode == "3" then
local result = false
for k,v in ipairs(filter_keyword_discard_list_default) do
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
end
for k,v in ipairs(filter_keyword_keep_list_default) do
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif filter_keyword_mode_default == "4" then
elseif mode == "4" then
local result = true
for k,v in ipairs(filter_keyword_keep_list_default) do
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
for k,v in ipairs(filter_keyword_discard_list_default) do
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
@ -448,16 +449,16 @@ local function get_subscribe_info(cfgid, value)
end
-- 设置 ss 协议实现类型
local function set_ss_implementation(result)
if ss_type_default == "shadowsocks-libev" and has_ss then
local function set_ss_implementation(ss_type, result)
if ss_type == "shadowsocks-libev" and has_ss then
result.type = "SS"
elseif ss_type_default == "shadowsocks-rust" and has_ss_rust then
elseif ss_type == "shadowsocks-rust" and has_ss_rust then
result.type = 'SS-Rust'
elseif ss_type_default == "xray" and has_xray then
elseif ss_type == "xray" and has_xray then
result.type = 'Xray'
result.protocol = 'shadowsocks'
result.transport = 'raw'
elseif ss_type_default == "sing-box" and has_singbox then
elseif ss_type == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = 'shadowsocks'
else
@ -468,8 +469,60 @@ local function set_ss_implementation(result)
end
-- 处理数据
local function processData(szType, content, add_mode, group)
--log(content, add_mode, group)
local function processData(szType, content, add_mode, group, sub_cfg)
--log(2, content, add_mode, group)
local default_allowinsecure = DEFAULT_ALLOWINSECURE
default_filter_keyword_mode = DEFAULT_FILTER_KEYWORD_MODE
default_filter_keyword_discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST
default_filter_keyword_keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST
local default_ss_type = DEFAULT_SS_TYPE
local default_trojan_type = DEFAULT_TROJAN_TYPE
local default_vmess_type = DEFAULT_VMESS_TYPE
local default_vless_type = DEFAULT_VLESS_TYPE
local default_hysteria2_type = DEFAULT_HYSTERIA2_TYPE
if sub_cfg then
if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then
default_allowinsecure = nil
end
local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global
if filter_keyword_mode == "0" then
default_filter_keyword_mode = "0"
elseif filter_keyword_mode == "1" then
default_filter_keyword_mode = "1"
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "2" then
default_filter_keyword_mode = "2"
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
elseif filter_keyword_mode == "3" then
default_filter_keyword_mode = "3"
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "4" then
default_filter_keyword_mode = "4"
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
end
local ss_type = sub_cfg.ss_type or "global"
if ss_type ~= "global" and core_has[ss_type] then
default_ss_type = ss_type
end
local trojan_type = sub_cfg.trojan_type or "global"
if trojan_type ~= "global" and core_has[trojan_type] then
default_trojan_type = trojan_type
end
local vmess_type = sub_cfg.vmess_type or "global"
if vmess_type ~= "global" and core_has[vmess_type] then
default_vmess_type = vmess_type
end
local vless_type = sub_cfg.vless_type or "global"
if vless_type ~= "global" and core_has[vless_type] then
default_vless_type = vless_type
end
local hysteria2_type = sub_cfg.hysteria2_type or "global"
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
default_hysteria2_type = hysteria2_type
end
end
local result = {
timeout = 60,
add_mode = add_mode, --0为手动配置,1为导入,2为订阅
@ -509,9 +562,9 @@ local function processData(szType, content, add_mode, group)
result.remarks = base64Decode(params.remarks)
elseif szType == 'vmess' then
local info = jsonParse(content)
if vmess_type_default == "sing-box" and has_singbox then
if default_vmess_type == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif vmess_type_default == "xray" and has_xray then
elseif default_vmess_type == "xray" and has_xray then
result.type = "Xray"
else
log("跳过 VMess 节点,因未适配到 VMess 核心程序,或未正确设置节点使用类型。")
@ -606,7 +659,7 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = info.pcs
result.tls_CertByName = info.vcn
local insecure = info.allowinsecure or info.allowInsecure or info.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
else
result.tls = "0"
end
@ -622,7 +675,7 @@ local function processData(szType, content, add_mode, group)
return nil
end
elseif szType == "ss" then
result = set_ss_implementation(result)
result = set_ss_implementation(default_ss_type, result)
if not result then return nil end
--SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ]
@ -886,7 +939,7 @@ local function processData(szType, content, add_mode, group)
end
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
result.uot = params.udp
elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then
result.error_msg = "请更换 Xray 或 Sing-Box 来支持 SS 更多的传输方式。"
@ -895,7 +948,7 @@ local function processData(szType, content, add_mode, group)
if params["shadow-tls"] then
if result.type ~= "sing-box" and result.type ~= "SS-Rust" then
result.error_msg = ss_type_default .. " 不支持 shadow-tls 插件。"
result.error_msg = default_ss_type .. " 不支持 shadow-tls 插件。"
else
-- 解析SS Shadow-TLS 插件参数
local function parseShadowTLSParams(b64str, out)
@ -937,12 +990,12 @@ local function processData(szType, content, add_mode, group)
end
end
elseif szType == "trojan" then
if trojan_type_default == "trojan-plus" and has_trojan_plus then
if default_trojan_type == "trojan-plus" and has_trojan_plus then
result.type = "Trojan-Plus"
elseif trojan_type_default == "sing-box" and has_singbox then
elseif default_trojan_type == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = 'trojan'
elseif trojan_type_default == "xray" and has_xray then
elseif default_trojan_type == "xray" and has_xray then
result.type = 'Xray'
result.protocol = 'trojan'
else
@ -991,7 +1044,7 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = params.pcs
result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
if not params.type then params.type = "tcp" end
params.type = string.lower(params.type)
@ -1077,7 +1130,7 @@ local function processData(szType, content, add_mode, group)
end
elseif szType == "ssd" then
result = set_ss_implementation(result)
result = set_ss_implementation(default_ss_type, result)
if not result then return nil end
result.address = content.server
result.port = content.port
@ -1088,9 +1141,9 @@ local function processData(szType, content, add_mode, group)
result.group = content.airport
result.remarks = content.remarks
elseif szType == "vless" then
if vless_type_default == "sing-box" and has_singbox then
if default_vless_type == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif vless_type_default == "xray" and has_xray then
elseif default_vless_type == "xray" and has_xray then
result.type = "Xray"
else
log("跳过 VLESS 节点,因未适配到 VLESS 核心程序,或未正确设置节点使用类型。")
@ -1243,7 +1296,7 @@ local function processData(szType, content, add_mode, group)
result.reality_mldsa65Verify = params.pqv or nil
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
end
result.port = port
@ -1300,7 +1353,7 @@ local function processData(szType, content, add_mode, group)
result.hysteria_auth_password = params.auth
result.tls_serverName = params.peer or params.sni or ""
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
result.alpn = params.alpn
result.hysteria_up_mbps = params.upmbps
result.hysteria_down_mbps = params.downmbps
@ -1346,12 +1399,12 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = params.pcs
result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
result.hysteria2_tls_pinSHA256 = params.pinSHA256
result.hysteria2_hop = params.mport
if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then
local is_singbox = hysteria2_type_default == "sing-box" and has_singbox
if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then
local is_singbox = default_hysteria2_type == "sing-box" and has_singbox
result.type = is_singbox and 'sing-box' or 'Xray'
result.protocol = "hysteria2"
if params["obfs-password"] or params["obfs_password"] then
@ -1426,7 +1479,7 @@ local function processData(szType, content, add_mode, group)
result.tuic_congestion_control = params.congestion_control or "cubic"
result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
local insecure = params.allowinsecure or params.insecure or params.allow_insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
elseif szType == "anytls" then
if has_singbox then
result.type = 'sing-box'
@ -1490,7 +1543,7 @@ local function processData(szType, content, add_mode, group)
end
result.port = port
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
end
elseif szType == 'naive+https' or szType == 'naive+quic' then
if has_singbox then
@ -1829,6 +1882,30 @@ local function update_node(manual)
for _, v in ipairs(nodeResult) do
local remark = v["remark"]
local list = v["list"]
local sub_cfg = v["sub_cfg"]
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
-- Subscription Group Chain Agent
local function valid_chain_node(node)
if not node then return "" end
local cp = uci:get(appname, node, "chain_proxy") or ""
local am = uci:get(appname, node, "add_mode") or "0"
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
chain_node_type = ""
return ""
end
return node
end
if sub_cfg then
domain_resolver = sub_cfg.domain_resolver
domain_resolver_dns = sub_cfg.domain_resolver_dns
domain_resolver_dns_https = sub_cfg.domain_resolver_dns_https
local map = { UseIPv4v6 = 1, UseIPv6v4 = 1, UseIPv4 = 1, UseIPv6 = 1 }
domain_strategy = (sub_cfg.domain_strategy and map[sub_cfg.domain_strategy]) and sub_cfg.domain_strategy or nil
preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or ""
to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or ""
end
for _, vv in ipairs(list) do
local cfgid = uci:section(appname, "nodes", api.gen_short_uuid())
for kkk, vvv in pairs(vv) do
@ -1916,8 +1993,12 @@ local function update_node(manual)
end
end
local function parse_link(raw, add_mode, group, cfgid)
local function parse_link(raw, add_mode, group, sub_cfg)
if raw and #raw > 0 then
local cfgid
if sub_cfg then
cfgid = sub_cfg[".name"]
end
local nodes, szType
local node_list = {}
-- SSD 似乎是这种格式 ssd:// 开头的
@ -1952,17 +2033,17 @@ local function parse_link(raw, add_mode, group, cfgid)
xpcall(function ()
local result
if szType == 'ssd' then
result = processData(szType, v, add_mode, group)
result = processData(szType, v, add_mode, group, sub_cfg)
elseif not szType then
local node = api.trim(v)
local dat = split(node, "://")
if dat and dat[1] and dat[2] then
if dat[1] == 'vmess' or dat[1] == 'ssr' then
local link = api.trim(dat[2]:gsub("#.*$", ""))
result = processData(dat[1], base64Decode(link), add_mode, group)
result = processData(dat[1], base64Decode(link), add_mode, group, sub_cfg)
else
local link = dat[2]:gsub("&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
else
@ -1974,7 +2055,7 @@ local function parse_link(raw, add_mode, group, cfgid)
log('丢弃节点:' .. result.remarks .. " ,原因:" .. result.error_msg)
elseif not result.type then
log('丢弃节点:' .. result.remarks .. " ,找不到可使用二进制。")
elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
elseif (add_mode == "2" and is_filter_keyword(default_filter_keyword_mode, default_filter_keyword_discard_list, default_filter_keyword_keep_list, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
(not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then
log('丢弃过滤节点:' .. result.type .. ' 节点,' .. result.remarks)
else
@ -1994,7 +2075,8 @@ local function parse_link(raw, add_mode, group, cfgid)
if #node_list > 0 then
nodeResult[#nodeResult + 1] = {
remark = group,
list = node_list
list = node_list,
sub_cfg = sub_cfg
}
end
log('成功解析【' .. group .. '】节点数量:' .. #node_list)
@ -2025,78 +2107,26 @@ local execute = function()
local cfgid = value[".name"]
local remark = value.remark or ""
local url = value.url or ""
if value.allowInsecure and value.allowInsecure == "1" then
allowInsecure_default = true
end
local filter_keyword_mode = value.filter_keyword_mode or "5"
if filter_keyword_mode == "0" then
filter_keyword_mode_default = "0"
elseif filter_keyword_mode == "1" then
filter_keyword_mode_default = "1"
filter_keyword_discard_list_default = value.filter_discard_list or {}
elseif filter_keyword_mode == "2" then
filter_keyword_mode_default = "2"
filter_keyword_keep_list_default = value.filter_keep_list or {}
elseif filter_keyword_mode == "3" then
filter_keyword_mode_default = "3"
filter_keyword_keep_list_default = value.filter_keep_list or {}
filter_keyword_discard_list_default = value.filter_discard_list or {}
elseif filter_keyword_mode == "4" then
filter_keyword_mode_default = "4"
filter_keyword_keep_list_default = value.filter_keep_list or {}
filter_keyword_discard_list_default = value.filter_discard_list or {}
end
local ss_type = value.ss_type or "global"
if ss_type ~= "global" and core_has[ss_type] then
ss_type_default = ss_type
end
local trojan_type = value.trojan_type or "global"
if trojan_type ~= "global" and core_has[trojan_type] then
trojan_type_default = trojan_type
end
local vmess_type = value.vmess_type or "global"
if vmess_type ~= "global" and core_has[vmess_type] then
vmess_type_default = vmess_type
end
local vless_type = value.vless_type or "global"
if vless_type ~= "global" and core_has[vless_type] then
vless_type_default = vless_type
end
local hysteria2_type = value.hysteria2_type or "global"
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
hysteria2_type_default = hysteria2_type
end
domain_resolver = value.domain_resolver
domain_resolver_dns = value.domain_resolver_dns
domain_resolver_dns_https = value.domain_resolver_dns_https
local map = { UseIPv4v6 = 1, UseIPv6v4 = 1, UseIPv4 = 1, UseIPv6 = 1 }
domain_strategy = (value.domain_strategy and map[value.domain_strategy]) and value.domain_strategy or nil
-- 订阅组链式代理
local function valid_chain_node(node)
if not node then return "" end
local cp = uci:get(appname, node, "chain_proxy") or ""
local am = uci:get(appname, node, "add_mode") or "0"
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
chain_node_type = ""
return ""
end
return node
end
preproxy_node_group = (value.chain_proxy == "1") and valid_chain_node(value.preproxy_node) or ""
to_node_group = (value.chain_proxy == "2") and valid_chain_node(value.to_node) or ""
local ua = value.user_agent
local access_mode = value.access_mode
local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连访问" or (access_mode == "proxy" and "通过代理" or "自动"))
log('正在订阅:【' .. remark .. '' .. url .. ' [' .. result .. ']')
local tmp_file = "/tmp/" .. cfgid
value.http_code = curl(url, tmp_file, ua, access_mode)
if value.http_code ~= 200 then
fail_list[#fail_list + 1] = value
local url_is_local
if fs.access(url) then
-- debug, reads local files.
log('正在订阅:【' .. remark .. '' .. url)
url_is_local = true
tmp_file = url
else
local ua = value.user_agent
local access_mode = value.access_mode
local result = (not access_mode) and "自动" or (access_mode == "direct" and "直连" or (access_mode == "proxy" and "代理" or "自动"))
log('正在订阅:【' .. remark .. '' .. url .. ' [' .. result .. ']')
tmp_file = "/tmp/" .. cfgid
value.http_code = curl(url, tmp_file, ua, access_mode)
if value.http_code ~= 200 then
fail_list[#fail_list + 1] = value
luci.sys.call("rm -f " .. tmp_file)
end
end
if fs.access(tmp_file) then
if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then
local f = io.open(tmp_file, "r")
local stdout = f:read("*all")
@ -2107,33 +2137,18 @@ local execute = function()
if not manual_sub and old_md5 == new_md5 then
log('订阅:【' .. remark .. '】没有变化,无需更新。')
else
parse_link(raw_data, "2", remark, cfgid)
parse_link(raw_data, "2", remark, value)
uci:set(appname, cfgid, "md5", new_md5)
end
else
fail_list[#fail_list + 1] = value
end
if url_is_local then
value.http_code = 0
else
luci.sys.call("rm -f " .. tmp_file)
end
end
luci.sys.call("rm -f " .. tmp_file)
allowInsecure_default = nil
filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
ss_type = uci:get(appname, "@global_subscribe[0]", "ss_type") or ""
ss_type_default = core_has[ss_type] and ss_type or ss_type_default
trojan_type = uci:get(appname, "@global_subscribe[0]", "trojan_type") or ""
trojan_type_default = core_has[trojan_type] and trojan_type or trojan_type_default
vmess_type = uci:get(appname, "@global_subscribe[0]", "vmess_type") or ""
vmess_type_default = core_has[vmess_type] and vmess_type or vmess_type_default
vless_type = uci:get(appname, "@global_subscribe[0]", "vless_type") or ""
vless_type_default = core_has[vless_type] and vless_type or vless_type_default
hysteria2_type = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or ""
hysteria2_type_default = core_has[hysteria2_type] and hysteria2_type or hysteria2_type_default
end
if #fail_list > 0 then

View File

@ -6,8 +6,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.4.10
PKG_RELEASE:=37
PKG_VERSION:=26.4.20
PKG_RELEASE:=38
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \

View File

@ -268,8 +268,8 @@ o = s:taboption("DNS", Flag, "dns_redirect", translate("DNS Redirect"), translat
o.default = "1"
o.rmempty = false
local use_nft = m:get("@global_forwarding[0]", "use_nft") == "1"
local set_title = api.i18n.translate(use_nft and "Clear NFTSET" or "Clear IPSET")
local prefer_nft = m:get("@global_forwarding[0]", "prefer_nft") == "1"
local set_title = api.i18n.translate(prefer_nft and "Clear NFTSET" or "Clear IPSET")
o = s:taboption("DNS", DummyValue, "clear_ipset", set_title, translate("Try this feature if the rule modification does not take effect."))
o.rawhtml = true
function o.cfgvalue(self, section)

View File

@ -48,15 +48,23 @@ o.default = ""
o:depends("console_auth", true)
---- Console Port
o = s:option(Value, "console_port", translate("Console Port"), translate(
"In the browser input routing IP plus port access, such as:192.168.1.1:1188"))
o.default = "1188"
o = s:option(Value, "console_port", translate("Console Port"), translate("In the browser input routing IP plus port access, such as:192.168.1.1:1188"))
o.datatype = "port"
o.default = 1188
o:depends("balancing_enable", true)
o = s:option(Flag, "bind_local", translate("Haproxy Port") .. " " .. translate("Bind Local"), translate("When selected, it can only be accessed localhost."))
o.default = "0"
o:depends("balancing_enable", true)
o = s:option(ListValue, "balancingStrategy", translate("Balancing Strategy"))
o:value("roundrobin", "roundRobin")
o:value("random", "random")
o:value("leastconn", "leastConn")
o:value("source", "source")
o.default = "roundrobin"
o:depends("balancing_enable", true)
---- Health Check Type
o = s:option(ListValue, "health_check_type", translate("Health Check Type"))
o.default = "passwall_logic"
@ -77,9 +85,11 @@ o.description = translate("The URL used to detect the connection status.")
o:depends("health_check_type", "passwall_logic")
---- Health Check Inter
o = s:option(Value, "health_check_inter", translate("Health Check Inter"), translate("Units:seconds"))
o.default = "60"
o = s:option(Value, "health_check_inter", translate("Health Check Inter"))
o.datatype = "min(20)"
o.default = 60
o:depends("balancing_enable", true)
o.description = translate("Units:seconds") .. " ( ≥ 20s )"
o = s:option(DummyValue, "health_check_tips", " ")
o.rawhtml = true
@ -89,11 +99,10 @@ end
o:depends("health_check_type", "passwall_logic")
-- [[ Balancing Settings ]]--
s = m:section(TypedSection, "haproxy_config", translate("Node List"),
"<font color='red'>" ..
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") ..
"\n" .. translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") ..
"</font>")
s = m:section(TypedSection, "haproxy_config", translate("Node List"))
s.description = "<font color='red'>" ..
translate("Add a node, Export Of Multi WAN Only support Multi Wan. Load specific gravity range 1-256. Multiple primary servers can be load balanced, standby will only be enabled when the primary server is offline! Multiple groups can be set, Haproxy port same one for each group.") .. "<br>" ..
translate("Note that the node configuration parameters for load balancing must be consistent when use TCP health check type, otherwise it cannot be used normally!") .. "</font>"
s.template = "cbi/tblsection"
s.sortable = true
s.anonymous = true
@ -148,7 +157,7 @@ o.rmempty = false
---- Node Weight
o = s:option(Value, "lbweight", translate("Node Weight"))
o.datatype = "uinteger"
o.datatype = "min(1)"
o.default = 5
o.rmempty = false

View File

@ -8,7 +8,7 @@ if not arg[1] or not m:get(arg[1]) then
luci.http.redirect(api.url())
end
m:append(Template(appname .. "/cbi/nodes_multivalue_com"))
m:append(Template(appname .. "/cbi/nodes_dynamiclist_com"))
m:append(Template(appname .. "/cbi/nodes_listvalue_com"))
local has_singbox = api.finded_com("sing-box")
@ -95,41 +95,28 @@ o.datatype = "min(1)"
o.default = 1
o:depends("enable_autoswitch", true)
o = s:option(MultiValue, "autoswitch_backup_node", translate("List of backup nodes"))
o = s:option(DynamicList, "autoswitch_backup_node", translate("List of backup nodes"))
o:depends("enable_autoswitch", true)
o.widget = "checkbox"
o.template = appname .. "/cbi/nodes_multivalue"
o.template = appname .. "/cbi/nodes_dynamiclist"
o.group = {}
o.write = function(self, section, value)
local n = s.fields["node"]:formvalue(section)
local v = type(value) == "table" and value or { value }
local t = {}
for _, x in ipairs(v) do
if x and x ~= n then t[#t+1] = x end
end
return DynamicList.write(self, section, t)
end
for i, v in pairs(nodes_table) do
o:value(v.id, v.remark)
o.group[#o.group+1] = v.group or ""
if v.protocol ~= "_shunt" then
o:value(v.id, v.remark)
o.group[#o.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
s.fields["enable_autoswitch"]:depends({ node = v.id })
end
socks_node:value(v.id, v["remark"])
socks_node.group[#socks_node.group+1] = (v.group and v.group ~= "") and v.group or translate("default")
end
-- Reading the old DynamicList
function o.cfgvalue(self, section)
return m.uci:get_list(appname, section, "autoswitch_backup_node") or {}
end
-- Write-and-hold DynamicList
function o.write(self, section, value)
local old = m.uci:get_list(appname, section, "autoswitch_backup_node") or {}
local new, set = {}, {}
for v in value:gmatch("%S+") do
new[#new + 1] = v
set[v] = 1
end
for _, v in ipairs(old) do
if not set[v] then
m.uci:set_list(appname, section, "autoswitch_backup_node", new)
return
end
set[v] = nil
end
for _ in pairs(set) do
m.uci:set_list(appname, section, "autoswitch_backup_node", new)
return
end
end
o = s:option(Flag, "autoswitch_restore_switch", translate("Restore Switch"), translate("When detects main node is available, switch back to the main node."))
o:depends("enable_autoswitch", true)

View File

@ -506,33 +506,6 @@ o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
o:depends({ [_n("mkcp_guise")] = "dns" })
o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU"))
o.default = "1350"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_tti"), translate("KCP TTI"))
o.default = "20"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity"))
o.default = "5"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity"))
o.default = "20"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Flag, _n("mkcp_congestion"), translate("KCP Congestion"))
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_readBufferSize"), translate("KCP readBufferSize"))
o.default = "1"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_writeBufferSize"), translate("KCP writeBufferSize"))
o.default = "1"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed"))
o:depends({ [_n("transport")] = "mkcp" })
@ -645,6 +618,14 @@ end
-- [[ User-Agent ]]--
o = s:option(Value, _n("user_agent"), translate("User-Agent"))
o.default = ""
o:value("", translate("default"))
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge")
o:value("Go-http-client/1.1", "golang")
o:value("curl/7.68.0", "curl")
o:depends({ [_n("tcp_guise")] = "http" })
o:depends({ [_n("transport")] = "ws" })
o:depends({ [_n("transport")] = "httpupgrade" })

View File

@ -416,8 +416,14 @@ if singbox_tags:find("with_quic") then
end
-- [[ SSH config start ]] --
o = s:option(Value, _n("ssh_priv_key"), translate("Private Key"))
o = s:option(TextValue, _n("ssh_priv_key"), translate("Private Key"))
o.rows = 5
o.wrap = "off"
o:depends({ [_n("protocol")] = "ssh" })
o.validate = function(self, value)
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
o = s:option(Value, _n("ssh_priv_key_pp"), translate("Private Key Passphrase"))
o.password = true
@ -509,11 +515,7 @@ o.rows = 5
o.wrap = "off"
o:depends({ [_n("ech")] = true })
o.validate = function(self, value)
value = value:gsub("^%s+", ""):gsub("%s+$","\n"):gsub("\r\n","\n"):gsub("[ \t]*\n[ \t]*", "\n")
value = value:gsub("^%s*\n", "")
if value:sub(-1) == "\n" then
value = value:sub(1, -2)
end
value = api.trim(value):gsub("\r\n", "\n"):gsub("[ \t]*\n[ \t]*", "\n"):gsub("\n+", "\n")
return value
end
@ -681,6 +683,14 @@ o:depends({ [_n("grpc_health_check")] = true })
-- [[ User-Agent ]]--
o = s:option(Value, _n("user_agent"), translate("User-Agent"))
o.default = ""
o:value("", translate("default"))
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.131 Safari/537.36", "chrome")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:90.0) Gecko/20100101 Firefox/90.0", "firefox")
o:value("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.1 Safari/605.1.15", "safari")
o:value("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36 Edg/91.0.864.70", "edge")
o:value("Go-http-client/1.1", "golang")
o:value("curl/7.68.0", "curl")
o:depends({ [_n("tcp_guise")] = "http" })
o:depends({ [_n("transport")] = "http" })
o:depends({ [_n("transport")] = "ws" })

View File

@ -356,33 +356,6 @@ o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_domain"), translate("Camouflage Domain"), translate("Use it together with the DNS disguised type. You can fill in any domain."))
o:depends({ [_n("mkcp_guise")] = "dns" })
o = s:option(Value, _n("mkcp_mtu"), translate("KCP MTU"))
o.default = "1350"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_tti"), translate("KCP TTI"))
o.default = "20"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_uplinkCapacity"), translate("KCP uplinkCapacity"))
o.default = "5"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_downlinkCapacity"), translate("KCP downlinkCapacity"))
o.default = "20"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Flag, _n("mkcp_congestion"), translate("KCP Congestion"))
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_readBufferSize"), translate("KCP readBufferSize"))
o.default = "1"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_writeBufferSize"), translate("KCP writeBufferSize"))
o.default = "1"
o:depends({ [_n("transport")] = "mkcp" })
o = s:option(Value, _n("mkcp_seed"), translate("KCP Seed"))
o:depends({ [_n("transport")] = "mkcp" })

View File

@ -217,13 +217,12 @@ function gen_outbound(flag, node, tag, proxy_table)
}
} or nil,
kcpSettings = (node.transport == "mkcp") and {
mtu = tonumber(node.mkcp_mtu),
tti = tonumber(node.mkcp_tti),
uplinkCapacity = tonumber(node.mkcp_uplinkCapacity),
downlinkCapacity = tonumber(node.mkcp_downlinkCapacity),
congestion = (node.mkcp_congestion == "1") and true or false,
readBufferSize = tonumber(node.mkcp_readBufferSize),
writeBufferSize = tonumber(node.mkcp_writeBufferSize)
mtu = 1350,
tti = 50,
uplinkCapacity = 12,
downlinkCapacity = 100,
CwndMultiplier = 1,
MaxSendingWindow = 2 * 1024 * 1024
} or nil,
wsSettings = (node.transport == "ws") and {
path = node.ws_path or "/",
@ -675,13 +674,12 @@ function gen_config_server(node)
}
} or nil,
kcpSettings = (node.transport == "mkcp") and {
mtu = tonumber(node.mkcp_mtu),
tti = tonumber(node.mkcp_tti),
uplinkCapacity = tonumber(node.mkcp_uplinkCapacity),
downlinkCapacity = tonumber(node.mkcp_downlinkCapacity),
congestion = (node.mkcp_congestion == "1") and true or false,
readBufferSize = tonumber(node.mkcp_readBufferSize),
writeBufferSize = tonumber(node.mkcp_writeBufferSize)
mtu = 1350,
tti = 50,
uplinkCapacity = 12,
downlinkCapacity = 100,
CwndMultiplier = 1,
MaxSendingWindow = 2 * 1024 * 1024
} or nil,
wsSettings = (node.transport == "ws") and {
host = node.ws_host or nil,

View File

@ -0,0 +1,149 @@
<%+cbi/valueheader%>
<%
-- Template Developers:
-- - lwb1978
-- - snowie2000
-- Copyright: copyright(c)20252027
-- 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%>

View File

@ -0,0 +1,654 @@
<%
-- Template Developers:
-- - lwb1978
-- - snowie2000
-- Copyright: copyright(c)20252027
-- 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>

View File

@ -1,11 +1,23 @@
#!/bin/sh
[[ "$ACTION" == "ifup" && $(uci get "passwall2.@global[0].enabled") == "1" ]] && [ -f /var/lock/passwall2_ready.lock ] && {
default_device=$(ip route | grep default | awk -F 'dev ' '{print $2}' | awk '{print $1}')
[ "$default_device" == "$DEVICE" ] && {
LOCK_FILE_DIR=/var/lock
[ ! -d ${LOCK_FILE_DIR} ] && mkdir -p ${LOCK_FILE_DIR}
LOCK_FILE="${LOCK_FILE_DIR}/passwall2_ifup.lock"
[ "$ACTION" = "ifup" ] || [ "$ACTION" = "ifupdate" ] || exit 0
. /usr/share/passwall2/utils.sh
([ "$(get_cache_var "ENABLED_DEFAULT_ACL")" = "1" ] || [ "$(get_cache_var "ENABLED_ACLS")" = "1" ]) && [ -f ${LOCK_PATH}/${CONFIG}_ready.lock ] && {
default_device=$(ip route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
default6_device=$(ip -6 route show default 2>/dev/null | awk -F 'dev ' '{print $2}' | awk '{print $1}' | head -n1)
( [ "$default_device" = "$DEVICE" ] || [ "$default6_device" = "$DEVICE" ] ) && {
if [ "$ACTION" = "ifupdate" ]; then
USE_TABLES=$(get_cache_var "USE_TABLES")
[ -n "$USE_TABLES" ] && ${APP_PATH}/${USE_TABLES}.sh update_wan_sets >/dev/null 2>&1 &
exit 0
fi
[ ! -d ${LOCK_PATH} ] && mkdir -p ${LOCK_PATH}
LOCK_FILE="${LOCK_PATH}/${CONFIG}_ifup.lock"
if [ -s ${LOCK_FILE} ]; then
SPID=$(cat ${LOCK_FILE})
if [ -e /proc/${SPID}/status ]; then
@ -15,8 +27,8 @@
fi
echo $$ > ${LOCK_FILE}
/etc/init.d/passwall2 restart >/dev/null 2>&1 &
logger -p notice -t network -s "passwall2: restart when $INTERFACE ifup"
/etc/init.d/${CONFIG} restart >/dev/null 2>&1 &
logger -p notice -t network -s "${CONFIG}: restart when $INTERFACE ifup"
rm -rf ${LOCK_FILE}
}

View File

@ -933,8 +933,8 @@ start_haproxy() {
[ "$(config_t_get global_haproxy balancing_enable 0)" != "1" ] && return
haproxy_path=$TMP_PATH/haproxy
haproxy_conf="config.cfg"
lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf} -dns ${LOCAL_DNS:-${AUTO_DNS}}
ln_run ${QUEUE_RUN} "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}"
lua $APP_PATH/haproxy.lua -path ${haproxy_path} -conf ${haproxy_conf}
ln_run 0 "$(first_type haproxy)" haproxy "/dev/null" -f "${haproxy_path}/${haproxy_conf}"
}
run_copy_dnsmasq() {

View File

@ -1,7 +1,7 @@
#!/usr/bin/lua
local api = require ("luci.passwall2.api")
local appname = "passwall2"
local appname = api.appname
local fs = api.fs
local jsonc = api.jsonc
local uci = api.uci
@ -18,11 +18,12 @@ end
local var = api.get_args(arg)
local haproxy_path = var["-path"]
local haproxy_conf = var["-conf"]
local haproxy_dns = var["-dns"] or "119.29.29.29:53,223.5.5.5:53"
local haproxy_dns = "127.0.0.1"
local cpu_thread = sys.exec('echo -n $(cat /proc/cpuinfo | grep "processor" | wc -l)') or "1"
local health_check_type = uci:get(appname, "@global_haproxy[0]", "health_check_type") or "tcp"
local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "10"
local health_check_inter = uci:get(appname, "@global_haproxy[0]", "health_check_inter") or "20"
local balancingStrategy = uci:get(appname, "@global_haproxy[0]", "balancingStrategy") or "roundrobin"
local console_port = uci:get(appname, "@global_haproxy[0]", "console_port")
local bind_local = uci:get(appname, "@global_haproxy[0]", "bind_local") or "0"
local bind_address = "0.0.0.0"
@ -38,35 +39,37 @@ local f_out = io.open(haproxy_file, "a")
local haproxy_config = [[
global
daemon
log 127.0.0.1 local2
maxconn 60000
stats socket {{path}}/haproxy.sock
log 127.0.0.1 local2
maxconn 60000
stats socket {{path}}/haproxy.sock
nbthread {{nbthread}}
external-check
insecure-fork-wanted
spread-checks 50
tune.max-checks-per-thread 5
defaults
mode tcp
log global
option tcplog
option dontlognull
mode tcp
log global
option tcplog
option dontlognull
option http-server-close
#option forwardfor except 127.0.0.0/8
option redispatch
retries 2
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
#option forwardfor except 127.0.0.0/8
option redispatch
retries 2
timeout http-request 10s
timeout queue 1m
timeout connect 10s
timeout client 1m
timeout server 1m
timeout http-keep-alive 10s
timeout check 10s
maxconn 3000
timeout check 10s
maxconn 3000
resolvers mydns
resolve_retries 1
timeout resolve 5s
hold valid 600s
resolve_retries 1
timeout resolve 5s
hold valid 600s
{{dns}}
]]
@ -97,10 +100,12 @@ uci:foreach(appname, "haproxy_config", function(t)
local lbss = t.lbss
local listen_port = tonumber(t.haproxy_port) or 0
local server_node = uci:get_all(appname, lbss)
if server_node and server_node.address and server_node.port then
server_remark = server_node.address .. ":" .. server_node.port
local hop = (health_check_type == "passwall_logic") and (server_node.hysteria_hop or server_node.hysteria2_hop) or nil
hop = hop and hop:gsub(":", "-") or nil
if server_node and server_node.address and (server_node.port or hop) then
server_remark = server_node.address .. ":" .. (server_node.port or hop)
server_address = server_node.address
server_port = server_node.port
server_port = server_node.port or hop
t.origin_address = server_address
t.origin_port = server_port
if health_check_type == "passwall_logic" then
@ -143,6 +148,10 @@ uci:foreach(appname, "haproxy_config", function(t)
end
end)
local inter = tonumber((health_check_inter or "20"):match("^%d+"))
inter = (inter and inter >= 20) and inter or 20
local inter_arg = "inter " .. inter .. "s fastinter " .. inter / 2 .. "s downinter " .. inter * 1.5 .. "s"
local sortTable = {}
for i in pairs(listens) do
if i ~= nil then
@ -158,14 +167,14 @@ for i, port in pairs(sortTable) do
listen %s
bind %s:%s
mode tcp
balance roundrobin
]], port, bind_address, port))
balance %s
]], port, bind_address, port, balancingStrategy))
if health_check_type == "passwall_logic" then
f_out:write(string.format([[
option external-check
external-check command "/usr/share/passwall2/haproxy_check.sh"
]], port, port))
external-check command "/usr/share/%s/haproxy_check.sh"
]], appname))
end
local count_M, count_B = 1, 1
@ -180,7 +189,7 @@ listen %s
count_B = count_B + 1
end
local server = o.server_address .. ":" .. o.server_port
local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check inter {{inter}} rise 1 fall 3 {{backup}}"
local server_conf = "server {{remark}} {{server}} weight {{weight}} {{resolvers}} check {{inter_arg}} rise 2 fall 2 {{backup}}"
server_conf = server_conf:gsub("{{remark}}", remark)
server_conf = server_conf:gsub("{{server}}", server)
server_conf = server_conf:gsub("{{weight}}", o.lbweight)
@ -189,13 +198,13 @@ listen %s
resolvers = ""
end
server_conf = server_conf:gsub("{{resolvers}}", resolvers)
server_conf = server_conf:gsub("{{inter}}", tonumber(health_check_inter) .. "s")
server_conf = server_conf:gsub("{{inter_arg}}", inter_arg)
server_conf = server_conf:gsub("{{backup}}", tostring(o.backup) == "1" and "backup" or "")
f_out:write(" " .. server_conf .. "\n")
f_out:write(" " .. api.trim(server_conf) .. "\n")
if o.export ~= "0" then
sys.call(string.format(". /usr/share/passwall2/utils.sh ; add_ip2route %s %s", o.origin_address, o.export))
sys.call(string.format(". /usr/share/%s/utils.sh ; add_ip2route %s %s", appname, o.origin_address, o.export))
end
log(2, string.format(api.i18n.translatef("Node: %s:%s, Weight: %s", o.origin_address, o.origin_port, o.lbweight)))

View File

@ -24,7 +24,7 @@ if /usr/bin/curl --help all | grep -q "\-\-retry-all-errors"; then
extra_params="${extra_params} --retry-all-errors"
fi
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 2 --max-time 10 -w "%{http_code}" "${probeUrl}")
status=$(/usr/bin/curl -I -o /dev/null -skL ${extra_params} --connect-timeout 3 --retry 1 --max-time 6 -w "%{http_code}" "${probeUrl}")
case "$status" in
200|204)

View File

@ -617,6 +617,24 @@ filter_direct_node_list() {
done
}
update_wan_sets() {
local WAN_IP=$(get_wan_ips ip4)
[ -n "$WAN_IP" ] && {
ipset -F "$IPSET_WAN"
for wan_ip in $WAN_IP; do
ipset -! add "$IPSET_WAN" "$wan_ip"
done
}
local WAN6_IP=$(get_wan_ips ip6)
[ -n "$WAN6_IP" ] && {
ipset -F "$IPSET_WAN6"
for wan6_ip in $WAN6_IP; do
ipset -! add "$IPSET_WAN6" "$wan6_ip"
done
}
}
add_firewall_rule() {
log_i18n 0 "Starting to load %s firewall rules..." "iptables"
@ -665,6 +683,8 @@ add_firewall_rule() {
EOF
}
update_wan_sets
[ -n "$ISP_DNS" ] && {
for ispip in $ISP_DNS; do
ipset -! add $IPSET_LAN $ispip
@ -716,6 +736,7 @@ add_firewall_rule() {
$ipt_n -N PSW2
$ipt_n -A PSW2 $(dst $IPSET_LAN) -j RETURN
$ipt_n -A PSW2 $(dst $IPSET_VPS) -j RETURN
$ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
[ "$accept_icmp" = "1" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p icmp -j PSW2"
[ -z "${is_tproxy}" ] && insert_rule_after "$ipt_n" "PREROUTING" "prerouting_rule" "-p tcp -j PSW2"
@ -747,19 +768,9 @@ add_firewall_rule() {
$ipt_m -N PSW2
$ipt_m -A PSW2 $(dst $IPSET_LAN) -j RETURN
$ipt_m -A PSW2 $(dst $IPSET_VPS) -j RETURN
$ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
$ipt_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN
WAN_IP=$(get_wan_ips ip4)
[ -n "${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in $WAN_IP; do
ipset -! add $IPSET_WAN ${wan_ip}
done
$ipt_n -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
$ipt_m -A PSW2 $(comment "WAN_IP_RETURN") $(dst $IPSET_WAN) -j RETURN
}
unset WAN_IP wan_ip
insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2"
# Only TCP, UDP Invalid.
insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT"
@ -807,17 +818,8 @@ add_firewall_rule() {
$ip6t_m -N PSW2
$ip6t_m -A PSW2 $(dst $IPSET_LAN6) -j RETURN
$ip6t_m -A PSW2 $(dst $IPSET_VPS6) -j RETURN
$ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN
$ip6t_m -A PSW2 -m conntrack --ctdir REPLY -j RETURN
WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in $WAN6_IP; do
ipset -! add $IPSET_WAN6 ${wan6_ip}
done
$ip6t_m -A PSW2 $(comment "WAN6_IP_RETURN") $(dst $IPSET_WAN6) -j RETURN
}
unset WAN6_IP wan6_ip
insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2"
# Only TCP, UDP Invalid.
@ -835,7 +837,7 @@ add_firewall_rule() {
local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}')
if [[ "$dns_address" == *::* ]]; then
$ip6t_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "${dns_address}:${dns_port:-53}")"
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "ip6tables" "[${dns_address}]:${dns_port:-53}")"
else
$ipt_m -I PSW2_OUTPUT -p udp -d ${dns_address} --dport ${dns_port:-53} -j RETURN
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "iptables" "${dns_address}:${dns_port:-53}")"
@ -1050,7 +1052,7 @@ gen_include() {
local __ipt=""
[ -n "${ipt}" ] && {
__ipt=$(cat <<- EOF
. $UTILS_PATH
${MY_PATH} update_wan_sets
$ipt-save -c | grep -v "PSW2" | $ipt-restore -c
$ipt-restore -n <<-EOT
$(extract_rules 4 nat)
@ -1062,21 +1064,13 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "mwan3" "-j PSW2")
\$(${MY_PATH} insert_rule_before "$ipt_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT")
WAN_IP=\$(get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
ipset -F $IPSET_WAN
for wan_ip in \$WAN_IP; do
ipset -! add $IPSET_WAN \${wan_ip}
done
}
EOF
)
}
local __ip6t=""
[ -n "${ip6t}" ] && {
__ip6t=$(cat <<- EOF
. $UTILS_PATH
${MY_PATH} update_wan_sets
$ip6t-save -c | grep -v "PSW2" | $ip6t-restore -c
$ip6t-restore -n <<-EOT
$(extract_rules 6 nat)
@ -1087,20 +1081,12 @@ gen_include() {
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "mwan3" "-j PSW2")
\$(${MY_PATH} insert_rule_before "$ip6t_m" "PREROUTING" "PSW2" "-p tcp -m socket -j PSW2_DIVERT")
WAN6_IP=\$(get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
ipset -F $IPSET_WAN6
for wan6_ip in \$WAN6_IP; do
ipset -! add $IPSET_WAN6 \${wan6_ip}
done
}
EOF
)
}
cat <<-EOF >> $FWI
${__ipt}
${__ip6t}
return 0
@ -1158,6 +1144,9 @@ get_ip6t_bin)
filter_direct_node_list)
filter_direct_node_list
;;
update_wan_sets)
update_wan_sets "$@"
;;
stop)
stop
;;

View File

@ -680,6 +680,20 @@ mwan3_start() {
nft list chain ip mangle mwan3_hook >/dev/null 2>&1 && nft insert rule ip mangle mwan3_hook ct mark ${FWMARK} counter return >/dev/null 2>&1
}
update_wan_sets() {
local WAN_IP=$(get_wan_ips ip4)
[ -n "$WAN_IP" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
echo "$WAN_IP" | insert_nftset $NFTSET_WAN "-1"
}
local WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
echo "$WAN6_IP" | insert_nftset $NFTSET_WAN6 "-1"
}
}
add_firewall_rule() {
log_i18n 0 "Starting to load %s firewall rules..." "nftables"
gen_nft_tables
@ -714,6 +728,8 @@ add_firewall_rule() {
[ -n "$lan_ip6" ] && echo $lan_ip6 | insert_nftset $NFTSET_LAN6 "-1"
}
update_wan_sets
[ -n "$ISP_DNS" ] && {
echo "$ISP_DNS" | insert_nftset $NFTSET_LAN 0
for ispip in $ISP_DNS; do
@ -835,14 +851,9 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME nat_output meta l4proto {icmp,icmpv6} counter jump PSW2_ICMP_REDIRECT"
fi
WAN_IP=$(get_wan_ips ip4)
[ -n "${WAN_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
echo $WAN_IP | insert_nftset $NFTSET_WAN "-1"
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
}
unset WAN_IP
#ipv4 wan_ip
[ -z "${is_tproxy}" ] && nft "add rule $NFTABLE_NAME PSW2_NAT ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
nft "add rule $NFTABLE_NAME PSW2_MANGLE ip daddr @$NFTSET_WAN counter return comment \"WAN_IP_RETURN\""
ip rule add fwmark ${FWMARK} table 999 priority 999
ip route add local 0.0.0.0/0 dev lo table 999
@ -867,10 +878,11 @@ add_firewall_rule() {
local dns_port=$(echo $auto_dns | awk -F '#' '{print $2}')
if [[ "$dns_address" == *::* ]]; then
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp ip6 daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return"
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "[${dns_address}]:${dns_port:-53}")"
else
nft "insert rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp ip daddr ${dns_address} $(factor ${dns_port:-53} "udp dport") counter return"
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")"
fi
log_i18n 1 "$(i18n "Add direct DNS to %s: %s" "nftables" "${dns_address}:${dns_port:-53}")"
done
}
@ -879,13 +891,7 @@ add_firewall_rule() {
nft "add rule $NFTABLE_NAME mangle_prerouting meta nfproto {ipv6} counter jump PSW2_MANGLE_V6"
nft "add rule $NFTABLE_NAME mangle_output meta nfproto {ipv6} counter jump PSW2_OUTPUT_MANGLE_V6 comment \"PSW2_OUTPUT_MANGLE\""
WAN6_IP=$(get_wan_ips ip6)
[ -n "${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
echo $WAN6_IP | insert_nftset $NFTSET_WAN6 "-1"
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
}
unset WAN6_IP
nft "add rule $NFTABLE_NAME PSW2_MANGLE_V6 ip6 daddr @$NFTSET_WAN6 counter return comment \"WAN6_IP_RETURN\""
ip -6 rule add fwmark ${FWMARK} table 999 priority 999
ip -6 route add local ::/0 dev lo table 999
@ -894,7 +900,7 @@ add_firewall_rule() {
[ "$ENABLED_DEFAULT_ACL" == 1 ] && {
TCP_LOCALHOST_PROXY=$LOCALHOST_PROXY
UDP_LOCALHOST_PROXY=$LOCALHOST_PROXY
msg="$(i18n "[Local],")"
[ "$TCP_NO_REDIR_PORTS" != "disable" ] && {
nft "add rule $NFTABLE_NAME $nft_output_chain ip protocol tcp $(factor $TCP_NO_REDIR_PORTS "tcp dport") counter return"
@ -906,7 +912,7 @@ add_firewall_rule() {
log 1 "${msg}$(i18n "not proxy all %s" "TCP")"
fi
}
[ "$UDP_NO_REDIR_PORTS" != "disable" ] && {
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE ip protocol udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return"
nft "add rule $NFTABLE_NAME PSW2_OUTPUT_MANGLE_V6 meta l4proto udp $(factor $UDP_NO_REDIR_PORTS "udp dport") counter return"
@ -1088,20 +1094,9 @@ gen_include() {
local __nft=" "
__nft=$(cat <<- EOF
. $UTILS_PATH
[ -z "\$(nft list chain $NFTABLE_NAME mangle_prerouting | grep PSW2)" ] && nft -f ${nft_chain_file}
WAN_IP=\$(get_wan_ips ip4)
[ ! -z "\${WAN_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN
echo "\${WAN_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN "-1"
}
[ "$PROXY_IPV6" == "1" ] && {
WAN6_IP=\$(get_wan_ips ip6)
[ ! -z "\${WAN6_IP}" ] && {
nft flush set $NFTABLE_NAME $NFTSET_WAN6
echo "\${WAN6_IP}" | sh ${MY_PATH} insert_nftset $NFTSET_WAN6 "-1"
}
}
${MY_PATH} update_wan_sets
EOF
)
@ -1150,6 +1145,9 @@ mwan3_start)
mwan3_stop)
mwan3_stop
;;
update_wan_sets)
update_wan_sets "$@"
;;
stop)
stop
;;

View File

@ -31,59 +31,60 @@ local has_ssr = api.is_finded("ssr-local") and api.is_finded("ssr-redir")
local has_singbox = api.finded_com("sing-box")
local has_xray = api.finded_com("xray")
local has_hysteria2 = api.finded_com("hysteria")
local allowInsecure_default = true
local DEFAULT_ALLOWINSECURE = true
local DEFAULT_FILTER_KEYWORD_MODE = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
local DEFAULT_FILTER_KEYWORD_DISCARD_LIST = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
local DEFAULT_FILTER_KEYWORD_KEEP_LIST = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
-- Nodes should be retrieved using the core type (if not set on the node subscription page, the default type will be used automatically).
local ss_type_default = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local trojan_type_default = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
local vmess_type_default = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local vless_type_default = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local hysteria2_type_default = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_SS_TYPE = api.get_core("ss_type", {{has_ss,"shadowsocks-libev"},{has_ss_rust,"shadowsocks-rust"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_xray,"xray"}})
local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}})
local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}})
local core_has = {
["xray"] = has_xray, ["sing-box"] = has_singbox, ["shadowsocks-libev"] = has_ss,["shadowsocks-rust"] = has_ss_rust,
["xray"] = has_xray,
["sing-box"] = has_singbox,
["shadowsocks-libev"] = has_ss,
["shadowsocks-rust"] = has_ss_rust,
["hysteria2"] = has_hysteria2
}
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
-- Determine whether to filter node keywords
local filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
local filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
local filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
local function is_filter_keyword(value)
if filter_keyword_mode_default == "1" then
for k,v in ipairs(filter_keyword_discard_list_default) do
local function is_filter_keyword(mode, discard_list, keep_list, value)
if mode == "1" then
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
return true
end
end
elseif filter_keyword_mode_default == "2" then
elseif mode == "2" then
local result = true
for k,v in ipairs(filter_keyword_keep_list_default) do
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif filter_keyword_mode_default == "3" then
elseif mode == "3" then
local result = false
for k,v in ipairs(filter_keyword_discard_list_default) do
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
end
for k,v in ipairs(filter_keyword_keep_list_default) do
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
return result
elseif filter_keyword_mode_default == "4" then
elseif mode == "4" then
local result = true
for k,v in ipairs(filter_keyword_keep_list_default) do
for k,v in ipairs(keep_list) do
if value:find(v, 1, true) then
result = false
end
end
for k,v in ipairs(filter_keyword_discard_list_default) do
for k,v in ipairs(discard_list) do
if value:find(v, 1, true) then
result = true
end
@ -425,16 +426,16 @@ local function get_subscribe_info(cfgid, value)
end
-- Configure the SS protocol implementation type
local function set_ss_implementation(result)
if ss_type_default == "shadowsocks-libev" and has_ss then
local function set_ss_implementation(ss_type, result)
if ss_type == "shadowsocks-libev" and has_ss then
result.type = "SS"
elseif ss_type_default == "shadowsocks-rust" and has_ss_rust then
elseif ss_type == "shadowsocks-rust" and has_ss_rust then
result.type = 'SS-Rust'
elseif ss_type_default == "xray" and has_xray then
elseif ss_type == "xray" and has_xray then
result.type = 'Xray'
result.protocol = 'shadowsocks'
result.transport = 'raw'
elseif ss_type_default == "sing-box" and has_singbox then
elseif ss_type == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = 'shadowsocks'
else
@ -445,8 +446,60 @@ local function set_ss_implementation(result)
end
-- Processing data
local function processData(szType, content, add_mode, group)
local function processData(szType, content, add_mode, group, sub_cfg)
--log(2, content, add_mode, group)
local default_allowinsecure = DEFAULT_ALLOWINSECURE
local default_filter_keyword_mode = DEFAULT_FILTER_KEYWORD_MODE
local default_filter_keyword_discard_list = DEFAULT_FILTER_KEYWORD_DISCARD_LIST
local default_filter_keyword_keep_list = DEFAULT_FILTER_KEYWORD_KEEP_LIST
local default_ss_type = DEFAULT_SS_TYPE
local default_trojan_type = DEFAULT_TROJAN_TYPE
local default_vmess_type = DEFAULT_VMESS_TYPE
local default_vless_type = DEFAULT_VLESS_TYPE
local default_hysteria2_type = DEFAULT_HYSTERIA2_TYPE
if sub_cfg then
if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then
default_allowinsecure = nil
end
local filter_keyword_mode = sub_cfg.filter_keyword_mode or "5" -- 5 is global
if filter_keyword_mode == "0" then
default_filter_keyword_mode = "0"
elseif filter_keyword_mode == "1" then
default_filter_keyword_mode = "1"
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "2" then
default_filter_keyword_mode = "2"
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
elseif filter_keyword_mode == "3" then
default_filter_keyword_mode = "3"
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
elseif filter_keyword_mode == "4" then
default_filter_keyword_mode = "4"
default_filter_keyword_keep_list = sub_cfg.filter_keep_list or {}
default_filter_keyword_discard_list = sub_cfg.filter_discard_list or {}
end
local ss_type = sub_cfg.ss_type or "global"
if ss_type ~= "global" and core_has[ss_type] then
default_ss_type = ss_type
end
local trojan_type = sub_cfg.trojan_type or "global"
if trojan_type ~= "global" and core_has[trojan_type] then
default_trojan_type = trojan_type
end
local vmess_type = sub_cfg.vmess_type or "global"
if vmess_type ~= "global" and core_has[vmess_type] then
default_vmess_type = vmess_type
end
local vless_type = sub_cfg.vless_type or "global"
if vless_type ~= "global" and core_has[vless_type] then
default_vless_type = vless_type
end
local hysteria2_type = sub_cfg.hysteria2_type or "global"
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
default_hysteria2_type = hysteria2_type
end
end
local result = {
timeout = 60,
add_mode = add_mode, -- `0` for manual configuration, `1` for import, `2` for subscription
@ -486,9 +539,9 @@ local function processData(szType, content, add_mode, group)
result.remarks = base64Decode(params.remarks)
elseif szType == 'vmess' then
local info = jsonParse(content)
if vmess_type_default == "sing-box" and has_singbox then
if default_vmess_type == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif vmess_type_default == "xray" and has_xray then
elseif default_vmess_type == "xray" and has_xray then
result.type = "Xray"
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VMess", "VMess"))
@ -559,12 +612,7 @@ local function processData(szType, content, add_mode, group)
if info.net == 'kcp' or info.net == 'mkcp' then
info.net = "mkcp"
result.mkcp_guise = info.type
result.mkcp_mtu = 1350
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
result.mkcp_seed = info.seed
end
if info.net == 'quic' then
result.quic_guise = info.type
@ -599,7 +647,7 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = info.pcs
result.tls_CertByName = info.vcn
local insecure = info.allowinsecure or info.allowInsecure or info.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
else
result.tls = "0"
end
@ -615,7 +663,7 @@ local function processData(szType, content, add_mode, group)
return nil
end
elseif szType == "ss" then
result = set_ss_implementation(result)
result = set_ss_implementation(default_ss_type, result)
if not result then return nil end
--SS-URI = "ss://" userinfo "@" hostname ":" port [ "/" ] [ "?" plugin ] [ "#" tag ]
@ -825,12 +873,6 @@ local function processData(szType, content, add_mode, group)
if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none"
result.mkcp_mtu = 1350
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
result.mkcp_seed = params.seed
end
if params.type == 'quic' then
@ -884,7 +926,7 @@ local function processData(szType, content, add_mode, group)
end
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
result.uot = params.udp
elseif (params.type ~= "tcp" and params.type ~= "raw") and (params.headerType and params.headerType ~= "none") then
result.error_msg = i18n.translatef("Please replace Xray or Sing-Box to support more transmission methods in Shadowsocks.")
@ -893,7 +935,7 @@ local function processData(szType, content, add_mode, group)
if params["shadow-tls"] then
if result.type ~= "sing-box" and result.type ~= "SS-Rust" then
result.error_msg = ss_type_default .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls")
result.error_msg = default_ss_type .. " " .. i18n.translatef("unsupport %s plugin.", "shadow-tls")
else
-- Parsing SS Shadow-TLS plugin parameters
local function parseShadowTLSParams(b64str, out)
@ -935,10 +977,10 @@ local function processData(szType, content, add_mode, group)
end
end
elseif szType == "trojan" then
if trojan_type_default == "sing-box" and has_singbox then
if default_trojan_type == "sing-box" and has_singbox then
result.type = 'sing-box'
result.protocol = 'trojan'
elseif trojan_type_default == "xray" and has_xray then
elseif default_trojan_type == "xray" and has_xray then
result.type = 'Xray'
result.protocol = 'trojan'
else
@ -988,7 +1030,7 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = params.pcs
result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
if not params.type then params.type = "tcp" end
params.type = string.lower(params.type)
@ -1042,12 +1084,6 @@ local function processData(szType, content, add_mode, group)
if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none"
result.mkcp_mtu = 1350
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
result.mkcp_seed = params.seed
end
if params.type == 'quic' then
@ -1079,7 +1115,7 @@ local function processData(szType, content, add_mode, group)
end
end
elseif szType == "ssd" then
result = set_ss_implementation(result)
result = set_ss_implementation(default_ss_type, result)
if not result then return nil end
result.address = content.server
result.port = content.port
@ -1090,9 +1126,9 @@ local function processData(szType, content, add_mode, group)
result.group = content.airport
result.remarks = content.remarks
elseif szType == "vless" then
if vless_type_default == "sing-box" and has_singbox then
if default_vless_type == "sing-box" and has_singbox then
result.type = 'sing-box'
elseif vless_type_default == "xray" and has_xray then
elseif default_vless_type == "xray" and has_xray then
result.type = "Xray"
else
log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "VLESS", "VLESS"))
@ -1189,12 +1225,7 @@ local function processData(szType, content, add_mode, group)
if params.type == 'kcp' or params.type == 'mkcp' then
result.transport = "mkcp"
result.mkcp_guise = params.headerType or "none"
result.mkcp_mtu = 1350
result.mkcp_tti = 50
result.mkcp_uplinkCapacity = 5
result.mkcp_downlinkCapacity = 20
result.mkcp_readBufferSize = 2
result.mkcp_writeBufferSize = 2
result.mkcp_seed = params.seed
end
if params.type == 'quic' then
result.quic_guise = params.headerType or "none"
@ -1250,7 +1281,7 @@ local function processData(szType, content, add_mode, group)
result.reality_mldsa65Verify = params.pqv or nil
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
end
result.port = port
@ -1307,7 +1338,7 @@ local function processData(szType, content, add_mode, group)
result.hysteria_auth_password = params.auth
result.tls_serverName = params.peer or params.sni or ""
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
result.alpn = params.alpn
result.hysteria_up_mbps = params.upmbps
result.hysteria_down_mbps = params.downmbps
@ -1353,12 +1384,12 @@ local function processData(szType, content, add_mode, group)
result.tls_CertSha = params.pcs
result.tls_CertByName = params.vcn
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
result.hysteria2_tls_pinSHA256 = params.pinSHA256
result.hysteria2_hop = params.mport
if (hysteria2_type_default == "sing-box" and has_singbox) or (hysteria2_type_default == "xray" and has_xray) then
local is_singbox = hysteria2_type_default == "sing-box" and has_singbox
if (default_hysteria2_type == "sing-box" and has_singbox) or (default_hysteria2_type == "xray" and has_xray) then
local is_singbox = default_hysteria2_type == "sing-box" and has_singbox
result.type = is_singbox and 'sing-box' or 'Xray'
result.protocol = "hysteria2"
if params["obfs-password"] or params["obfs_password"] then
@ -1433,7 +1464,7 @@ local function processData(szType, content, add_mode, group)
result.tuic_congestion_control = params.congestion_control or "cubic"
result.tuic_udp_relay_mode = params.udp_relay_mode or "native"
local insecure = params.allowinsecure or params.insecure or params.allow_insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
elseif szType == "anytls" then
if has_singbox then
result.type = 'sing-box'
@ -1497,7 +1528,7 @@ local function processData(szType, content, add_mode, group)
end
result.port = port
local insecure = params.allowinsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (allowInsecure_default and "1" or "0")
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (default_allowinsecure and "1" or "0")
end
elseif szType == 'naive+https' or szType == 'naive+quic' then
if has_singbox then
@ -1840,6 +1871,29 @@ local function update_node(manual)
for _, v in ipairs(nodeResult) do
local remark = v["remark"]
local list = v["list"]
local sub_cfg = v["sub_cfg"]
local domain_resolver, domain_resolver_dns, domain_resolver_dns_https, domain_strategy
local preproxy_node_group, to_node_group, chain_node_type = "", "", ""
-- Subscription Group Chain Agent
local function valid_chain_node(node)
if not node then return "" end
local cp = uci:get(appname, node, "chain_proxy") or ""
local am = uci:get(appname, node, "add_mode") or "0"
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
chain_node_type = ""
return ""
end
return node
end
if sub_cfg then
domain_resolver = sub_cfg.domain_resolver
domain_resolver_dns = sub_cfg.domain_resolver_dns
domain_resolver_dns_https = sub_cfg.domain_resolver_dns_https
domain_strategy = (sub_cfg.domain_strategy == "UseIPv4" or sub_cfg.domain_strategy == "UseIPv6") and sub_cfg.domain_strategy or nil
preproxy_node_group = (sub_cfg.chain_proxy == "1") and valid_chain_node(sub_cfg.preproxy_node) or ""
to_node_group = (sub_cfg.chain_proxy == "2") and valid_chain_node(sub_cfg.to_node) or ""
end
for _, vv in ipairs(list) do
local cfgid = uci:section(appname, "nodes", api.gen_short_uuid())
for kkk, vvv in pairs(vv) do
@ -1929,8 +1983,12 @@ local function update_node(manual)
end
end
local function parse_link(raw, add_mode, group, cfgid)
local function parse_link(raw, add_mode, group, sub_cfg)
if raw and #raw > 0 then
local cfgid
if sub_cfg then
cfgid = sub_cfg[".name"]
end
local nodes, szType
local node_list = {}
-- ssd appear to be in this format, starting with ssd://.
@ -1965,17 +2023,17 @@ local function parse_link(raw, add_mode, group, cfgid)
xpcall(function ()
local result
if szType == 'ssd' then
result = processData(szType, v, add_mode, group)
result = processData(szType, v, add_mode, group, sub_cfg)
elseif not szType then
local node = api.trim(v)
local dat = split(node, "://")
if dat and dat[1] and dat[2] then
if dat[1] == 'vmess' or dat[1] == 'ssr' then
local link = api.trim(dat[2]:gsub("#.*$", ""))
result = processData(dat[1], base64Decode(link), add_mode, group)
result = processData(dat[1], base64Decode(link), add_mode, group, sub_cfg)
else
local link = dat[2]:gsub("&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
else
@ -1987,7 +2045,7 @@ local function parse_link(raw, add_mode, group, cfgid)
log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. result.error_msg)
elseif not result.type then
log(2, i18n.translatef("Discard node: %s, Reason:", result.remarks) .. " " .. i18n.translatef("No usable binary was found."))
elseif (add_mode == "2" and is_filter_keyword(result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
elseif (add_mode == "2" and is_filter_keyword(default_filter_keyword_mode, default_filter_keyword_discard_list, default_filter_keyword_keep_list, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
(not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then
log(2, i18n.translatef("Discard filter nodes: %s type node %s", result.type, result.remarks))
else
@ -2007,7 +2065,8 @@ local function parse_link(raw, add_mode, group, cfgid)
if #node_list > 0 then
nodeResult[#nodeResult + 1] = {
remark = group,
list = node_list
list = node_list,
sub_cfg = sub_cfg
}
end
log(2, i18n.translatef("Successfully resolved the [%s] node, number: %s", group, #node_list))
@ -2038,77 +2097,26 @@ local execute = function()
local cfgid = value[".name"]
local remark = value.remark or ""
local url = value.url or ""
if value.allowInsecure and value.allowInsecure ~= "1" then
allowInsecure_default = nil
end
local filter_keyword_mode = value.filter_keyword_mode or "5"
if filter_keyword_mode == "0" then
filter_keyword_mode_default = "0"
elseif filter_keyword_mode == "1" then
filter_keyword_mode_default = "1"
filter_keyword_discard_list_default = value.filter_discard_list or {}
elseif filter_keyword_mode == "2" then
filter_keyword_mode_default = "2"
filter_keyword_keep_list_default = value.filter_keep_list or {}
elseif filter_keyword_mode == "3" then
filter_keyword_mode_default = "3"
filter_keyword_keep_list_default = value.filter_keep_list or {}
filter_keyword_discard_list_default = value.filter_discard_list or {}
elseif filter_keyword_mode == "4" then
filter_keyword_mode_default = "4"
filter_keyword_keep_list_default = value.filter_keep_list or {}
filter_keyword_discard_list_default = value.filter_discard_list or {}
end
local ss_type = value.ss_type or "global"
if ss_type ~= "global" and core_has[ss_type] then
ss_type_default = ss_type
end
local trojan_type = value.trojan_type or "global"
if trojan_type ~= "global" and core_has[trojan_type] then
trojan_type_default = trojan_type
end
local vmess_type = value.vmess_type or "global"
if vmess_type ~= "global" and core_has[vmess_type] then
vmess_type_default = vmess_type
end
local vless_type = value.vless_type or "global"
if vless_type ~= "global" and core_has[vless_type] then
vless_type_default = vless_type
end
local hysteria2_type = value.hysteria2_type or "global"
if hysteria2_type ~= "global" and core_has[hysteria2_type] then
hysteria2_type_default = hysteria2_type
end
domain_resolver = value.domain_resolver
domain_resolver_dns = value.domain_resolver_dns
domain_resolver_dns_https = value.domain_resolver_dns_https
domain_strategy = (value.domain_strategy == "UseIPv4" or value.domain_strategy == "UseIPv6") and value.domain_strategy or nil
-- Subscription Group Chain Agent
local function valid_chain_node(node)
if not node then return "" end
local cp = uci:get(appname, node, "chain_proxy") or ""
local am = uci:get(appname, node, "add_mode") or "0"
chain_node_type = (cp == "" and am ~= "2") and (uci:get(appname, node, "type") or "") or ""
if chain_node_type ~= "Xray" and chain_node_type ~= "sing-box" then
chain_node_type = ""
return ""
end
return node
end
preproxy_node_group = (value.chain_proxy == "1") and valid_chain_node(value.preproxy_node) or ""
to_node_group = (value.chain_proxy == "2") and valid_chain_node(value.to_node) or ""
local ua = value.user_agent
local access_mode = value.access_mode
local result = (not access_mode) and i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto")))
log(1, i18n.translatef("Start subscribing: %s", '' .. remark .. '' .. url .. ' [' .. result .. ']'))
local tmp_file = "/tmp/" .. cfgid
value.http_code = curl(url, tmp_file, ua, access_mode)
if value.http_code ~= 200 then
fail_list[#fail_list + 1] = value
local url_is_local
if fs.access(url) then
-- debug, reads local files.
log(1, i18n.translatef("Start subscribing: %s", '' .. remark .. '' .. url))
url_is_local = true
tmp_file = url
else
local ua = value.user_agent
local access_mode = value.access_mode
local result = (not access_mode) and i18n.translatef("Auto") or (access_mode == "direct" and i18n.translatef("Direct") or (access_mode == "proxy" and i18n.translatef("Proxy") or i18n.translatef("Auto")))
log(1, i18n.translatef("Start subscribing: %s", '' .. remark .. '' .. url .. ' [' .. result .. ']'))
tmp_file = "/tmp/" .. cfgid
value.http_code = curl(url, tmp_file, ua, access_mode)
if value.http_code ~= 200 then
fail_list[#fail_list + 1] = value
luci.sys.call("rm -f " .. tmp_file)
end
end
if fs.access(tmp_file) then
if luci.sys.call("[ -f " .. tmp_file .. " ] && sed -i -e '/^[ \t]*$/d' -e '/^[ \t]*\r$/d' " .. tmp_file) == 0 then
local f = io.open(tmp_file, "r")
local stdout = f:read("*all")
@ -2119,33 +2127,18 @@ local execute = function()
if not manual_sub and old_md5 == new_md5 then
log(1, i18n.translatef("Subscription: [%s] No changes, no update required.", remark))
else
parse_link(raw_data, "2", remark, cfgid)
parse_link(raw_data, "2", remark, value)
uci:set(appname, cfgid, "md5", new_md5)
end
else
fail_list[#fail_list + 1] = value
end
if url_is_local then
value.http_code = 0
else
luci.sys.call("rm -f " .. tmp_file)
end
end
allowInsecure_default = true
luci.sys.call("rm -f " .. tmp_file)
filter_keyword_mode_default = uci:get(appname, "@global_subscribe[0]", "filter_keyword_mode") or "0"
filter_keyword_discard_list_default = uci:get(appname, "@global_subscribe[0]", "filter_discard_list") or {}
filter_keyword_keep_list_default = uci:get(appname, "@global_subscribe[0]", "filter_keep_list") or {}
ss_type = uci:get(appname, "@global_subscribe[0]", "ss_type") or ""
ss_type_default = core_has[ss_type] and ss_type or ss_type_default
trojan_type = uci:get(appname, "@global_subscribe[0]", "trojan_type") or ""
trojan_type_default = core_has[trojan_type] and trojan_type or trojan_type_default
vmess_type = uci:get(appname, "@global_subscribe[0]", "vmess_type") or ""
vmess_type_default = core_has[vmess_type] and vmess_type or vmess_type_default
vless_type = uci:get(appname, "@global_subscribe[0]", "vless_type") or ""
vless_type_default = core_has[vless_type] and vless_type or vless_type_default
hysteria2_type = uci:get(appname, "@global_subscribe[0]", "hysteria2_type") or ""
hysteria2_type_default = core_has[hysteria2_type] and hysteria2_type or hysteria2_type_default
end
if #fail_list > 0 then