diff --git a/readme.md b/readme.md index c1e2d0f..1bf4205 100644 --- a/readme.md +++ b/readme.md @@ -65,6 +65,16 @@ Não coloque código de produção abaixo desse marcador. O app trata isso como O app injeta o bridge de compatibilidade antes de `` quando o HTML contém uma tag `head`. Se o HTML não tiver ``, o bridge é colocado no começo do documento. +### 2.1 Cada objeto nativo pode não existir + +Nesta versão, todo objeto `Cake*` só é registrado no WebView se a camada nativa autorizar aquele nome (`SecureOnlineNative.isBridgeAllowed`). Um APK pode ser publicado com parte do bridge desativada. + +Consequências práticas para o tema: + +- Um objeto bloqueado fica `undefined`, e o alias `Dt*` correspondente também, porque o alias é criado como `window.DtX = window.DtX || window.CakeX`. +- `CakeApp.*` é injetado sempre, mas os métodos dele chamam os objetos nativos diretamente. Se o objeto estiver bloqueado, a chamada lança erro. +- Por isso, sempre use `try/catch` e checagem de tipo, como nos helpers da seção 1, tanto para `Dt*` quanto para `CakeApp.*`. + --- ## 3. Esqueleto rápido de tema @@ -300,14 +310,25 @@ O helper `CakeApp` injetado nesta versão expõe somente `CakeApp.vpn`, `CakeApp | API | Assinatura | Retorno | Finalidade | |---|---:|---|---| -| `DtUsername.get()` | sem argumentos | string | Retorna o usuário manual salvo, ou `Locked` quando a config selecionada não precisa de credenciais SSH manuais. | -| `DtUsername.set(value)` | string | void | Salva o usuário somente se credenciais manuais forem necessárias. Ignorado se estiver bloqueado. | -| `DtPassword.get()` | sem argumentos | string | Retorna a senha manual salva, ou `Locked` quando não for necessária. | -| `DtPassword.set(value)` | string | void | Salva a senha somente se credenciais manuais forem necessárias. Ignorado se estiver bloqueado. | -| `DtUuid.get()` | sem argumentos | string | Retorna o UUID manual V2Ray/Xray salvo, ou `Locked` quando a config selecionada não precisa de UUID manual. | -| `DtUuid.set(value)` | string | void | Salva o UUID. Nesta versão do app, escreve o valor do UUID e mantém cache para futuras seleções de config. | +| `DtUsername.get()` | sem argumentos | string | Retorna o usuário manual salvo, ou **string vazia** quando a config selecionada não aceita usuário manual. | +| `DtUsername.set(value)` | string | void | Sempre guarda o valor no cache manual do OnlineConfig. Só grava nas preferências do perfil se credenciais manuais forem aceitas. | +| `DtPassword.get()` | sem argumentos | string | Retorna a senha manual salva, ou **string vazia** quando não for aceita. | +| `DtPassword.set(value)` | string | void | Mesmo comportamento de `DtUsername.set`: cache sempre, gravação somente se aceito. | +| `DtUuid.get()` | sem argumentos | string | Retorna o UUID manual V2Ray/Xray salvo, ou **string vazia** quando a config selecionada não aceita UUID manual. | +| `DtUuid.set(value)` | string | void | Sempre guarda o UUID no cache manual. Só grava nas preferências se o perfil aceitar UUID manual. | -Importante: limpe `Locked` antes de colocar valores em inputs. +Importante nesta versão: **o bridge não retorna mais o texto `Locked`**. Quando o input manual não é aceito, `get()` retorna string vazia (`""`). + +O que decide se o input manual é aceito: + +| Getter/Setter | Aceito quando | +|---|---| +| `DtUsername`, `DtPassword` | O payload do perfil veio sem `username`/`password` (o app marca credenciais manuais como obrigatórias) **ou** o perfil do catálogo declara `auth.username: true` ou `auth.password: true`. | +| `DtUuid` | O perfil é Xray/V2Ray (`tunnelType 6`) sem UUID no payload **ou** o perfil do catálogo declara `auth.v2ray_uuid: true`. | + +Como o cache manual é sempre gravado, `DtUsername.set()` / `DtPassword.set()` / `DtUuid.set()` chamados antes da seleção do servidor continuam valendo: quando o perfil for aplicado e pedir credencial manual, o app reaproveita o último valor em cache. + +O helper de limpeza abaixo continua recomendado, porque trata string vazia, `null` e também o antigo `Locked` de temas migrados de outros apps: ```js function cleanBridgeValue(value) { @@ -341,6 +362,8 @@ Possíveis estados de `DtGetVpnState` / `DtVpnStateListener`: | `NO_NETWORK` | Aguardando rede. | | `AUTH_FAILED` | Falha de autenticação. | +Atenção: o estado nativo `RECONECTANDO` também é mapeado para `CONNECTING`. Nesta versão, a reconexão automática é silenciosa e mantém a VPN/TUN ativa, então `CONNECTING` depois de um `CONNECTED` **não** significa que o usuário perdeu a sessão nem que o tema deve limpar a tela. Veja a seção 23. + #### 5.3.1 Erros de conexão/autenticação/proxy O patch expõe um bridge específico para o tema entender por que a conexão falhou, sem precisar depender apenas de texto solto no botão ou no status. @@ -460,8 +483,8 @@ O payload esperado no callback é o que o seu servidor retornar. O app não for | `DtGetLocalIP.execute()` | sem argumentos | string | Retorna o IPv4 local, ou `0.0.0.0`/vazio quando indisponível. | | `DtGetNetworkName.execute()` | sem argumentos | string | Retorna o nome exibido da rede/operadora ativa. | | `DtGetPingResult.execute()` | sem argumentos | string | A versão atual retorna `N/A`. Temas não devem depender disso para ping real. | -| `DtGetStatusBarHeight.execute()` | sem argumentos | string numérica | Altura da status bar nativa do Android em pixels. | -| `DtGetNavigationBarHeight.execute()` | sem argumentos | string numérica | Altura da navigation bar nativa do Android em pixels. | +| `DtGetStatusBarHeight.execute()` | sem argumentos | string numérica | Altura da status bar nativa **já convertida para pixels CSS** (dp). Use direto em `px` no CSS, sem dividir por `devicePixelRatio`. Retorna `0` se o Android não expor a dimensão. | +| `DtGetNavigationBarHeight.execute()` | sem argumentos | string numérica | Altura da navigation bar nativa **em pixels CSS** (dp), mesma regra acima. | | `DtStartWebViewActivity.execute(url)` | string URL | void | Abre uma URL externamente/atividade WebView nativa. | | `DtIgnoreBatteryOptimizations.execute()` | sem argumentos | void | Abre as configurações de otimização de bateria. | | `DtStartApnActivity.execute()` | sem argumentos | void | Abre as configurações de APN. | @@ -759,8 +782,9 @@ Depois da seleção, o tema deve atualizar a própria UI lendo `DtGetDefaultConf O bridge atual não injeta `CakeApp.applyAuthVisibility()`. O tema deve decidir a visibilidade usando: -- `auth.username`, `auth.password` e `auth.v2ray_uuid` vindos de `DtGetDefaultConfig.execute()`. -- `DtUsername.get()`, `DtPassword.get()` e `DtUuid.get()`, que retornam `Locked` quando aquele input manual não deve ser editado. +- `auth.username`, `auth.password` e `auth.v2ray_uuid` vindos de `DtGetDefaultConfig.execute()`. **Esta é a fonte principal nesta versão.** +- `mode` do perfil, para perfis baseados em UUID (`V2RAY`, `XRAY`, `VMESS`, `VLESS`). +- Os getters `DtUsername.get()`, `DtPassword.get()` e `DtUuid.get()` apenas como fonte de valor inicial. Eles retornam string vazia quando o input não é aceito, e string vazia também é o retorno normal de um input aceito mas ainda não preenchido, então **getter vazio não serve como sinal de "campo bloqueado"**. HTML recomendado: @@ -813,9 +837,13 @@ Mesmo assim, o formato mais limpo para configs baseadas em UUID continua sendo: Use isto no tema se quiser exibição confiável do UUID para nomes `vmess`, `vless`, `xray` e `v2ray`. +Cuidado com XHTTP: não use nomes como `XRAY XHTTP` no `mode` do catálogo para perfis XHTTP + SSH, porque o helper abaixo detecta `xray` no texto e mostraria o campo UUID em um perfil que na verdade usa usuário/senha. Use `XHTTP`, `SSH_XHTTP` ou `XHTTP_SSH`. Veja a seção 22. + ```js function isLocked(value) { - return String(value ?? '').trim().toLowerCase() === 'locked'; + // Compatibilidade: esta versão retorna string vazia, versões/apps antigos retornavam 'Locked'. + const text = String(value ?? '').trim(); + return text === '' || text.toLowerCase() === 'locked'; } function bridgeGet(name) { @@ -838,6 +866,7 @@ function bridgeExec(name, fallback = '') { function isUuidCatalogMode(config) { const mode = String(config?.mode || '').toLowerCase(); + if (mode.includes('xhttp')) return false; // XHTTP + SSH usa usuário/senha return mode.includes('v2ray') || mode.includes('xray') || mode.includes('vmess') || @@ -851,24 +880,23 @@ function updateAuthVisibility(config = null) { } const auth = config?.auth || {}; - const uuidLocked = isLocked(bridgeGet('DtUuid')); - const usernameLocked = isLocked(bridgeGet('DtUsername')); - const passwordLocked = isLocked(bridgeGet('DtPassword')); - const uuidRequired = ( + // Decida pela config, não pelos getters: nesta versão eles retornam string + // vazia tanto para campo não aceito quanto para campo aceito e ainda vazio. + const uuidRequired = auth.v2ray_uuid === true || auth.manual_v2ray_uuid === true || - isUuidCatalogMode(config) - ) && !uuidLocked; + isUuidCatalogMode(config); - const sshRequired = ( + const sshRequired = !uuidRequired && ( auth.username === true || auth.manual_username === true || auth.password === true || auth.manual_password === true || - !usernameLocked || - !passwordLocked - ) && !uuidRequired; + // Fallback: perfil sem bloco auth mas com valor manual já salvo/cacheado. + !isLocked(bridgeGet('DtUsername')) || + !isLocked(bridgeGet('DtPassword')) + ); const uuidGroup = document.querySelector('#uuid-group'); const usernameGroup = document.querySelector('#username-group'); @@ -880,6 +908,8 @@ function updateAuthVisibility(config = null) { } ``` +Se o perfil não tem bloco `auth` no catálogo e nada foi salvo ainda, nenhum dos dois sinais aparece. Nesse caso, prefira mostrar usuário/senha por padrão para modos SSH (`SSH_DIRECT`, `SSH_PROXY`, `SSH_SSL`, `SSL_PROXY`, `DNSTT`, `XHTTP`) em vez de esconder tudo. Um tema que esconde todos os inputs deixa o usuário sem como conectar. + --- ## 10. Iniciando e parando a conexão @@ -987,8 +1017,36 @@ window.dtLogsUpdatedListener = function(logsJson) { O buffer de logs nativo exposto ao HTML é limitado a entradas recentes de status, mensagens de banner do servidor e mensagens úteis de proxy. Ele não é o logcat/debug completo. +### 11.1 Limites e o que não aparece em `DtGetLogs` -### 11.1 Como extrair o código HTTP do proxy no tema +| Buffer | Tamanho nesta versão | Conteúdo | +|---|---|---| +| Feed exposto ao tema (`DtGetLogs.execute()`) | 200 entradas mais recentes | Mudanças de estado, banner do servidor e linhas de proxy/HTTP. | +| Buffer nativo de log do serviço | 10.000 entradas (antes 1.000) | Log completo do túnel, visível em `DtShowLoggerDialog.execute()` e no logcat. | + +O corte do buffer nativo também mudou: antes o app apagava um bloco grande de entradas de uma vez, o que parecia log truncado. Agora ele remove uma entrada por vez ao passar do limite. + +Linhas que **não** entram no feed do tema, mesmo estando no log nativo: + +- Mensagens de transporte XHTTP, como `Iniciando XHTTP (TLS)...`, `XHTTP proto=h2` e `XHTTP downlink stopped: ...`. +- `VPN lost. Reconnecting... ` e as mensagens de reconexão automática. +- `UDP forward is off: only TCP and DNS are carried by the tunnel`. +- `Ignoring unsupported DNS resolver (IPv4 required): `. + +Para essas, o tema deve usar o estado (`DtVpnStateListener`) e o diálogo nativo (`DtShowLoggerDialog.execute()`). Se você precisa mostrar progresso de XHTTP na UI do tema, baseie-se em `CONNECTING` → `AUTH` → `CONNECTED`, não em texto de log. + +### 11.2 Depurando um tema com logcat + +Como a tela do app é um WebView, o log interno nem sempre é acessível durante o desenvolvimento. Nesta versão, **toda entrada do log nativo também é espelhada no logcat** com a tag `VOIDPRO`, já sem as tags HTML de formatação: + +```txt +adb logcat -s VOIDPRO +``` + +O nível é mapeado assim: erro do túnel vira `E`, aviso vira `W`, debug/verbose vira `D` e o restante vira `I`. Isso é útil para conferir se a sua chamada de bridge realmente disparou a ação nativa que você esperava. + + +### 11.3 Como extrair o código HTTP do proxy no tema Quando o app recebe uma linha `HTTP/...`, o classificador do bridge também atualiza `DtGetLastConnectionError.execute()` e dispara `dtConnectionErrorListener(error)` quando o status for relevante para o tema. @@ -1202,12 +1260,24 @@ Perfis contêm: O HTML recebe os perfis como `items`, não como `profiles`, ao chamar `DtGetConfigs.execute()`. +O catálogo também aceita um formato sem grupos. Se o JSON não tiver `groups` mas tiver `profiles` na raiz, o app cria automaticamente um grupo único com `id: "default"` e nome vindo de `groupName`. O tema continua recebendo grupos normalmente em `DtGetConfigs.execute()`. + +```json +{ + "title": "My VPN", + "groupName": "Servidores", + "profiles": [] +} +``` + --- ## 16. Campos de payload/settings do perfil que afetam o bridge Quando um perfil é selecionado, o app aplica um arquivo/base64 de config ou um payload JSON de settings. +O app reconhece um JSON como payload de settings quando ele contém pelo menos uma destas chaves: `serverHost`, `server_host`, `sshServer`, `mode`, `tunnelType`, `config_v2ray`, `xrayConfig`, `xhttpPath`, `xhttp_path`, `xhttp` ou `payload`. Um payload XHTTP que só traga `xhttp: { ... }` já é reconhecido. + Os campos de settings abaixo importam para o que o bridge HTML vai reportar depois: | Campo settings | Aliases | Usado para | @@ -1226,6 +1296,38 @@ Os campos de settings abaixo importam para o que o bridge HTML vai reportar depo | `slowNameServer` | `nameServer`, `ns`, `nameserver` | Nameserver SlowDNS. | | `slowDnsServer` | `dns`, `dnsServer`, `slowDns` | Servidor DNS SlowDNS. | | `slowDnsKey` | `key`, `slowKey` | Chave SlowDNS. | +| `slowDnsMode` | `dnsMode` | Modo SlowDNS. Padrão `udp` quando ausente. | +| `localPort` | `sshLocalPort`, `local_port` | Porta SOCKS local. Padrão `1080` quando ausente. | +| `useDefaultPayload` | `defaultPayload`, `use_default_payload` | Usar payload padrão. Padrão `true`. | +| `tlsMode` | `tlsVersion`, `tls12` | Modo/versão TLS. | +| `publicKey` | `pubKey`, `customPubkey` | Chave pública customizada. | +| `xhttpPath` | `xhttp_path`, `xhttp.path` | Path XHTTP. Padrão `/xhttp` quando ausente. | +| `xhttpHost` | `xhttp_host`, `xhttp.host` | Host/`:authority` HTTP do XHTTP. | +| `xhttpTls` | `xhttp_tls`, `xhttp.tls` | TLS do XHTTP. Padrão `true`. | + +Campos de VPN/DNS/UDP também aceitos no payload de settings: + +| Campo settings | Aliases | Padrão | Usado para | +|---|---|---|---| +| `dnsForward` | `vpn.dnsForward` | `true` | Encaminhar DNS pelo túnel. | +| `dnsResolver` | `dns1`, `vpn.dnsResolver` | `8.8.8.8` | Resolver DNS primário. | +| `dnsResolverSecondary` | `dns2`, `vpn.dnsResolverSecondary` | `8.8.4.4` | Resolver DNS secundário. | +| `udpForward` | `vpn.udpForward` | `true` | Encaminhar UDP pelo túnel. Com `false`, o túnel carrega só TCP e DNS. | +| `udpResolver` | `udpResolverHost`, `vpn.udpResolver` | `127.0.0.1:7300` | Endpoint do resolvedor UDP. | +| `disableIpv6Tunnel` | `ipv6Disabled`, `vpn.disableIpv6Tunnel` | `true` | Desativar IPv6 no TUN. | + +Os resolvers precisam ser IPv4. Um valor IPv6 é ignorado e o app registra `Ignoring unsupported DNS resolver (IPv4 required): ` no log nativo. + +O payload de settings aceita objetos aninhados, que são achatados para os campos acima. O valor de raiz tem prioridade sobre o aninhado: + +| Objeto aninhado | Mapeia para | +|---|---| +| `server: { host, port }` | `serverHost`, `serverPort` | +| `auth: { username, password, v2ray_uuid, uuid }` | `username`, `password`, `xrayUuid` | +| `proxy: { host, port }` | `proxyHost`, `proxyPort` | +| `xhttp: { path, host, tls }` | `xhttpPath`, `xhttpHost`, `xhttpTls` | +| `slowDns: { ns, nameserver, dns, dnsServer, key, dnsMode }` | campos SlowDNS | +| `vpn: { dnsForward, dnsResolver, dnsResolverSecondary, udpForward, udpResolver, disableIpv6Tunnel }` | campos de VPN/DNS/UDP | Valores atuais de tipo de túnel reconhecidos pelo parser de settings do app: @@ -1237,6 +1339,9 @@ Valores atuais de tipo de túnel reconhecidos pelo parser de settings do app: | `4`, `SLOW_DNS`, `DNSTT` | SlowDNS/DNSTT. | | `5`, `SSL_PROXY`, `SSH_SSL_PROXY` | SSH SSL proxy. | | `6`, `V2RAY`, `XRAY` | Xray/V2Ray. | +| `7`, `XHTTP`, `SSH_XHTTP`, `XHTTP_SSH` | XHTTP + SSH. Novo nesta versão, veja a seção 22. | + +Qualquer outro valor cai em `SSH_DIRECT` (`1`). Um `mode` escrito errado no painel não gera erro: o perfil simplesmente conecta como SSH direto. Para perfis V2Ray com UUID, use `mode: "V2RAY"` ou `tunnelType: 6` no payload de settings. @@ -1301,7 +1406,7 @@ let configs = []; try { configs = JSON.parse(DtGetConfigs.execute() || '[]'); } catch (e) {} ``` -### Erro: mostrar `Locked` nos inputs +### Erro: jogar o retorno cru dos getters nos inputs Ruim: @@ -1315,6 +1420,25 @@ Bom: usernameInput.value = cleanBridgeValue(DtUsername.get()); ``` +`cleanBridgeValue` cobre os três casos: `null`, string vazia desta versão e o antigo `Locked` de temas migrados. + +### Erro: usar getter vazio como "campo bloqueado" + +Nesta versão, `DtUsername.get()`, `DtPassword.get()` e `DtUuid.get()` retornam string vazia tanto quando o input não é aceito quanto quando ele é aceito e ainda está vazio. + +Ruim: + +```js +if (!DtUuid.get()) hideUuidField(); +``` + +Bom: + +```js +const current = OnlineBridge.getCurrentConfig(); +if (!(current.auth?.v2ray_uuid || isUuidCatalogMode(current))) hideUuidField(); +``` + ### Erro: não tratar erros classificados do bridge Ruim: @@ -1481,7 +1605,8 @@ const OnlineBridge = { Antes de publicar um tema, verifique: - Funciona quando `DtGetDefaultConfig.execute()` retorna `{}`. -- Não mostra `Locked` para o usuário. +- Funciona quando um objeto do bridge não existe, porque o APK pode ter aquele nome desativado. +- Não mostra `Locked` nem string vazia crua para o usuário. - Consegue fazer parse seguro de JSON vazio ou inválido. - Escuta `DtVpnStateListener`. - Escuta `dtLogsUpdatedListener` se o tema mostra logs/proxy-status em tempo real. @@ -1490,6 +1615,9 @@ Antes de publicar um tema, verifique: - Salva usuário/senha/UUID antes de conectar. - Trata `auth.username`, `auth.password` e `auth.v2ray_uuid`. - O campo UUID aparece para configs com `auth.v2ray_uuid: true`; se o catálogo usar `mode: "VMESS"` ou `"VLESS"`, o helper do tema trata esses nomes. +- Perfis XHTTP mostram usuário/senha, não UUID. +- Não trata `CONNECTING` depois de `CONNECTED` como sessão perdida, porque a reconexão automática é silenciosa. +- Não depende de texto de log para mostrar progresso de XHTTP. - Se usar proxy local, confirma `DtGetStatusHotSpotService.execute()` depois de chamar start/stop. - Se usar proxy local em Android 13+, considera que a notificação pode depender da permissão `POST_NOTIFICATIONS`. - Não depende de `DtGetPingResult` retornar ping real. @@ -1498,7 +1626,7 @@ Antes de publicar um tema, verifique: --- -## 21. Resumo dos bridges novos do patch V2/V3/V4/V5/V6 +## 21. Resumo dos bridges novos do patch V2/V3/V4/V5/V6/V7 Esta versão do patch adiciona/atualiza estes pontos para temas HTML OnlineConfig: @@ -1516,7 +1644,22 @@ Esta versão do patch adiciona/atualiza estes pontos para temas HTML OnlineConfi | SSH_DIRECT com payload/proxy | logs + erro classificado | Mesmo em SSH direto, o app tenta detectar resposta HTTP inicial do proxy sem quebrar banner SSH. | | Proxy local foreground | `DtStartHotSpotService.execute()` | Inicia `ProxyService` com notificação foreground em vez de ligar apenas o socket interno. | | Permissão de notificação | abertura do `OnlineConfigWebActivity` e start do proxy | Android 13+ pede `POST_NOTIFICATIONS`; se negar, o tema deve confirmar o status do proxy. | -| Detecção UUID no tema | `DtGetDefaultConfig.execute()` + `DtUuid.get()` | O bridge expõe `auth.v2ray_uuid` e `Locked`; o tema decide a visibilidade, incluindo `v2ray`, `xray`, `vmess` e `vless` quando necessário. | +| Detecção UUID no tema | `DtGetDefaultConfig.execute()` + `DtUuid.get()` | O bridge expõe `auth.v2ray_uuid`; o tema decide a visibilidade, incluindo `v2ray`, `xray`, `vmess` e `vless` quando necessário. | + +Novidades específicas da V7: + +| Item | Bridge/callback | O que muda para o tema | +|---|---|---| +| Modo XHTTP + SSH | `DtGetConfigs.execute()` / `DtGetDefaultConfig.execute()` (`mode`) | Novo `tunnelType 7`. O catálogo pode trazer `mode: "XHTTP"`, `"SSH_XHTTP"` ou `"XHTTP_SSH"`. Pede usuário/senha, nunca UUID. Veja a seção 22. | +| Campos XHTTP no payload | payload de settings | `xhttpPath` (padrão `/xhttp`), `xhttpHost`, `xhttpTls` (padrão ligado), além do objeto `xhttp: { path, host, tls }`. | +| Reconexão silenciosa | `DtVpnStateListener` | Reconexão automática mantém VPN/TUN/porta SOCKS local ativas e reporta `CONNECTING`. O tema não deve limpar a tela nem tratar como desconexão. Veja a seção 23. | +| Trava de sessão | nenhum bridge novo | Servidor, porta, SNI, Host, Path e TLS do XHTTP são congelados no início da sessão. Mudança de perfil durante o túnel ativo não afeta a reconexão. | +| `dtVpnStoppedSuccessListener` | callback | Continua disparando só quando a parada foi pedida pelo tema, nunca em reconexão automática. | +| Getters de auth sem `Locked` | `DtUsername.get()`, `DtPassword.get()`, `DtUuid.get()` | Retornam string vazia quando o input manual não é aceito. A visibilidade deve vir de `auth.*` e do `mode`. | +| Alturas de barra em px CSS | `DtGetStatusBarHeight`, `DtGetNavigationBarHeight` | Já vêm divididas pela densidade da tela. Use direto em `px` no CSS. | +| Campos de UDP/DNS no payload | payload de settings | `udpForward`, `udpResolver`, `dnsForward`, `dnsResolver`, `dnsResolverSecondary`, `disableIpv6Tunnel`, com o objeto `vpn: { ... }`. | +| Log maior + logcat | `DtGetLogs.execute()`, `DtShowLoggerDialog.execute()` | Feed do tema com as 200 entradas recentes, buffer nativo com 10.000, e todo o log espelhado no logcat com a tag `VOIDPRO`. | +| Bridge opcional por APK | todos os objetos `Cake*`/`Dt*` | Um objeto pode não ser registrado. Sempre proteja as chamadas. Veja a seção 2.1. | Exemplo completo para tema tratar logs e erros: @@ -1560,3 +1703,192 @@ function restoreLastErrorOnLoad() { if (error.code) window.dtConnectionErrorListener(error); } ``` + +--- + +## 22. Modo XHTTP + SSH (novo) + +O app passou a suportar um transporte XHTTP (SplitHTTP) sobre HTTP/2 para o túnel SSH. Ele é o `tunnelType 7` e aparece na tela nativa com o rótulo `XHTTP + SSH`. + +Para o tema, XHTTP é **um modo SSH**: usa usuário e senha, e não usa UUID. + +### 22.1 Como o painel declara um perfil XHTTP + +No perfil do catálogo, use um destes valores de `mode`: + +```json +{ "mode": "XHTTP" } +{ "mode": "SSH_XHTTP" } +{ "mode": "XHTTP_SSH" } +{ "tunnelType": 7 } +``` + +E declare o `auth` normalmente: + +```json +{ + "id": "xhttp_1", + "name": "Servidor XHTTP", + "description": "XHTTP + SSH via CDN", + "mode": "XHTTP", + "methodName": "XHTTP", + "auth": { + "username": true, + "password": true, + "v2ray_uuid": false + } +} +``` + +Não escreva `mode` misturando nomes de Xray, como `XRAY XHTTP`. Helpers de tema que procuram `xray`/`v2ray` no texto do `mode` mostrariam o campo UUID em um perfil que precisa de usuário/senha. + +### 22.2 Campos de payload usados pelo XHTTP + +| Campo | Aliases | Padrão | Significado | +|---|---|---|---| +| `serverHost` | `sshServer`, `server_host` | — | IP/host que o app realmente conecta. | +| `serverPort` | `sshPort`, `server_port` | — | Porta do servidor XHTTP. | +| `xhttpPath` | `xhttp_path`, `xhttp.path` | `/xhttp` | Path do inbound XHTTP. | +| `xhttpHost` | `xhttp_host`, `xhttp.host` | vazio | Cabeçalho `Host`/`:authority` HTTP. Normalmente a borda da CDN. | +| `xhttpTls` | `xhttp_tls`, `xhttp.tls` | `true` | TLS ligado. Envie `false` para XHTTP em texto claro. | +| `sni` | `sslSni`, `customSni` | vazio | SNI TLS. Com TLS ligado, é também o host da URL, o que permite domain fronting com bug host. | + +Exemplo de payload de settings: + +```json +{ + "mode": "XHTTP", + "serverHost": "203.0.113.10", + "serverPort": 443, + "username": "", + "password": "", + "sni": "bughost.operadora.com.br", + "xhttp": { + "path": "/xhttp", + "host": "edge.cdn.exemplo.com", + "tls": true + } +} +``` + +Com `username` e `password` vazios, o app marca credenciais manuais como obrigatórias, então o tema deve mostrar os inputs de usuário/senha. + +### 22.3 O que o tema vê durante uma conexão XHTTP + +O transporte XHTTP não publica linhas no feed de logs do tema. O que o tema recebe é a sequência normal de estados: + +```txt +CONNECTING -> AUTH -> CONNECTED +``` + +As mensagens de diagnóstico do XHTTP ficam no log nativo e no logcat (`adb logcat -s VOIDPRO`): + +| Mensagem | Significado | +|---|---| +| `Iniciando XHTTP (TLS)...` / `Iniciando XHTTP (sem TLS)...` | Abertura da sessão XHTTP. | +| `XHTTP proto=h2` | Protocolo negociado. `http/1.1` aqui indica que a CDN não passou HTTP/2 e o handshake tende a travar. | +| `XHTTP download rejected: HTTP ` | O servidor/CDN recusou o downlink. | +| `XHTTP downlink stopped: ` | O stream de download terminou. | +| `XHTTP uplink stopped: ` | O canal de upload terminou. | + +Limitação conhecida nesta versão: essas mensagens de rejeição do XHTTP usam o formato `HTTP `, sem o prefixo `HTTP/1.1`, então o classificador de erro não as converte em códigos `PROXY_*`. Uma recusa de XHTTP normalmente chega ao tema como falha de conexão genérica, sem `code` específico em `DtGetLastConnectionError.execute()`. Se o tema precisa distinguir esse caso, oriente o usuário pelo estado e pelo diálogo nativo de logs. + +### 22.4 Configuração travada durante o túnel + +Servidor, porta, SNI, Host, Path e TLS do XHTTP são fotografados no início da sessão do serviço. Enquanto o túnel estiver ativo: + +- Trocar de perfil com `DtSetConfig.execute(id)` altera a seleção salva, mas **não** muda o transporte da sessão em andamento nem o que a reconexão automática vai usar. +- Para aplicar um perfil novo de verdade, pare a VPN (`DtExecuteVpnStop.execute()`), espere `DISCONNECTED` e só então inicie de novo. + +Padrão recomendado no tema: + +```js +function applyProfileAndReconnect(profileId) { + const state = OnlineBridge.getState(); + OnlineBridge.selectConfig(profileId); + + if (state === 'DISCONNECTED') { + refreshCurrentServer(); + return; + } + + window.dtVpnStoppedSuccessListener = function() { + window.dtVpnStoppedSuccessListener = null; + refreshCurrentServer(); + OnlineBridge.start(); + }; + + OnlineBridge.stop(); +} +``` + +--- + +## 23. Reconexão silenciosa + +Quando a conexão SSH/XHTTP cai sozinha, o app não derruba mais a VPN. Ele mantém ativos o `VpnService`, a interface TUN, as rotas, o processo tun2socks e a porta SOCKS local, e substitui apenas a conexão que falhou. + +O que isso significa para o tema: + +| Situação | Estado reportado | O que o tema deve fazer | +|---|---|---| +| Queda e reconexão automática | `CONNECTING` (estado nativo `RECONECTANDO`) | Mostrar "reconectando" e manter a tela conectada. Não limpar dados, não pedir login de novo. | +| Rede indisponível durante a reconexão | `NO_NETWORK` | Mostrar aviso de rede. A sessão pode voltar sozinha quando a rede retornar. | +| Reconexão concluída | `CONNECTED` | Voltar ao estado conectado normal. | +| Usuário parou pelo tema | `DISCONNECTING` e depois `DISCONNECTED`, mais `dtVpnStoppedSuccessListener()` | Aí sim resetar a UI. | + +Regras práticas: + +- `dtVpnStoppedSuccessListener` só dispara quando a parada foi pedida pelo bridge. Use esse callback, e não `DISCONNECTED` puro, como sinal de "o usuário desconectou". +- Não use `CONNECTING` como gatilho para zerar contador de tempo, limpar log ou reabrir a tela de login. Guarde o estado anterior e trate a transição `CONNECTED -> CONNECTING` como reconexão. +- Notificação, vibração e som ficam suprimidos durante a reconexão automática. O tema também não deve emitir alerta a cada tentativa. +- Callbacks atrasados de conexões antigas são descartados pelo app, então o tema não recebe eventos duplicados de uma sessão já substituída. + +Exemplo de tratamento: + +```js +let lastState = 'DISCONNECTED'; + +window.DtVpnStateListener = function(state) { + const wasOnline = lastState === 'CONNECTED' || lastState === 'AUTH'; + const reconnecting = wasOnline && state === 'CONNECTING'; + + if (reconnecting) { + renderStatus('Reconectando...'); + } else { + renderVpnState(state); + } + + if (state === 'CONNECTED') { + renderStatus('Conectado'); + } + + lastState = state; +}; + +window.dtVpnStoppedSuccessListener = function() { + lastState = 'DISCONNECTED'; + resetUi(); +}; +``` + +--- + +## 24. UDP, DNS e IPv6 + +O payload do perfil controla o que o túnel carrega. Os padrões, quando o campo não vem no payload: + +| Campo | Padrão | +|---|---| +| `dnsForward` | `true` | +| `dnsResolver` | `8.8.8.8` | +| `dnsResolverSecondary` | `8.8.4.4` | +| `udpForward` | `true` | +| `udpResolver` | `127.0.0.1:7300` | +| `disableIpv6Tunnel` | `true` | + +Pontos que aparecem em suporte de tema: + +- Com `udpForward: false`, o túnel carrega só TCP e DNS. Jogos e chamadas de voz que dependem de UDP não funcionam nesse perfil, e o log nativo registra `UDP forward is off: only TCP and DNS are carried by the tunnel`. +- Os resolvers precisam ser IPv4. Um endereço IPv6 é ignorado, com a linha `Ignoring unsupported DNS resolver (IPv4 required): ` no log nativo. +- O tema não tem bridge para ler ou alterar esses campos. Eles vêm do painel. Se o tema mostra "UDP ativo", isso precisa vir de informação do seu próprio painel, não do bridge.