From 7d90568869f03d5ed1ada56d7dd54a78ad3d39f4 Mon Sep 17 00:00:00 2001 From: penguinehis Date: Mon, 13 Jul 2026 01:20:57 -0300 Subject: [PATCH] Xhttp and panel update --- README.md | 22 ++ admin/assets/app.css | 31 ++ admin/assets/js/01-core.js | 17 ++ admin/assets/js/04-xray.js | 2 + admin/assets/js/09-xray-wizard.js | 489 +++++++++++++++++++++++++++--- admin/index.html | 91 ++++-- xray_integration.go | 17 +- xray_native.go | 139 ++++++++- xray_native_test.go | 50 +++ xray_xhttp.go | 103 ++++++- 10 files changed, 855 insertions(+), 106 deletions(-) diff --git a/README.md b/README.md index 4538ac3..0d048e6 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ DragonCoreSSH V40 é um painel/servidor em Go para SSH com HTTP Injection, paine - Banco de dados PostgreSQL - Integração com Xray-core/V2Ray - Configurador visual para VLESS e VMess +- Editor visual completo de inbounds (criar, editar, duplicar e remover) +- Endpoint XHTTP compartilhado no modo nativo: VLESS **ou** VMess em `/` e SSH em `/ssh`, usando o mesmo domínio/porta/TLS - Contas de revendedor (reseller) com cota de usuários e escopo próprio - Gerenciamento multi-servidor (master/slave) direto pelo painel - API HTTP completa para bots/automações (ver **HTTP API Reference**) @@ -48,6 +50,15 @@ gRPC Observação: Reality deve ser usado apenas em protocolos compatíveis. No configurador visual, VMess não usa Reality. +### Endpoint XHTTP compartilhado + +Na aba **Xray → Configuração Xray → Visual**, o cartão **Shared XHTTP endpoint** cria dois inbounds na mesma porta: + +- VLESS **ou** VMess em `/` (selecione um protocolo raiz); +- SSH sobre XHTTP em `/ssh`, autenticado pelas contas SSH normais do painel. + +O listener HTTP interno escolhe o caminho mais específico, portanto `/ssh` é encaminhado ao servidor SSH antes do inbound raiz. Todos os inbounds na mesma porta devem usar a mesma opção TLS e o mesmo certificado. Esse recurso exige o modo **Internal native emulator**; o binário Xray externo não reconhece o protocolo `ssh` específico do DragonCore. + ### Requisitos - Servidor Linux com `systemd` @@ -522,6 +533,8 @@ DragonCoreSSH V40 is a Go-based SSH HTTP Injection server with a web panel, Post - PostgreSQL database - Xray-core/V2Ray integration - Visual configurator for VLESS and VMess +- Full visual inbound editor (create, edit, duplicate, and remove) +- Native shared XHTTP endpoint: VLESS **or** VMess on `/` and SSH on `/ssh`, using the same domain/port/TLS - Reseller accounts with a user quota and self-scoped access - Multi-server (master/slave) management directly from the panel - Full HTTP API for bots/automations (see **HTTP API Reference**) @@ -559,6 +572,15 @@ gRPC Note: Reality should only be used with compatible protocols. In the visual configurator, VMess does not use Reality. +### Shared XHTTP endpoint + +In **Xray → Xray Config → Visual**, the **Shared XHTTP endpoint** card creates two inbounds on the same port: + +- VLESS **or** VMess on `/` (select one root protocol); +- SSH over XHTTP on `/ssh`, authenticated by the panel's regular SSH accounts. + +The internal HTTP listener selects the most specific path, so `/ssh` reaches the SSH server before the root inbound. Every inbound sharing a port must use the same TLS setting and certificate. This feature requires **Internal native emulator** mode; the external Xray binary does not recognize DragonCore's custom `ssh` protocol. + ### Requirements - Linux server with `systemd` diff --git a/admin/assets/app.css b/admin/assets/app.css index 651c41d..01d3d58 100644 --- a/admin/assets/app.css +++ b/admin/assets/app.css @@ -650,6 +650,37 @@ select:disabled { .update-statusbar{align-items:center;gap:12px;flex-wrap:wrap;} .btn-xs{padding:5px 8px!important;font-size:.67rem!important;border-radius:9px!important;} +/* --- Shared visual language for every workspace tab --- */ +.page-hero{ + --hero-accent:34,211,238; + position:relative;display:flex;align-items:flex-end;justify-content:space-between;gap:22px; + min-height:150px;margin-bottom:18px;padding:25px 27px;overflow:hidden; + border:1px solid rgba(var(--hero-accent),.22);border-radius:28px; + background: + radial-gradient(circle at 88% 8%,rgba(var(--hero-accent),.24),transparent 33%), + radial-gradient(circle at 8% 115%,rgba(139,92,246,.12),transparent 40%), + linear-gradient(135deg,rgba(17,22,35,.98),rgba(8,11,18,.98)); + box-shadow:0 24px 70px rgba(0,0,0,.32),inset 0 1px 0 rgba(255,255,255,.04); +} +.page-hero::after{content:"";position:absolute;right:-65px;top:-100px;width:250px;height:250px;border:1px solid rgba(255,255,255,.055);border-radius:50%;box-shadow:0 0 0 34px rgba(255,255,255,.017),0 0 0 68px rgba(255,255,255,.011);pointer-events:none;} +.page-hero[data-tone="green"]{--hero-accent:49,214,123}.page-hero[data-tone="purple"]{--hero-accent:139,92,246}.page-hero[data-tone="amber"]{--hero-accent:255,200,87}.page-hero[data-tone="blue"]{--hero-accent:80,145,255} +.page-hero-copy,.page-hero-pills,.page-hero-mark{position:relative;z-index:1}.page-hero-copy{max-width:700px}.page-kicker{display:block;color:rgb(var(--hero-accent));font-size:.69rem;font-weight:900;letter-spacing:.17em;text-transform:uppercase}.page-hero h2{margin-top:7px;font-size:2rem;line-height:1.05;letter-spacing:-.045em}.page-hero p{max-width:650px;margin-top:9px;color:var(--muted);font-size:.82rem;line-height:1.55}.page-hero-pills{display:flex;align-items:center;justify-content:flex-end;gap:7px;flex-wrap:wrap;max-width:42%}.page-hero-pills span{padding:7px 10px;border:1px solid rgba(var(--hero-accent),.18);border-radius:999px;background:rgba(var(--hero-accent),.075);color:var(--text-2);font-size:.69rem;font-weight:850;letter-spacing:.04em}.page-hero-mark{display:grid;place-items:center;width:64px;height:64px;border:1px solid rgba(var(--hero-accent),.25);border-radius:22px;background:rgba(var(--hero-accent),.10);color:rgb(var(--hero-accent));font-size:1.05rem;font-weight:950;box-shadow:0 18px 44px rgba(0,0,0,.24)} + +/* Bring legacy screens up to the Bot workspace's information density. */ +.tab-pane:not(#tab-bot)>.grid2,.tab-pane:not(#tab-bot)>#serversListView>.grid2{gap:16px}.tab-pane:not(#tab-bot) .card-hdr{padding-bottom:12px;border-bottom:1px solid rgba(148,163,184,.09)}.tab-pane:not(#tab-bot) .card-title{font-size:.96rem}.tab-pane:not(#tab-bot) .statusbar{margin-top:13px;padding-top:11px;border-top:1px solid rgba(148,163,184,.08)} + +/* Xray visual configuration studio */ +.shared-endpoint-card{--hero-accent:139,92,246;position:relative;margin-bottom:18px;padding:20px;overflow:hidden;border:1px solid rgba(139,92,246,.22);border-radius:22px;background:radial-gradient(circle at 96% 0,rgba(139,92,246,.17),transparent 32%),rgba(8,12,20,.82)} +.shared-endpoint-head{display:flex;align-items:flex-start;justify-content:space-between;gap:14px}.shared-endpoint-head h3,.visual-editor-heading h3{margin-top:5px;font-size:1.12rem;letter-spacing:-.02em}.shared-endpoint-head p{margin-top:5px;color:var(--muted);font-size:.75rem;line-height:1.45}.shared-endpoint-head code,.shared-route-preview code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;color:#c8bbff} +.shared-route-preview{display:grid;grid-template-columns:1fr 48px 1fr;align-items:center;gap:8px;margin:17px 0;padding:10px;border:1px solid rgba(148,163,184,.11);border-radius:17px;background:rgba(255,255,255,.025)}.shared-route-preview span{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 12px;border:1px solid rgba(139,92,246,.16);border-radius:13px;background:rgba(139,92,246,.07)}.shared-route-preview strong{font-size:.78rem}.shared-route-preview code{font-size:.77rem;font-weight:900}.shared-route-preview i{height:1px;background:linear-gradient(90deg,rgba(139,92,246,.2),rgba(34,211,238,.7),rgba(139,92,246,.2));position:relative}.shared-route-preview i::after{content:"";position:absolute;right:0;top:-3px;width:7px;height:7px;border-radius:50%;background:var(--accent);box-shadow:0 0 12px var(--accent)} +.shared-endpoint-grid{grid-template-columns:repeat(3,minmax(0,1fr));}.shared-endpoint-actions{display:flex;align-items:center;justify-content:space-between;gap:14px;margin-top:15px;padding-top:14px;border-top:1px solid rgba(148,163,184,.1)}.shared-endpoint-actions .hint{max-width:650px} +.visual-config-toolbar{display:grid;grid-template-columns:180px minmax(0,1fr) auto;align-items:end;gap:14px;margin-bottom:13px;padding:13px 15px;border:1px solid rgba(148,163,184,.1);border-radius:18px;background:rgba(255,255,255,.025)}.visual-config-toolbar-copy{display:flex;flex-direction:column;gap:4px;padding-bottom:4px}.visual-config-toolbar-copy strong{font-size:.84rem}.visual-config-toolbar-copy span{color:var(--muted);font-size:.71rem} +.visual-inbound-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin-bottom:14px}.visual-inbound-card{position:relative;display:flex;flex-direction:column;gap:13px;min-width:0;padding:15px;border:1px solid rgba(148,163,184,.11);border-radius:18px;background:rgba(255,255,255,.027);transition:.15s ease}.visual-inbound-card:hover{border-color:rgba(139,92,246,.3);background:rgba(139,92,246,.045);transform:translateY(-1px)}.visual-inbound-card-head,.visual-inbound-meta,.visual-inbound-actions{display:flex;align-items:center;gap:8px}.visual-inbound-card-head{justify-content:space-between}.visual-inbound-name{min-width:0}.visual-inbound-name strong{display:block;overflow:hidden;text-overflow:ellipsis;color:var(--text);font-size:.84rem;white-space:nowrap}.visual-inbound-name small{display:block;margin-top:4px;color:var(--muted);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:.67rem}.visual-inbound-meta{flex-wrap:wrap}.visual-inbound-meta span{padding:4px 7px;border-radius:8px;background:rgba(148,163,184,.07);color:var(--muted);font-size:.67rem}.visual-inbound-actions{justify-content:flex-end;margin-top:auto;padding-top:11px;border-top:1px solid rgba(148,163,184,.08)} +.visual-inbound-editor{margin:14px 0;padding:18px;border:1px solid rgba(34,211,238,.2);border-radius:22px;background:radial-gradient(circle at 100% 0,rgba(34,211,238,.09),transparent 28%),rgba(6,10,16,.86)}.visual-editor-heading{--hero-accent:34,211,238;display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:15px;padding-bottom:13px;border-bottom:1px solid rgba(148,163,184,.1)}.visual-save-bar{position:sticky;bottom:14px;z-index:8;display:flex;align-items:center;justify-content:space-between;gap:14px;margin-top:16px;padding:13px 15px;border:1px solid rgba(139,92,246,.2);border-radius:19px;background:rgba(8,12,20,.9);box-shadow:0 18px 48px rgba(0,0,0,.35);backdrop-filter:blur(16px)} + +@media(max-width:1100px){.shared-endpoint-grid{grid-template-columns:repeat(2,minmax(0,1fr));}.visual-inbound-list{grid-template-columns:1fr}} +@media(max-width:760px){.page-hero{min-height:0;padding:20px;border-radius:22px;align-items:flex-start;flex-direction:column}.page-hero h2{font-size:1.55rem}.page-hero-pills{max-width:none;justify-content:flex-start}.page-hero-mark{width:50px;height:50px;border-radius:17px}.shared-endpoint-card{padding:15px}.shared-endpoint-head,.shared-endpoint-actions,.visual-save-bar{align-items:flex-start;flex-direction:column}.shared-endpoint-actions .btn,.visual-save-bar .btn{width:100%}.shared-route-preview{grid-template-columns:1fr}.shared-route-preview i{width:1px;height:22px;justify-self:center}.shared-route-preview i::after{right:-3px;top:auto;bottom:0}.shared-endpoint-grid{grid-template-columns:1fr!important}.visual-config-toolbar{grid-template-columns:1fr;align-items:stretch}.visual-config-toolbar .btn{width:100%}} + /* --- Bot sales workspace --- */ #tab-bot{--bot-accent:#7c5cff;--bot-line:rgba(160,174,192,.14);} .bot-hero{ diff --git a/admin/assets/js/01-core.js b/admin/assets/js/01-core.js index 96c5063..d406eb1 100644 --- a/admin/assets/js/01-core.js +++ b/admin/assets/js/01-core.js @@ -15,6 +15,7 @@ let wzLoadedFullConfig = null; let wzLoadedConfigText = ""; let wzLoadedServerID = null; let wzDirty = false; +let wzEditingIndex = -1; let dashboardCache = { sshUsers: [], xrayInbounds: [], me: null }; let currentTab = "dashboard"; let inboundsRefreshInFlight = false; @@ -114,6 +115,22 @@ Object.assign(I18N_ALIASES, { "Atualizações do painel":"Panel Updates","Verificando…":"Checking…","Abrir Git":"Open Git","Verificar agora":"Check now","Versão instalada":"Installed version","Última versão no Git":"Latest Git version","Última verificação":"Last checked", "Comparando a versão instalada com o repositório Git.":"Comparing the installed version with the Git repository.","Para atualizar:":"To update:","Copiar comando":"Copy command","Atualizado":"Up to date","Atualização disponível":"Update available","Alterações locais":"Local changes","Desconhecido":"Unknown" }); +Object.assign(I18N_TEXT["en-US"], { + "Command center":"Command center","Overview command copy":"Accounts, connections, and infrastructure at a glance.","Access workspace":"Access workspace","SSH accounts":"SSH accounts","SSH accounts copy":"Create, limit, and monitor SSH and SlowDNS access securely.", + "Proxy studio":"Proxy studio","Xray visual":"Visual Xray","Xray visual copy":"Manage clients, inbounds, and a shared XHTTP endpoint without editing JSON.","Partner operations":"Partner operations","Partner operations copy":"Control partner quotas, expiration, and access in one place.", + "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" +}); +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.", + "Proxy studio":"Estúdio de proxy","Xray visual":"Xray visual","Xray visual copy":"Gerencie clientes, inbounds e um endpoint XHTTP compartilhado sem editar JSON.","Partner operations":"Operação de parceiros","Partner operations copy":"Controle cotas, validade e acesso dos parceiros em um só lugar.", + "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" +}); 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(); let i18nTranslating = false; diff --git a/admin/assets/js/04-xray.js b/admin/assets/js/04-xray.js index d1310b5..21f090a 100644 --- a/admin/assets/js/04-xray.js +++ b/admin/assets/js/04-xray.js @@ -98,9 +98,11 @@ async function xrayCtrl(action) { xStatus.textContent = currentLang === "pt-BR" ? "Xray OK." : "Xray "+action+" OK."; setTimeout(loadXrayStatus, 700); setTimeout(() => loadInbounds({ force: true }), 1200); + return true; } catch (e) { if (e.message==="auth") doAuthError(); else xStatus.textContent = t("Error: {error}", {error: e.message}); + return false; } } diff --git a/admin/assets/js/09-xray-wizard.js b/admin/assets/js/09-xray-wizard.js index 99931b4..1ee0f6c 100644 --- a/admin/assets/js/09-xray-wizard.js +++ b/admin/assets/js/09-xray-wizard.js @@ -82,7 +82,9 @@ function loadWizardFromConfig() { wzLoadedFullConfig = cloneJsonSafe(cfg); document.getElementById("wzLogLevel").value = cfg.log?.loglevel || "warning"; wzInbounds = cloneJsonSafe((cfg.inbounds || []).filter(ib => ib && ib.tag !== "api")) || []; + wzEditingIndex = -1; renderWzInbounds(); + loadSharedEndpointForm(); wzDirty = false; if (st) st.textContent = `Config loaded from ${target}.`; }).catch(e => { @@ -90,7 +92,9 @@ function loadWizardFromConfig() { wzLoadedConfigText = ""; wzLoadedFullConfig = null; wzInbounds = []; + wzEditingIndex = -1; renderWzInbounds(); + loadSharedEndpointForm(); if (e.message === "auth") doAuthError(); else if (st) st.textContent = "Error: " + e.message; }); @@ -99,49 +103,396 @@ function loadWizardFromConfig() { function renderWzInbounds() { const list = document.getElementById("wzInboundsList"); if (!list) return; + list.replaceChildren(); if (!wzInbounds.length) { - list.innerHTML = '
No inbounds. Click + Add to create one.
'; + const empty = document.createElement("div"); + empty.className = "hint visual-empty-state"; + empty.textContent = "Nenhum inbound configurado. Crie um endpoint compartilhado ou adicione um inbound."; + list.appendChild(empty); return; } - list.innerHTML = ""; wzInbounds.forEach((ib, i) => { - const row = document.createElement("div"); - row.style = "display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid var(--border);font-size:.73rem;"; + const row = document.createElement("article"); + row.className = "visual-inbound-card"; const portStr = ib.port !== undefined ? `:${ib.port}` : ""; const ss = ib.streamSettings || {}; const net = ss.network || ""; const sec = ss.security || ""; - const secLabel = sec === "tls" ? " TLS" : sec === "reality" ? " Reality" : ""; - const modeLabel = net === "xhttp" && ss.xhttpSettings?.mode ? " ("+ss.xhttpSettings.mode+")" : ""; - row.innerHTML = `${escapeHTML(ib.protocol || "")} - ${escapeHTML((ib.tag||"untagged")+portStr)} - ${escapeHTML((ib.listen||"0.0.0.0")+(net?" · "+net:"")+modeLabel+secLabel)}`; + const transportSettings = ss.xhttpSettings || ss.splithttpSettings || ss.wsSettings || ss.httpupgradeSettings || ss.httpSettings || ss.grpcSettings || {}; + const head = document.createElement("div"); + head.className = "visual-inbound-card-head"; + const name = document.createElement("div"); + name.className = "visual-inbound-name"; + const title = document.createElement("strong"); + title.textContent = ib.tag || "untagged"; + const address = document.createElement("small"); + address.textContent = `${ib.listen || "0.0.0.0"}${portStr}`; + name.append(title, address); + const protocol = document.createElement("span"); + protocol.className = "chip"; + protocol.textContent = String(ib.protocol || "unknown").toUpperCase(); + head.append(name, protocol); + row.appendChild(head); + + const meta = document.createElement("div"); + meta.className = "visual-inbound-meta"; + [net || "default", transportSettings.path || transportSettings.serviceName || "no path", sec || "no TLS"].forEach(value => { + const item = document.createElement("span"); + item.textContent = value; + meta.appendChild(item); + }); + row.appendChild(meta); const clients = ib.settings?.clients; if (Array.isArray(clients) && clients.length) { const badge = document.createElement("span"); badge.className = "chip green"; badge.textContent = clients.length + " client" + (clients.length!==1?"s":""); - row.appendChild(badge); + meta.appendChild(badge); } + + const actions = document.createElement("div"); + actions.className = "visual-inbound-actions"; + const duplicateBtn = document.createElement("button"); + duplicateBtn.className = "btn btn-ghost btn-sm"; + duplicateBtn.type = "button"; + duplicateBtn.textContent = "Duplicar"; + duplicateBtn.onclick = () => duplicateWzInbound(i); + const editBtn = document.createElement("button"); + editBtn.className = "btn btn-ghost btn-sm"; + editBtn.type = "button"; + editBtn.textContent = "Editar"; + editBtn.onclick = () => editWzInbound(i); const delBtn = document.createElement("button"); delBtn.className = "btn btn-danger btn-sm"; - delBtn.textContent = "Remove"; - delBtn.onclick = () => { wzInbounds.splice(i,1); wzDirty = true; renderWzInbounds(); }; - row.appendChild(delBtn); + delBtn.type = "button"; + delBtn.textContent = "Remover"; + delBtn.onclick = () => { + if (!confirm(`Remover o inbound ${ib.tag || "untagged"}?`)) return; + wzInbounds.splice(i,1); + if (wzEditingIndex === i) wzCancelInbound(); + else if (wzEditingIndex > i) wzEditingIndex--; + wzDirty = true; + renderWzInbounds(); + loadSharedEndpointForm(); + }; + actions.append(duplicateBtn, editBtn, delBtn); + row.appendChild(actions); list.appendChild(row); }); } function wzToggleAddInbound() { const form = document.getElementById("wzAddInboundForm"); - form.classList.toggle("hidden"); - if (!form.classList.contains("hidden")) { - onWzProtoChange(document.getElementById("wzProtocol").value); - onWzNetworkChange(document.getElementById("wzNetwork").value); - onWzTLSChange(document.getElementById("wzTLS").value); + if (!form.classList.contains("hidden") && wzEditingIndex < 0) return wzCancelInbound(); + resetWzInboundForm(); + form.classList.remove("hidden"); + form.scrollIntoView({ behavior:"smooth", block:"nearest" }); +} + +function setWzValue(id, value) { + const el = document.getElementById(id); + if (el) el.value = value ?? ""; +} + +function resetWzInboundForm() { + wzEditingIndex = -1; + setWzValue("wzProtocol", "vless"); + setWzValue("wzPort", "10086"); + setWzValue("wzListenIP", "0.0.0.0"); + setWzValue("wzTag", "vless-in"); + setWzValue("wzNetwork", "tcp"); + setWzValue("wzWSPath", "/ws"); + setWzValue("wzXHTTPPath", "/xhttp"); + setWzValue("wzXHTTPHost", ""); + setWzValue("wzXHTTPMode", "auto"); + setWzValue("wzHUPath", "/upgrade"); + setWzValue("wzHUHost", ""); + setWzValue("wzH2Path", "/h2"); + setWzValue("wzH2Host", ""); + setWzValue("wzGRPCService", "grpc-service"); + document.getElementById("wzGRPCMulti").checked = false; + setWzValue("wzTLS", "none"); + ["wzTLSCert", "wzTLSKey", "wzTLSCertPath", "wzTLSKeyPath", "wzRealityDest", "wzRealitySNI", "wzRealityPriv", "wzRealityShortID", "wzTrojanPass", "wzSSPass"].forEach(id => setWzValue(id, "")); + setWzValue("wzSSMethod", "chacha20-ietf-poly1305"); + document.getElementById("wzInboundFormTitle").textContent = "Novo inbound"; + document.getElementById("wzSaveInboundBtn").textContent = "Adicionar inbound"; + document.getElementById("wzEditingBadge").classList.add("hidden"); + onWzProtoChange("vless"); + onWzNetworkChange("tcp"); + onWzTLSChange("none"); +} + +function wzCancelInbound() { + wzEditingIndex = -1; + document.getElementById("wzAddInboundForm")?.classList.add("hidden"); + document.getElementById("wzEditingBadge")?.classList.add("hidden"); +} + +function editWzInbound(index) { + const ib = wzInbounds[index]; + if (!ib) return; + resetWzInboundForm(); + wzEditingIndex = index; + const proto = String(ib.protocol || "vless").toLowerCase(); + setWzValue("wzProtocol", proto); + onWzProtoChange(proto); + setWzValue("wzPort", ib.port ?? ""); + setWzValue("wzListenIP", ib.listen || (proto === "socks" ? "127.0.0.1" : "0.0.0.0")); + setWzValue("wzTag", ib.tag || `${proto}-in`); + + const ss = ib.streamSettings || {}; + const network = proto === "ssh" ? "xhttp" : (ss.network || "tcp"); + setWzValue("wzNetwork", network); + onWzNetworkChange(network); + const xh = ss.xhttpSettings || ss.splithttpSettings || {}; + setWzValue("wzWSPath", ss.wsSettings?.path || "/ws"); + setWzValue("wzXHTTPPath", xh.path || "/xhttp"); + setWzValue("wzXHTTPHost", xh.host || ""); + setWzValue("wzXHTTPMode", xh.mode || "auto"); + setWzValue("wzHUPath", ss.httpupgradeSettings?.path || "/upgrade"); + setWzValue("wzHUHost", ss.httpupgradeSettings?.host || ""); + setWzValue("wzH2Path", ss.httpSettings?.path || "/h2"); + setWzValue("wzH2Host", Array.isArray(ss.httpSettings?.host) ? (ss.httpSettings.host[0] || "") : (ss.httpSettings?.host || "")); + setWzValue("wzGRPCService", ss.grpcSettings?.serviceName || "grpc-service"); + document.getElementById("wzGRPCMulti").checked = !!ss.grpcSettings?.multiMode; + + const security = ss.security === "tls" ? "tls" : (ss.security === "reality" ? "reality" : "none"); + setWzValue("wzTLS", security); + onWzTLSChange(security); + const cert = ss.tlsSettings?.certificates?.[0] || {}; + setWzValue("wzTLSCert", cert.certificateFile || ""); + setWzValue("wzTLSKey", cert.keyFile || ""); + setWzValue("wzTLSCertPath", cert.certificateFile || ""); + setWzValue("wzTLSKeyPath", cert.keyFile || ""); + setWzValue("wzRealityDest", ss.realitySettings?.dest || ""); + setWzValue("wzRealitySNI", ss.realitySettings?.serverNames?.[0] || ""); + setWzValue("wzRealityPriv", ss.realitySettings?.privateKey || ""); + setWzValue("wzRealityShortID", ss.realitySettings?.shortIds?.[0] || ""); + setWzValue("wzTrojanPass", ib.settings?.clients?.[0]?.password || ""); + setWzValue("wzSSPass", ib.settings?.password || ""); + setWzValue("wzSSMethod", ib.settings?.method || "chacha20-ietf-poly1305"); + + document.getElementById("wzInboundFormTitle").textContent = `Editar ${ib.tag || "inbound"}`; + document.getElementById("wzSaveInboundBtn").textContent = "Salvar alterações"; + document.getElementById("wzEditingBadge").classList.remove("hidden"); + const form = document.getElementById("wzAddInboundForm"); + form.classList.remove("hidden"); + form.scrollIntoView({ behavior:"smooth", block:"start" }); +} + +function duplicateWzInbound(index) { + const source = wzInbounds[index]; + if (!source) return; + const copy = cloneJsonSafe(source); + const tags = new Set(wzInbounds.map(ib => ib?.tag)); + let n = 2; + let tag = `${source.tag || source.protocol || "inbound"}-copy`; + while (tags.has(tag)) tag = `${source.tag || source.protocol || "inbound"}-copy-${n++}`; + copy.tag = tag; + const port = Number(copy.port || 0); + if (port > 0 && port < 65535) copy.port = port + 1; + wzInbounds.push(copy); + wzDirty = true; + renderWzInbounds(); + editWzInbound(wzInbounds.length - 1); +} + +function normalizeVisualPath(value) { + let path = String(value || "/").trim().split("?", 1)[0]; + if (!path.startsWith("/")) path = `/${path}`; + path = path.replace(/\/+$/, "") || "/"; + return path; +} + +function visualXHTTPSettings(ib) { + const ss = ib?.streamSettings || {}; + if (!["xhttp", "splithttp"].includes(String(ss.network || "").toLowerCase())) return null; + return ss.xhttpSettings || ss.splithttpSettings || {}; +} + +function validateVisualInbounds(inbounds) { + const tags = new Set(); + const binds = new Map(); + const nativeMode = (document.getElementById("xCoreMode")?.value || "native") === "native"; + for (const ib of inbounds || []) { + const tag = String(ib?.tag || "").trim(); + if (!tag) throw new Error("every inbound needs a tag"); + if (tags.has(tag)) throw new Error(`duplicate inbound tag: ${tag}`); + tags.add(tag); + const port = Number(ib?.port || 0); + if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error(`invalid port on ${tag}`); + const bind = `${String(ib?.listen || "0.0.0.0").trim()}:${port}`; + const xh = visualXHTTPSettings(ib); + if (!binds.has(bind)) binds.set(bind, []); + binds.get(bind).push({ ib, xh }); + if (String(ib?.protocol || "").toLowerCase() === "ssh") { + if (!xh) throw new Error(`SSH inbound ${tag} requires XHTTP`); + if (!nativeMode) throw new Error(`SSH inbound ${tag} requires native Xray mode`); + } + } + for (const [bind, rows] of binds) { + if (rows.length < 2) continue; + if (!nativeMode || rows.some(row => !row.xh)) throw new Error(`multiple inbounds cannot share ${bind} unless all use native XHTTP`); + const paths = new Set(); + const firstSecurity = String(rows[0].ib.streamSettings?.security || "none"); + const firstCert = rows[0].ib.streamSettings?.tlsSettings?.certificates?.[0] || {}; + for (const row of rows) { + const path = normalizeVisualPath(row.xh.path); + if (paths.has(path)) throw new Error(`duplicate XHTTP path ${path} on ${bind}`); + paths.add(path); + const security = String(row.ib.streamSettings?.security || "none"); + const cert = row.ib.streamSettings?.tlsSettings?.certificates?.[0] || {}; + if (security !== firstSecurity) throw new Error(`shared XHTTP inbounds on ${bind} must use the same TLS setting`); + if (security === "tls" && (cert.certificateFile !== firstCert.certificateFile || cert.keyFile !== firstCert.keyFile)) { + throw new Error(`shared XHTTP inbounds on ${bind} must use the same certificate`); + } + } } } +function findSharedEndpointPair() { + const roots = wzInbounds.filter(ib => { + const xh = visualXHTTPSettings(ib); + return !!xh && ["vless", "vmess"].includes(String(ib?.protocol || "").toLowerCase()) && normalizeVisualPath(xh.path) === "/"; + }); + for (const proxy of roots) { + const ssh = wzInbounds.find(ib => String(ib?.protocol || "").toLowerCase() === "ssh" && + String(ib.listen || "0.0.0.0") === String(proxy.listen || "0.0.0.0") && String(ib.port) === String(proxy.port) && + normalizeVisualPath(visualXHTTPSettings(ib)?.path) === "/ssh"); + if (ssh) return { proxy, ssh }; + } + const proxy = wzInbounds.find(ib => ib?.tag === "shared-proxy-xhttp") || null; + const ssh = wzInbounds.find(ib => ib?.tag === "shared-ssh-xhttp") || null; + return proxy && ssh ? { proxy, ssh } : null; +} + +function loadSharedEndpointForm() { + const status = document.getElementById("sharedXHTTPStatus"); + if (!status) return; + const pair = findSharedEndpointPair(); + if (!pair) { + status.textContent = wzLoadedConfigText ? "Nenhum endpoint compartilhado detectado. Preencha os campos para criar um." : "Carregue a configuração para detectar um endpoint existente."; + return; + } + const xh = visualXHTTPSettings(pair.proxy) || {}; + const ss = pair.proxy.streamSettings || {}; + const cert = ss.tlsSettings?.certificates?.[0] || {}; + setWzValue("sharedXHTTPProtocol", pair.proxy.protocol || "vless"); + setWzValue("sharedXHTTPPort", pair.proxy.port || 443); + setWzValue("sharedXHTTPListen", pair.proxy.listen || "0.0.0.0"); + setWzValue("sharedXHTTPHost", xh.host || ""); + setWzValue("sharedXHTTPMode", xh.mode || "auto"); + setWzValue("sharedXHTTPSecurity", ss.security === "tls" ? "tls" : "none"); + setWzValue("sharedXHTTPCert", cert.certificateFile || ""); + setWzValue("sharedXHTTPKey", cert.keyFile || ""); + updateSharedEndpointControls(); + status.textContent = `Endpoint detectado em ${pair.proxy.listen || "0.0.0.0"}:${pair.proxy.port} — ${String(pair.proxy.protocol).toUpperCase()} / e SSH /ssh.`; +} + +function updateSharedEndpointControls() { + const protocol = document.getElementById("sharedXHTTPProtocol")?.value || "vless"; + const security = document.getElementById("sharedXHTTPSecurity")?.value || "none"; + document.getElementById("sharedProxyRouteLabel").textContent = protocol.toUpperCase(); + document.querySelectorAll(".shared-tls-field").forEach(el => el.classList.toggle("hidden", security !== "tls")); +} + +function applySharedXHTTPEndpoint() { + const status = document.getElementById("sharedXHTTPStatus"); + const selectedID = selectedXrayServer() || "local"; + if (!wzLoadedConfigText || String(wzLoadedServerID || "") !== String(selectedID)) { + status.textContent = "Carregue a configuração do servidor selecionado antes de editar."; + return; + } + if ((document.getElementById("xCoreMode")?.value || "native") !== "native") { + status.textContent = "O endpoint compartilhado requer o modo Xray nativo."; + return; + } + const protocol = document.getElementById("sharedXHTTPProtocol").value; + const port = Number(document.getElementById("sharedXHTTPPort").value || 0); + const listen = document.getElementById("sharedXHTTPListen").value.trim() || "0.0.0.0"; + const host = document.getElementById("sharedXHTTPHost").value.trim(); + const mode = document.getElementById("sharedXHTTPMode").value || "auto"; + const security = document.getElementById("sharedXHTTPSecurity").value; + const cert = document.getElementById("sharedXHTTPCert").value.trim(); + const key = document.getElementById("sharedXHTTPKey").value.trim(); + if (!["vless", "vmess"].includes(protocol) || !Number.isInteger(port) || port < 1 || port > 65535) { + status.textContent = "Escolha VLESS/VMess e uma porta válida."; + return; + } + if (/[\u0000-\u001f\u007f]/.test(`${listen}${host}${cert}${key}`)) { + status.textContent = "Os campos contêm caracteres de controle inválidos."; + return; + } + if (security === "tls" && (!cert || !key)) { + status.textContent = "Informe os arquivos do certificado e da chave para usar TLS."; + return; + } + + const pair = findSharedEndpointPair(); + const sameEndpoint = ib => String(ib?.listen || "0.0.0.0") === listen && String(ib?.port) === String(port); + const existingProxy = pair?.proxy || wzInbounds.find(ib => ib?.tag === "shared-proxy-xhttp") || wzInbounds.find(ib => { + const xh = visualXHTTPSettings(ib); + return sameEndpoint(ib) && !!xh && ["vless", "vmess"].includes(String(ib?.protocol || "").toLowerCase()) && normalizeVisualPath(xh.path) === "/"; + }) || null; + const existingSSH = pair?.ssh || wzInbounds.find(ib => ib?.tag === "shared-ssh-xhttp") || wzInbounds.find(ib => { + const xh = visualXHTTPSettings(ib); + return sameEndpoint(ib) && !!xh && String(ib?.protocol || "").toLowerCase() === "ssh" && normalizeVisualPath(xh.path) === "/ssh"; + }) || null; + const removeSet = new Set([existingProxy, existingSSH].filter(Boolean)); + const others = wzInbounds.filter(ib => !removeSet.has(ib)); + const blocking = others.find(ib => String(ib.listen || "0.0.0.0") === listen && String(ib.port) === String(port) && !visualXHTTPSettings(ib)); + if (blocking) { + status.textContent = `A porta já é usada pelo inbound não-XHTTP ${blocking.tag || "sem tag"}. Escolha outra porta.`; + return; + } + + const buildSharedStream = (existing, path) => { + const stream = cloneJsonSafe(existing?.streamSettings || {}); + stream.network = "xhttp"; + stream.xhttpSettings = Object.assign({}, stream.xhttpSettings || stream.splithttpSettings || {}, { path, mode }); + delete stream.splithttpSettings; + if (host) stream.xhttpSettings.host = host; + else delete stream.xhttpSettings.host; + if (security === "tls") { + stream.security = "tls"; + stream.tlsSettings = Object.assign({}, stream.tlsSettings || {}, { certificates:[{ certificateFile:cert, keyFile:key }] }); + } else { + delete stream.security; + delete stream.tlsSettings; + } + delete stream.realitySettings; + return stream; + }; + const previousClients = Array.isArray(existingProxy?.settings?.clients) ? cloneJsonSafe(existingProxy.settings.clients) : []; + const proxyInbound = cloneJsonSafe(existingProxy || {}); + proxyInbound.tag = existingProxy?.tag || "shared-proxy-xhttp"; + proxyInbound.listen = listen; + proxyInbound.port = port; + proxyInbound.protocol = protocol; + proxyInbound.settings = existingProxy?.protocol === protocol ? cloneJsonSafe(existingProxy.settings || {}) : {}; + proxyInbound.settings.clients = previousClients; + if (protocol === "vless") proxyInbound.settings.decryption = "none"; + else delete proxyInbound.settings.decryption; + proxyInbound.streamSettings = buildSharedStream(existingProxy, "/"); + const sshInbound = cloneJsonSafe(existingSSH || {}); + sshInbound.tag = existingSSH?.tag || "shared-ssh-xhttp"; + sshInbound.listen = listen; + sshInbound.port = port; + sshInbound.protocol = "ssh"; + sshInbound.settings = {}; + sshInbound.streamSettings = buildSharedStream(existingSSH, "/ssh"); + wzInbounds = [...others, proxyInbound, sshInbound]; + wzDirty = true; + renderWzInbounds(); + loadSharedEndpointForm(); + status.textContent = "Endpoint atualizado no rascunho. Clique em Salvar configuração e reiniciar para aplicar."; +} + +document.getElementById("sharedXHTTPProtocol")?.addEventListener("change", updateSharedEndpointControls); +document.getElementById("sharedXHTTPSecurity")?.addEventListener("change", updateSharedEndpointControls); +document.getElementById("sharedXHTTPApplyBtn")?.addEventListener("click", applySharedXHTTPEndpoint); +updateSharedEndpointControls(); + function onWzProtoChange(val) { const isSSH = val === "ssh"; // SSH tunnels reuse the VLESS/VMess transport block to expose the XHTTP @@ -227,78 +578,115 @@ function wzSaveInbound() { const port = parseInt(document.getElementById("wzPort").value || "0", 10); const listen = document.getElementById("wzListenIP").value.trim() || "0.0.0.0"; const tag = document.getElementById("wzTag").value.trim() || proto+"-in"; - if (!port) { alert("Port required."); return; } - const ib = { tag, port, listen, protocol: proto, settings: {} }; + const st = document.getElementById("wzStatus"); + if (!Number.isInteger(port) || port < 1 || port > 65535) { st.textContent = "Informe uma porta válida."; return; } + if (/[\u0000-\u001f\u007f]/.test(`${listen}${tag}`)) { st.textContent = "Listen ou tag contém caracteres inválidos."; return; } + if (wzInbounds.some((item, index) => index !== wzEditingIndex && item?.tag === tag)) { st.textContent = `A tag ${tag} já está em uso.`; return; } + + const original = wzEditingIndex >= 0 ? cloneJsonSafe(wzInbounds[wzEditingIndex]) : null; + const ib = original || {}; + const previousClients = Array.isArray(original?.settings?.clients) ? cloneJsonSafe(original.settings.clients) : []; + ib.tag = tag; + ib.port = port; + ib.listen = listen; + ib.protocol = proto; + ib.settings = {}; if (proto === "vless" || proto === "vmess") { - ib.settings = proto === "vless" ? { clients: [], decryption: "none" } : { clients: [] }; + ib.settings = original?.protocol === proto && original.settings ? cloneJsonSafe(original.settings) : {}; + ib.settings.clients = previousClients; + if (proto === "vless") ib.settings.decryption = "none"; + else delete ib.settings.decryption; const net = document.getElementById("wzNetwork").value; const tlsVal = document.getElementById("wzTLS").value; - ib.streamSettings = { network: net }; + const previousStream = original?.protocol === proto && original?.streamSettings?.network === net ? cloneJsonSafe(original.streamSettings) : {}; + ib.streamSettings = previousStream || {}; + ib.streamSettings.network = net; + ["wsSettings", "xhttpSettings", "splithttpSettings", "httpupgradeSettings", "httpSettings", "grpcSettings"].forEach(key => { + if (!((net === "ws" && key === "wsSettings") || (net === "xhttp" && (key === "xhttpSettings" || key === "splithttpSettings")) || (net === "httpupgrade" && key === "httpupgradeSettings") || (net === "h2" && key === "httpSettings") || (net === "grpc" && key === "grpcSettings"))) delete ib.streamSettings[key]; + }); // Transport-specific settings switch (net) { case "ws": - ib.streamSettings.wsSettings = { path: document.getElementById("wzWSPath").value.trim() || "/" }; + ib.streamSettings.wsSettings = Object.assign({}, ib.streamSettings.wsSettings || {}, { path: document.getElementById("wzWSPath").value.trim() || "/" }); break; case "xhttp": - ib.streamSettings.xhttpSettings = { + ib.streamSettings.xhttpSettings = Object.assign({}, ib.streamSettings.xhttpSettings || ib.streamSettings.splithttpSettings || {}, { path: document.getElementById("wzXHTTPPath").value.trim() || "/xhttp", host: document.getElementById("wzXHTTPHost").value.trim() || undefined, mode: document.getElementById("wzXHTTPMode").value, - }; + }); + delete ib.streamSettings.splithttpSettings; if (!ib.streamSettings.xhttpSettings.host) delete ib.streamSettings.xhttpSettings.host; break; case "httpupgrade": - ib.streamSettings.httpupgradeSettings = { + ib.streamSettings.httpupgradeSettings = Object.assign({}, ib.streamSettings.httpupgradeSettings || {}, { path: document.getElementById("wzHUPath").value.trim() || "/", host: document.getElementById("wzHUHost").value.trim() || undefined, - }; + }); if (!ib.streamSettings.httpupgradeSettings.host) delete ib.streamSettings.httpupgradeSettings.host; break; case "h2": - ib.streamSettings.httpSettings = { + ib.streamSettings.httpSettings = Object.assign({}, ib.streamSettings.httpSettings || {}, { path: document.getElementById("wzH2Path").value.trim() || "/", host: [document.getElementById("wzH2Host").value.trim()].filter(Boolean), - }; + }); break; case "grpc": - ib.streamSettings.grpcSettings = { + ib.streamSettings.grpcSettings = Object.assign({}, ib.streamSettings.grpcSettings || {}, { serviceName: document.getElementById("wzGRPCService").value.trim() || "grpc", multiMode: document.getElementById("wzGRPCMulti").checked, - }; + }); break; } // TLS / Reality if (tlsVal === "tls") { + const certificateFile = document.getElementById("wzTLSCert").value.trim(); + const keyFile = document.getElementById("wzTLSKey").value.trim(); + if (!certificateFile || !keyFile) { st.textContent = "TLS exige os arquivos de certificado e chave."; return; } ib.streamSettings.security = "tls"; - ib.streamSettings.tlsSettings = { - certificates: [{ certificateFile: document.getElementById("wzTLSCert").value.trim(), keyFile: document.getElementById("wzTLSKey").value.trim() }], - }; + ib.streamSettings.tlsSettings = Object.assign({}, ib.streamSettings.tlsSettings || {}, { + certificates: [{ certificateFile, keyFile }], + }); + delete ib.streamSettings.realitySettings; } else if (tlsVal === "reality" && proto === "vless") { ib.streamSettings.security = "reality"; - ib.streamSettings.realitySettings = { + ib.streamSettings.realitySettings = Object.assign({}, ib.streamSettings.realitySettings || {}, { dest: document.getElementById("wzRealityDest").value.trim(), serverNames: [document.getElementById("wzRealitySNI").value.trim()].filter(Boolean), privateKey: document.getElementById("wzRealityPriv").value.trim(), shortIds: [document.getElementById("wzRealityShortID").value.trim()].filter(Boolean), - }; + }); + delete ib.streamSettings.tlsSettings; + } else { + delete ib.streamSettings.security; + delete ib.streamSettings.tlsSettings; + delete ib.streamSettings.realitySettings; } } else if (proto === "ssh") { // SSH tunnel over XHTTP: no proxy clients — the decoded stream is handed to // the SSH server, so authentication is an ordinary SSH account. ib.settings = {}; - ib.streamSettings = { network: "xhttp" }; - ib.streamSettings.xhttpSettings = { + ib.streamSettings = original?.protocol === "ssh" ? (cloneJsonSafe(original.streamSettings) || {}) : {}; + ib.streamSettings.network = "xhttp"; + ib.streamSettings.xhttpSettings = Object.assign({}, ib.streamSettings.xhttpSettings || ib.streamSettings.splithttpSettings || {}, { path: document.getElementById("wzXHTTPPath").value.trim() || "/xhttp", host: document.getElementById("wzXHTTPHost").value.trim() || undefined, mode: document.getElementById("wzXHTTPMode").value, - }; + }); + delete ib.streamSettings.splithttpSettings; if (!ib.streamSettings.xhttpSettings.host) delete ib.streamSettings.xhttpSettings.host; const tlsVal = document.getElementById("wzTLS").value; if (tlsVal === "tls") { + const certificateFile = document.getElementById("wzTLSCert").value.trim(); + const keyFile = document.getElementById("wzTLSKey").value.trim(); + if (!certificateFile || !keyFile) { st.textContent = "TLS exige os arquivos de certificado e chave."; return; } ib.streamSettings.security = "tls"; - ib.streamSettings.tlsSettings = { - certificates: [{ certificateFile: document.getElementById("wzTLSCert").value.trim(), keyFile: document.getElementById("wzTLSKey").value.trim() }], - }; + ib.streamSettings.tlsSettings = Object.assign({}, ib.streamSettings.tlsSettings || {}, { + certificates: [{ certificateFile, keyFile }], + }); + } else { + delete ib.streamSettings.security; + delete ib.streamSettings.tlsSettings; } } else if (proto === "trojan") { ib.settings = { clients: [{ password: document.getElementById("wzTrojanPass").value.trim() || "change-me" }] }; @@ -309,13 +697,13 @@ function wzSaveInbound() { ib.settings = { auth: "noauth", udp: true }; ib.streamSettings = { network: "tcp" }; } - wzInbounds.push(ib); + if (wzEditingIndex >= 0) wzInbounds[wzEditingIndex] = ib; + else wzInbounds.push(ib); wzDirty = true; renderWzInbounds(); - document.getElementById("wzAddInboundForm").classList.add("hidden"); - document.getElementById("wzPort").value = ""; - document.getElementById("wzTag").value = ""; - document.getElementById("wzListenIP").value = ""; + loadSharedEndpointForm(); + st.textContent = original ? `Inbound ${tag} atualizado no rascunho.` : `Inbound ${tag} adicionado ao rascunho.`; + wzCancelInbound(); } @@ -344,6 +732,7 @@ function buildConfigFromVisualEditor() { const existingInbounds = Array.isArray(cfg.inbounds) ? cfg.inbounds : []; const hiddenApiInbounds = existingInbounds.filter(ib => ib && ib.tag === "api"); const visualInbounds = cloneJsonSafe((wzInbounds || []).filter(ib => ib && ib.tag !== "api")) || []; + validateVisualInbounds(visualInbounds); cfg.inbounds = [...hiddenApiInbounds, ...visualInbounds]; return cfg; @@ -384,8 +773,10 @@ async function applyWizardConfig() { wzLoadedServerID = selectedID; wzDirty = false; if (st) st.textContent = `Saved on ${target}. Restarting Xray...`; - await xrayCtrl("restart"); - if (st) st.textContent = `Config saved on ${target} and Xray restarted.`; + const restarted = await xrayCtrl("restart"); + if (st) st.textContent = restarted + ? `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); } catch (e) { if (e.message==="auth") doAuthError(); diff --git a/admin/index.html b/admin/index.html index 8d38052..6955e82 100644 --- a/admin/index.html +++ b/admin/index.html @@ -16,7 +16,7 @@ setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500); })(); - +
@@ -87,6 +87,10 @@
+
+
Command center

Visão geral

Contas, conexões e infraestrutura em uma leitura rápida.

+ +
@@ -209,6 +213,10 @@
+
+
Access workspace

Contas SSH

Crie, limite e acompanhe acessos SSH e SlowDNS com segurança.

+
SSHTOTPSlowDNS
+