XHTTP Panel Fix
This commit is contained in:
@@ -121,7 +121,8 @@ Object.assign(I18N_TEXT["en-US"], {
|
||||
"Fleet control":"Fleet control","Fleet control copy":"Add nodes, test credentials, and configure remote infrastructure.","Live fleet":"Live fleet","Server status copy":"Health, load, and active sessions for every managed node.","Observability":"Observability","Monitoring copy":"Server resources, interfaces, and capacity in real time.",
|
||||
"Traffic intelligence":"Traffic intelligence","Network traffic":"Network traffic","Network traffic copy":"Daily and monthly history for understanding infrastructure usage.","Diagnostics":"Diagnostics","System logs copy":"Investigate the panel, DNSTT, and Xray in a focused view.","System studio":"System studio","Settings copy":"Network, SSH, tunnels, and TLS organized visually and applied live.",
|
||||
"One domain and port":"One domain and port","Shared endpoint copy":"The selected protocol uses /; SSH uses /ssh. Available in native Xray mode.","Protocol on /":"Protocol on /","Shared port":"Shared port","Listen IP":"Listen IP","HTTP host":"HTTP host","optional":"optional","XHTTP mode":"XHTTP mode","Security":"Security","No TLS":"No TLS","Certificate file":"Certificate file","Key file":"Key file","Create / update endpoint":"Create / update endpoint",
|
||||
"Configured inbounds":"Configured inbounds","Visual inbound help":"Edit any card visually or use JSON for advanced fields.","New inbound":"New inbound","Visual editor":"Visual editor","Add inbound":"Add inbound","Save changes":"Save changes","Duplicate":"Duplicate","Remove":"Remove","Save config and restart":"Save config and restart"
|
||||
"Configured inbounds":"Configured inbounds","Visual inbound help":"Edit any card visually or use JSON for advanced fields.","New inbound":"New inbound","Visual editor":"Visual editor","Add inbound":"Add inbound","Save changes":"Save changes","Duplicate":"Duplicate","Remove":"Remove","Save config and restart":"Save config and restart",
|
||||
"Old XHTTP configuration?":"Old XHTTP configuration?","Legacy XHTTP migration copy":"Use “Enable SSH /ssh” on the existing inbound card. The panel preserves the inbound, clients, and every current option; it only adds the SSH route and restarts Xray.","Enable SSH /ssh":"Enable SSH /ssh","SSH /ssh enabled":"SSH /ssh enabled","Legacy XHTTP migration":"Legacy XHTTP migration"
|
||||
});
|
||||
Object.assign(I18N_TEXT["pt-BR"], {
|
||||
"Command center":"Central de comando","Overview command copy":"Contas, conexões e infraestrutura em uma leitura rápida.","Access workspace":"Área de acessos","SSH accounts":"Contas SSH","SSH accounts copy":"Crie, limite e acompanhe acessos SSH e SlowDNS com segurança.",
|
||||
@@ -129,7 +130,8 @@ Object.assign(I18N_TEXT["pt-BR"], {
|
||||
"Fleet control":"Controle da frota","Fleet control copy":"Adicione nós, teste credenciais e configure a infraestrutura remota.","Live fleet":"Frota ao vivo","Server status copy":"Saúde, carga e sessões ativas de cada nó gerenciado.","Observability":"Observabilidade","Monitoring copy":"Recursos, interfaces e capacidade do servidor em tempo real.",
|
||||
"Traffic intelligence":"Inteligência de tráfego","Network traffic":"Tráfego de rede","Network traffic copy":"Histórico diário e mensal para entender o consumo da infraestrutura.","Diagnostics":"Diagnóstico","System logs copy":"Investigue painel, DNSTT e Xray com uma visualização focada.","System studio":"Estúdio do sistema","Settings copy":"Rede, SSH, túneis e TLS organizados em blocos visuais e aplicados ao vivo.",
|
||||
"One domain and port":"Um domínio e uma porta","Shared endpoint copy":"O protocolo selecionado usa /; SSH usa /ssh. Disponível no modo Xray nativo.","Protocol on /":"Protocolo em /","Shared port":"Porta compartilhada","Listen IP":"IP de listen","HTTP host":"Host HTTP","optional":"opcional","XHTTP mode":"Modo XHTTP","Security":"Segurança","No TLS":"Sem TLS","Certificate file":"Arquivo do certificado","Key file":"Arquivo da chave","Create / update endpoint":"Criar / atualizar endpoint",
|
||||
"Configured inbounds":"Inbounds configurados","Visual inbound help":"Edite qualquer cartão visualmente ou use JSON para campos avançados.","New inbound":"Novo inbound","Visual editor":"Editor visual","Add inbound":"Adicionar inbound","Save changes":"Salvar alterações","Duplicate":"Duplicar","Remove":"Remover","Save config and restart":"Salvar configuração e reiniciar"
|
||||
"Configured inbounds":"Inbounds configurados","Visual inbound help":"Edite qualquer cartão visualmente ou use JSON para campos avançados.","New inbound":"Novo inbound","Visual editor":"Editor visual","Add inbound":"Adicionar inbound","Save changes":"Salvar alterações","Duplicate":"Duplicar","Remove":"Remover","Save config and restart":"Salvar configuração e reiniciar",
|
||||
"Old XHTTP configuration?":"Configuração XHTTP antiga?","Legacy XHTTP migration copy":"Use “Ativar SSH /ssh” no cartão do inbound existente. O painel mantém o inbound, os clientes e todas as opções atuais; adiciona somente a rota SSH e reinicia o Xray.","Enable SSH /ssh":"Ativar SSH /ssh","SSH /ssh enabled":"SSH /ssh ativado","Legacy XHTTP migration":"Migração de configuração XHTTP antiga"
|
||||
});
|
||||
const I18N_REVERSE = Object.fromEntries(SUPPORTED_LANGS.map(lang => [lang, Object.fromEntries(Object.entries(I18N_TEXT[lang] || {}).map(([k, v]) => [v, k]))]));
|
||||
let currentLang = detectInitialLanguage();
|
||||
|
||||
@@ -152,6 +152,18 @@ function renderWzInbounds() {
|
||||
|
||||
const actions = document.createElement("div");
|
||||
actions.className = "visual-inbound-actions";
|
||||
const xhttp = visualXHTTPSettings(ib);
|
||||
const proxyProtocol = String(ib.protocol || "").toLowerCase();
|
||||
if (xhttp && ["vless", "vmess"].includes(proxyProtocol)) {
|
||||
const sshRoute = findSSHRouteForInbound(ib);
|
||||
const sshBtn = document.createElement("button");
|
||||
sshBtn.className = sshRoute ? "btn btn-soft btn-sm legacy-ssh-btn is-enabled" : "btn btn-sm legacy-ssh-btn";
|
||||
sshBtn.type = "button";
|
||||
sshBtn.textContent = t(sshRoute ? "SSH /ssh enabled" : "Enable SSH /ssh");
|
||||
sshBtn.disabled = !!sshRoute;
|
||||
sshBtn.onclick = () => enableSSHForWzInbound(i);
|
||||
actions.appendChild(sshBtn);
|
||||
}
|
||||
const duplicateBtn = document.createElement("button");
|
||||
duplicateBtn.className = "btn btn-ghost btn-sm";
|
||||
duplicateBtn.type = "button";
|
||||
@@ -310,6 +322,108 @@ function visualXHTTPSettings(ib) {
|
||||
return ss.xhttpSettings || ss.splithttpSettings || {};
|
||||
}
|
||||
|
||||
function sameVisualEndpoint(a, b) {
|
||||
return String(a?.listen || "0.0.0.0") === String(b?.listen || "0.0.0.0") && String(a?.port) === String(b?.port);
|
||||
}
|
||||
|
||||
function findSSHRouteForInbound(source) {
|
||||
return wzInbounds.find(candidate => {
|
||||
const xh = visualXHTTPSettings(candidate);
|
||||
return sameVisualEndpoint(source, candidate) && !!xh && String(candidate?.protocol || "").toLowerCase() === "ssh" && normalizeVisualPath(xh.path) === "/ssh";
|
||||
}) || null;
|
||||
}
|
||||
|
||||
function uniqueLegacySSHTag(source) {
|
||||
const tags = new Set(wzInbounds.map(ib => String(ib?.tag || "")));
|
||||
const sourceTag = String(source?.tag || source?.port || "xhttp").toLowerCase().replace(/[^a-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "xhttp";
|
||||
let tag = `ssh-${sourceTag}`;
|
||||
let suffix = 2;
|
||||
while (tags.has(tag)) tag = `ssh-${sourceTag}-${suffix++}`;
|
||||
return tag;
|
||||
}
|
||||
|
||||
// Adds SSH to a legacy VLESS/VMess XHTTP listener without rebuilding or
|
||||
// modifying the original inbound. The companion inherits its listen address,
|
||||
// port, host, XHTTP mode, advanced transport options, TLS certificate, and key;
|
||||
// only its protocol, empty SSH settings, tag, and /ssh path differ.
|
||||
async function enableSSHForWzInbound(index, applyNow = true) {
|
||||
const st = document.getElementById("wzStatus");
|
||||
const source = wzInbounds[index];
|
||||
const selectedID = selectedXrayServer() || "local";
|
||||
if (!source || !visualXHTTPSettings(source) || !["vless", "vmess"].includes(String(source.protocol || "").toLowerCase())) {
|
||||
if (st) st.textContent = "Selecione um inbound VLESS/VMess com transporte XHTTP.";
|
||||
return false;
|
||||
}
|
||||
if (!wzLoadedConfigText || String(wzLoadedServerID || "") !== String(selectedID)) {
|
||||
if (st) st.textContent = "Carregue a configuração do servidor selecionado antes de ativar SSH.";
|
||||
return false;
|
||||
}
|
||||
if ((document.getElementById("xCoreMode")?.value || "native") !== "native") {
|
||||
if (st) st.textContent = "SSH compartilhado requer o modo Xray nativo.";
|
||||
return false;
|
||||
}
|
||||
const security = String(source.streamSettings?.security || "").toLowerCase();
|
||||
if (security && security !== "none" && security !== "tls") {
|
||||
if (st) st.textContent = `O inbound ${source.tag || "selecionado"} usa ${security}, que não é suportado pelo SSH XHTTP nativo. Use TLS ou sem TLS.`;
|
||||
return false;
|
||||
}
|
||||
const existingSSH = findSSHRouteForInbound(source);
|
||||
if (existingSSH) {
|
||||
if (st) st.textContent = `SSH já está ativado em /ssh pelo inbound ${existingSSH.tag}.`;
|
||||
return true;
|
||||
}
|
||||
const pathConflict = wzInbounds.find(candidate => {
|
||||
const xh = visualXHTTPSettings(candidate);
|
||||
return sameVisualEndpoint(source, candidate) && !!xh && normalizeVisualPath(xh.path) === "/ssh";
|
||||
});
|
||||
if (pathConflict) {
|
||||
if (st) st.textContent = `O caminho /ssh já é usado pelo inbound ${pathConflict.tag || "sem tag"}. Edite esse caminho primeiro.`;
|
||||
return false;
|
||||
}
|
||||
if (security === "tls") {
|
||||
const certificate = source.streamSettings?.tlsSettings?.certificates?.[0] || {};
|
||||
if (!certificate.certificateFile || !certificate.keyFile) {
|
||||
if (st) st.textContent = "O inbound usa TLS, mas não possui arquivos de certificado e chave reutilizáveis.";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (applyNow && !confirm(`Ativar SSH em /ssh no mesmo endpoint de ${source.tag || "este inbound"}?\n\nA configuração antiga e todos os clientes serão mantidos. O Xray será reiniciado para aplicar.`)) return false;
|
||||
|
||||
const streamSettings = cloneJsonSafe(source.streamSettings || {});
|
||||
const settingsKey = streamSettings.xhttpSettings ? "xhttpSettings" : (streamSettings.splithttpSettings ? "splithttpSettings" : "xhttpSettings");
|
||||
streamSettings[settingsKey] = Object.assign({}, streamSettings[settingsKey] || {}, { path:"/ssh" });
|
||||
const sshInbound = {
|
||||
tag: uniqueLegacySSHTag(source),
|
||||
listen: source.listen || "0.0.0.0",
|
||||
port: cloneJsonSafe(source.port),
|
||||
protocol: "ssh",
|
||||
settings: {},
|
||||
streamSettings,
|
||||
};
|
||||
const before = cloneJsonSafe(source);
|
||||
wzInbounds.push(sshInbound);
|
||||
try {
|
||||
validateVisualInbounds(wzInbounds);
|
||||
} catch (error) {
|
||||
wzInbounds.pop();
|
||||
if (st) st.textContent = `Não foi possível ativar SSH: ${error.message}`;
|
||||
return false;
|
||||
}
|
||||
if (JSON.stringify(source) !== JSON.stringify(before)) {
|
||||
wzInbounds.pop();
|
||||
if (st) st.textContent = "A migração foi cancelada porque alteraria o inbound antigo.";
|
||||
return false;
|
||||
}
|
||||
wzDirty = true;
|
||||
renderWzInbounds();
|
||||
if (!applyNow) {
|
||||
if (st) st.textContent = `SSH /ssh adicionado ao rascunho sem alterar ${source.tag || "o inbound antigo"}.`;
|
||||
return true;
|
||||
}
|
||||
if (st) st.textContent = `Ativando SSH /ssh sem alterar ${source.tag || "o inbound antigo"}…`;
|
||||
return await applyWizardConfig();
|
||||
}
|
||||
|
||||
function validateVisualInbounds(inbounds) {
|
||||
const tags = new Set();
|
||||
const binds = new Map();
|
||||
@@ -752,7 +866,7 @@ async function applyWizardConfig() {
|
||||
if (String(wzLoadedServerID || "") !== String(selectedID) || !wzLoadedConfigText) {
|
||||
if (st) st.textContent = `Reloading config from ${target} before saving...`;
|
||||
loadWizardFromConfig();
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
let cfg;
|
||||
@@ -760,7 +874,7 @@ async function applyWizardConfig() {
|
||||
cfg = buildConfigFromVisualEditor();
|
||||
} catch(e) {
|
||||
if (st) st.textContent = `Invalid visual config: ${e.message}`;
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (st) st.textContent = `Saving config to ${target}...`;
|
||||
@@ -778,8 +892,10 @@ async function applyWizardConfig() {
|
||||
? `Config saved on ${target} and Xray restarted.`
|
||||
: `Config saved on ${target}, but Xray could not restart. Check Xray logs before editing again.`;
|
||||
setTimeout(() => { loadXrayStatus(); loadInbounds({ force: true }); }, 700);
|
||||
return !!restarted;
|
||||
} catch (e) {
|
||||
if (e.message==="auth") doAuthError();
|
||||
else if (st) st.textContent = "Error: " + e.message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user