XHTTP Panel Fix
This commit is contained in:
@@ -59,6 +59,8 @@ Na aba **Xray → Configuração Xray → Visual**, o cartão **Shared XHTTP end
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
Para configurações XHTTP antigas, carregue a configuração visual e clique em **Ativar SSH /ssh** no cartão VLESS/VMess existente. O painel preserva o inbound original, seus clientes, path, host, modo, opções avançadas e TLS; cria apenas o inbound SSH correspondente na mesma porta e salva/reinicia o Xray. Não é necessário recriar clientes ou o endpoint.
|
||||||
|
|
||||||
### Requisitos
|
### Requisitos
|
||||||
|
|
||||||
- Servidor Linux com `systemd`
|
- Servidor Linux com `systemd`
|
||||||
@@ -581,6 +583,8 @@ In **Xray → Xray Config → Visual**, the **Shared XHTTP endpoint** card creat
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
|
For older XHTTP configurations, load the visual configuration and click **Enable SSH /ssh** on the existing VLESS/VMess card. The panel preserves the original inbound, clients, path, host, mode, advanced options, and TLS; it only creates the matching SSH inbound on the same port and then saves/restarts Xray. There is no need to recreate clients or the endpoint.
|
||||||
|
|
||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- Linux server with `systemd`
|
- Linux server with `systemd`
|
||||||
|
|||||||
@@ -674,12 +674,14 @@ select:disabled {
|
|||||||
.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-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-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}
|
.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}
|
||||||
|
.legacy-xhttp-migration{display:flex;align-items:center;gap:13px;margin:-3px 0 15px;padding:13px 15px;border:1px solid rgba(49,214,123,.19);border-radius:18px;background:linear-gradient(135deg,rgba(49,214,123,.075),rgba(34,211,238,.035));color:var(--text-2)}.legacy-xhttp-icon{display:grid;place-items:center;flex:0 0 auto;width:42px;height:42px;border:1px solid rgba(49,214,123,.25);border-radius:14px;background:rgba(49,214,123,.11);color:#72e6a4;font-size:.68rem;font-weight:950;letter-spacing:.035em}.legacy-xhttp-migration strong{display:block;color:var(--text);font-size:.8rem}.legacy-xhttp-migration p{margin-top:3px;color:var(--muted);font-size:.71rem;line-height:1.5}
|
||||||
.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-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-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)}
|
||||||
|
.legacy-ssh-btn{margin-right:auto;border-color:rgba(49,214,123,.3)!important;background:linear-gradient(135deg,rgba(49,214,123,.18),rgba(34,211,238,.09))!important;color:#8af0b5!important;box-shadow:inset 0 1px 0 rgba(255,255,255,.04)}.legacy-ssh-btn:hover:not(:disabled){border-color:rgba(49,214,123,.52)!important;transform:translateY(-1px)}.legacy-ssh-btn.is-enabled:disabled{opacity:1;border-color:rgba(49,214,123,.16)!important;background:rgba(49,214,123,.07)!important;color:#72b98e!important;cursor:default}
|
||||||
.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)}
|
.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: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%}}
|
@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}.legacy-xhttp-migration{align-items:flex-start}.visual-config-toolbar{grid-template-columns:1fr;align-items:stretch}.visual-config-toolbar .btn{width:100%}.visual-inbound-actions{align-items:stretch;flex-wrap:wrap}.legacy-ssh-btn{flex:1 0 100%;margin-right:0}}
|
||||||
|
|
||||||
/* --- Bot sales workspace --- */
|
/* --- Bot sales workspace --- */
|
||||||
#tab-bot{--bot-accent:#7c5cff;--bot-line:rgba(160,174,192,.14);}
|
#tab-bot{--bot-accent:#7c5cff;--bot-line:rgba(160,174,192,.14);}
|
||||||
|
|||||||
@@ -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.",
|
"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.",
|
"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",
|
"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"], {
|
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.",
|
"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.",
|
"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.",
|
"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",
|
"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]))]));
|
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 currentLang = detectInitialLanguage();
|
||||||
|
|||||||
@@ -152,6 +152,18 @@ function renderWzInbounds() {
|
|||||||
|
|
||||||
const actions = document.createElement("div");
|
const actions = document.createElement("div");
|
||||||
actions.className = "visual-inbound-actions";
|
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");
|
const duplicateBtn = document.createElement("button");
|
||||||
duplicateBtn.className = "btn btn-ghost btn-sm";
|
duplicateBtn.className = "btn btn-ghost btn-sm";
|
||||||
duplicateBtn.type = "button";
|
duplicateBtn.type = "button";
|
||||||
@@ -310,6 +322,108 @@ function visualXHTTPSettings(ib) {
|
|||||||
return ss.xhttpSettings || ss.splithttpSettings || {};
|
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) {
|
function validateVisualInbounds(inbounds) {
|
||||||
const tags = new Set();
|
const tags = new Set();
|
||||||
const binds = new Map();
|
const binds = new Map();
|
||||||
@@ -752,7 +866,7 @@ async function applyWizardConfig() {
|
|||||||
if (String(wzLoadedServerID || "") !== String(selectedID) || !wzLoadedConfigText) {
|
if (String(wzLoadedServerID || "") !== String(selectedID) || !wzLoadedConfigText) {
|
||||||
if (st) st.textContent = `Reloading config from ${target} before saving...`;
|
if (st) st.textContent = `Reloading config from ${target} before saving...`;
|
||||||
loadWizardFromConfig();
|
loadWizardFromConfig();
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cfg;
|
let cfg;
|
||||||
@@ -760,7 +874,7 @@ async function applyWizardConfig() {
|
|||||||
cfg = buildConfigFromVisualEditor();
|
cfg = buildConfigFromVisualEditor();
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
if (st) st.textContent = `Invalid visual config: ${e.message}`;
|
if (st) st.textContent = `Invalid visual config: ${e.message}`;
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (st) st.textContent = `Saving config to ${target}...`;
|
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} and Xray restarted.`
|
||||||
: `Config saved on ${target}, but Xray could not restart. Check Xray logs before editing again.`;
|
: `Config saved on ${target}, but Xray could not restart. Check Xray logs before editing again.`;
|
||||||
setTimeout(() => { loadXrayStatus(); loadInbounds({ force: true }); }, 700);
|
setTimeout(() => { loadXrayStatus(); loadInbounds({ force: true }); }, 700);
|
||||||
|
return !!restarted;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.message==="auth") doAuthError();
|
if (e.message==="auth") doAuthError();
|
||||||
else if (st) st.textContent = "Error: " + e.message;
|
else if (st) st.textContent = "Error: " + e.message;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-13
@@ -16,7 +16,7 @@
|
|||||||
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
setTimeout(function(){document.documentElement.classList.remove("i18n-pending");},2500);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
<link rel="stylesheet" href="assets/app.css?v=20260713xhttp2"/>
|
<link rel="stylesheet" href="assets/app.css?v=20260713legacyssh3"/>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app">
|
<div class="app">
|
||||||
@@ -428,6 +428,14 @@
|
|||||||
<div class="shared-endpoint-actions"><span id="sharedXHTTPStatus" class="hint">Carregue a configuração para detectar um endpoint existente.</span><button class="btn" id="sharedXHTTPApplyBtn" type="button">Criar / atualizar endpoint</button></div>
|
<div class="shared-endpoint-actions"><span id="sharedXHTTPStatus" class="hint">Carregue a configuração para detectar um endpoint existente.</span><button class="btn" id="sharedXHTTPApplyBtn" type="button">Criar / atualizar endpoint</button></div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<aside class="legacy-xhttp-migration" aria-label="Migração de configuração XHTTP antiga">
|
||||||
|
<span class="legacy-xhttp-icon" aria-hidden="true">SSH+</span>
|
||||||
|
<div>
|
||||||
|
<strong>Configuração XHTTP antiga?</strong>
|
||||||
|
<p>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.</p>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
<div class="visual-config-toolbar">
|
<div class="visual-config-toolbar">
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label>Log Level</label>
|
<label>Log Level</label>
|
||||||
@@ -1437,17 +1445,17 @@
|
|||||||
<!-- app.js was split into ordered modules for maintainability. They are plain
|
<!-- app.js was split into ordered modules for maintainability. They are plain
|
||||||
classic scripts sharing one global scope; `defer` preserves execution order,
|
classic scripts sharing one global scope; `defer` preserves execution order,
|
||||||
so behavior is identical to the old single file. Keep this load order. -->
|
so behavior is identical to the old single file. Keep this load order. -->
|
||||||
<script defer src="assets/js/01-core.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/01-core.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/02-shell.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/02-shell.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/03-ssh-users.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/03-ssh-users.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/04-xray.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/04-xray.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/05-resellers.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/05-resellers.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/06-servers.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/06-servers.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/07-stats-logs.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/07-stats-logs.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/08-server-config.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/08-server-config.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/09-xray-wizard.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/09-xray-wizard.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/11-update-status.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/11-update-status.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/12-bot.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/12-bot.js?v=20260713legacyssh3"></script>
|
||||||
<script defer src="assets/js/10-boot.js?v=20260713xhttp2"></script>
|
<script defer src="assets/js/10-boot.js?v=20260713legacyssh3"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
+1
-1
@@ -50,7 +50,7 @@ func TestSharedXHTTPListenerRejectsDuplicatePath(t *testing.T) {
|
|||||||
|
|
||||||
func TestValidateNativeSharedXHTTPConfig(t *testing.T) {
|
func TestValidateNativeSharedXHTTPConfig(t *testing.T) {
|
||||||
valid := []byte(`{"inbounds":[
|
valid := []byte(`{"inbounds":[
|
||||||
{"tag":"proxy","listen":"0.0.0.0","port":443,"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"path":"/"}}},
|
{"tag":"legacy-proxy","listen":"0.0.0.0","port":443,"protocol":"vless","streamSettings":{"network":"xhttp","xhttpSettings":{"path":"/legacy"}}},
|
||||||
{"tag":"ssh","listen":"0.0.0.0","port":443,"protocol":"ssh","streamSettings":{"network":"xhttp","xhttpSettings":{"path":"/ssh"}}}
|
{"tag":"ssh","listen":"0.0.0.0","port":443,"protocol":"ssh","streamSettings":{"network":"xhttp","xhttpSettings":{"path":"/ssh"}}}
|
||||||
]}`)
|
]}`)
|
||||||
if err := validateNativeInboundBindings(valid); err != nil {
|
if err := validateNativeInboundBindings(valid); err != nil {
|
||||||
|
|||||||
Reference in New Issue
Block a user