Skip to content
mobius1qwe edited this page Nov 9, 2026 · 4 revisions

Internos: Plugins 🚧

Home > Guia de contribuição > Internos: Plugins

Para quem mexe no código: o host de plugins, autenticação, segurança, CORS, conteúdo, concorrência e certificado automático. Aqui fica o porquê de cada parte ser como é, e o que não pode mudar sem quebrar algo. O código traz só a descrição curta de cada coisa.

Conteúdo da versão 2.0, ainda em desenvolvimento (branch 2.0).

O que a página de uso já diz (fases, Priority, como escrever um) está em Criar um plugin.

Os dois laços do servidor

  • Plugins primeiro, módulos depois. ProcessCommands roda os plugins por Priority e depois os módulos, que acham a rota e a executam. Servidor sem plugin ainda responde. Tudo o que não é roteamento é plugin (limite, compressão, cifra, listas, força bruta, flood, CORS, cada autenticação, corpo JSON como params), como um middleware, mas a ordem vem da prioridade e não da ordem em que a aplicação liga; empate mantém a ordem em que foram adicionados.
  • Uma lista por fase. O plugin declara as suas (Phases) e o host guarda uma lista para cada uma: fase que ninguém usa não custa nada.
  • ppValidate roda antes de o corpo ser decodificado: pedido recusado nunca é descomprimido nem decifrado. ppProcess é o laço dos plugins (quem responde marca AHandled). ppResolveRoute, ppAuthenticate e ppAuthResult não são laços do servidor: são chamadas entre plugins (rota própria, autenticadores decidindo, e quem quer saber o veredito; a força bruta conta as falhas ali, antes de qualquer módulo).
  • A rota é procurada uma vez (FindRoute) e guardada no pedido. A autenticação precisa dela (o método pula autenticação?), o CORS precisa dos métodos dela, e os módulos respondem sem procurar de novo.
  • RALPriorityConcurrency (50) fica depois de todo plugin do RAL que recusa em ppValidate: um pedido que a lista negra, o flood ou o limite de tamanho recusam nunca espera uma vaga na fila.

Ciclo de vida

  • ServerActivating/ServerDeactivating têm o contrato dos módulos. "Ativando" quer dizer pedido, não porta aberta: os motores chamam antes de abrir a porta, então o plugin ainda muda como ela abre (o TRALSelfSignedPlugin entrega o certificado ali). Uma exceção mantém o servidor parado. Plugin adicionado a servidor rodando ouve ServerActivating na hora.
  • PluginRemoved é o único aviso garantido. O plugin se tira do host antes de as notificações de Free saírem. Descendente que guarda referência a um plugin (o Authentication do servidor) limpa ali; só com Notification a referência ficaria solta.

Leitura sem lock

Qualquer mudança (adicionar, remover, Priority, Enabled) monta um snapshot novo (TRALPluginSnapshot) sob o lock. O velho fica vivo até o host ser destruído, porque um pedido pode estar percorrendo-o. Cada pedido lê o snapshot uma vez e anda nele.

Força bruta: quem conta é o autenticador

O que vale como tentativa é o autenticador que diz (AttemptOf, que devolve TRALAuthAttempt): segredo errado conta contra o endereço, certo zera a contagem, e nenhum dos dois quando não veio credencial (não é tentativa) ou o token venceu (ninguém adivinha token).

Plugins que olham a resposta pronta (operação)

Não há fase depois do módulo. Log, métricas, rastreio e cache registram, na passagem de antes do módulo, um aviso no pedido (TRALRequest.AddFinishHandler) ou na resposta (TRALResponse.AddWireHandler, que roda uma vez no início de TakeWireStream/ TakeWireString, antes de o corpo sair). O pedido guarda o que o fim precisa: StartTick (posto em ValidateRequest), ResponseStatus e BytesSent (postos ao entregar a resposta ao motor; -1 numa resposta em fluxo) e TraceId.

Três restrições moldaram o código:

  • Os parâmetros do pedido somem antes do fim. ProcessCommands limpa ARequest.Params ao terminar. Cabeçalhos, agente, usuário e corpo que o log quer, e a chave, o TTL e o If-None-Match que o cache quer, são copiados na passagem de antes (o log num retrato por pedido; o cache num objeto por pedido cujo aviso de resposta faz o trabalho e cujo aviso de fim o libera).
  • O aviso não pode ser método do plugin. Um formulário libera os componentes em qualquer ordem, e um pedido pode terminar depois de o plugin ter ido embora. O aviso é método de um objeto com contagem de referência (TRALLogSink, TRALMetricsRegistry, TRALTraceCollector): o plugin e cada pedido em curso seguram uma referência, e o destrutor do plugin só desliga o objeto (não escreve mais) e solta a sua.
  • Uma resposta abaixo de 400 em ppValidate não encerra o pedido. Só >= 400 encerra ali; por isso o redirecionamento para HTTPS responde em ppProcess com AHandled := True, e o cache também é ppProcess.

O corpo de texto de uma resposta não é um parâmetro ral_body: quem lê a resposta pronta usa AResponse.Params.SingleBody.

CORS

  • O texto de AllowHeaders é montado quando a lista muda e publicado como snapshot (TRALSnapshots): um pedido que lê enquanto a lista muda nunca fica com um texto sendo liberado. Os itens são juntados por vírgula como estão: o DelimitedText poria entre aspas um item com espaço ou aspas, e o cabeçalho sairia inutilizável.
  • Os nomes e valores fixos dos cabeçalhos são globais preenchidas no initialization: no Delphi, um literal atribuído a um param vira uma cópia nova a cada pedido.

Concorrência

  • Sob carga, os pedidos disputam a CPU e o gerenciador de memória: passado um ponto, cada pedido a mais deixa todos mais lentos. Ter um limite importa mais que o valor dele (4, 8 e 16 deram quase o mesmo).
  • O pedido pega a vaga antes de decodificar o corpo (ppValidate, depois de todo plugin que recusa ali, para um pedido recusado nunca esperar) e devolve quando a resposta está pronta (codificada, comprimida, cifrada) e antes de o motor enviar: um download grande para um cliente lento não segura vaga. Esse momento é TRALRequest.Finish, que todo motor chama depois do TakeWireStream; o destrutor do pedido chama se uma exceção pulou.
  • O portão (TRALConcurrencyGate) fica separado do plugin e é contado por referência: um pedido segura a vaga por mais tempo do que o plugin tem certeza de viver (um formulário libera os componentes em qualquer ordem). O plugin tem uma referência, cada vaga e cada pedido na fila outra; a última libera.
  • Uma rota que chama o próprio servidor espera vaga segurando uma: com todas as vagas presas assim, nada libera. Por isso MaxQueueWait não é infinito por padrão.

Proteções (RALSecurity)

  • A poda das listas (PruneIdle) tira as entradas ociosas numa só tomada da trava da lista, com os objetos: andar a lista pegando a trava por elemento correria contra outra thread (índice já removido, ou a entrada viva que escorregou para ele).
  • Força bruta: só um segredo aceito zera a contagem. Zerar a caminho de qualquer rota que respondesse deixaria um adivinhador alternar senha errada com uma rota aberta e nunca chegar a MaxTry. O veredito do autenticador fica como veio: um 403 continua 403.
  • A verificação e a inserção na lista ficam sob a mesma trava: duas threads que não acham nada e inserem perdem uma contagem, e a tentativa que passaria de MaxTry não passa. A expiração conta da última tentativa errada.
  • Flood: um endereço visto pela primeira vez não tem intervalo para medir. Uma recusa de flood é taxa, não palpite: 403 e OnClientBlock, mas não conta para a força bruta. Entrada ociosa por um minuto (ou um múltiplo folgado do intervalo) sai da lista: guardá-la faria de uma varredura de origens aleatórias um vazamento.

JWT (RALAuthentication)

  • Os eventos de token são of object: OnGetToken decide quem recebe token e OnValidate se um ainda vale, e os dois precisam chegar a um banco, isto é, ao objeto que tem a conexão. Para procedimento simples há as variantes ...Gen.
  • A rota de token, nesta ordem: credenciais (ou nenhum token) vão para OnGetToken, mesmo com um Bearer (senão o próximo usuário de uma máquina compartilhada renovaria o token do anterior); um Bearer sozinho se renova com OnRenewToken, e recusado é 401 e nada mais (não tenta uma senha ao lado de um token morto, e não conta como login errado: um navegador com token vencido manda um a cada renovação; com UseCookie ele é mandado apagar o cookie); sem nenhum dos dois, 401: sem OnGetToken ninguém confere quem pede.
  • O cookie: o cabeçalho Cookie traz todos os cookies do site, em qualquer ordem; só o raltoken é o token. O motor separa os cookies em params rpkCOOKIE; o cabeçalho cru é o recurso de quem o manteve inteiro.
  • Cliente: num 401, o token só é descartado se for o mesmo com que o pedido foi recusado (comparado sob a trava): um autenticador serve muitos clientes, e os 401 dos pedidos em voo chegam depois de um deles já ter buscado o próximo token.
  • SetToken não é atribuição: divide o token, decodifica e reescreve FPayload (um objeto). Tudo sob Lock, e nada é publicado antes de se saber que é bom: limpar FToken antes faria cada outra thread ver IsAuthenticated = False e buscar um token próprio.

Digest

  • O servidor não guarda estado por nonce: o nonce é o próprio carimbo de tempo mais um HMAC dele com um segredo do plugin. Todo nonce emitido é conferido recalculando, e um velho ainda é reconhecido como do servidor: é isso que deixa responder stale=true (credenciais certas, nonce velho) e o cliente renovar sem contar falha. O único estado é o cache de repetição opcional: o último nc visto por nonce, podado quando os nonces vencem.
  • O cliente lê o desafio do 401 do próprio pedido (HandleChallenge), sem pedido extra para saber o nonce, e conta o nc sob a trava do autenticador (um autenticador serve vários clientes).
  • qop=auth-int (hash do corpo) não é oferecido: o servidor precisaria do corpo como veio do fio, antes de decodificar, e cada motor o entrega num ponto diferente.

OAuth2

  • TRALServerOAuth2 são duas coisas, cada uma desligável: o servidor de autorização (Issuing: endpoint de token com client_credentials, refresh_token e authorization_code com PKCE S256 sempre exigido, introspecção RFC 7662, revogação RFC 7009, JWK set e metadados RFC 8414; os tokens são JWT, HS* com SignSecretKey, RS*/ES* com PrivateKey pela OpenSSL) e o servidor de recursos (Validate: os próprios tokens, os de outro emissor pelo JWK set ou opacos por introspecção, e o escopo de cada rota).
  • Clientes, segredos, usuários e consentimento são da aplicação, por eventos: o plugin nunca decide quem alguém é. Códigos, refresh tokens e ids revogados ficam em Store (memória por padrão).
  • Sem o grant password (RFC 9700 e OAuth 2.1 o tiram; o /gettoken do JWT cobre o caso), sem fluxo implícito, DPoP ou mTLS.

Certificado automático (RALSelfSigned)

  • Ligado a um servidor, olha o certificado que o motor recebeu ao ligar: de uma CA, deixa e não vigia; autoassinado válido, usa e vigia; autoassinado vencido ou a vencer em RenewBeforeDays, renova (chave e certificado novos com os mesmos nomes, gravados onde estava o antigo); nenhum, faz o próprio em Folder e aponta o motor para ele.
  • O certificado novo chega ao motor rodando por TRALServer.SetTLSCertificate, trocado sob o listener: as conexões abertas ficam com o antigo, os próximos handshakes pegam o novo. O Sagui é a exceção: a libsagui só aceita certificado ao começar a escutar, e reabre o listener.
  • A chave fica sem senha em Folder, como openssl req -nodes: quem protege é a pasta. O padrão fica no perfil do usuário, não ao lado do executável, onde um módulo web que serve arquivos a exporia. Tudo é Pascal (RALRSA, RALX509): nenhuma biblioteca para fazer o certificado, só a que cada motor já carrega para servi-lo.

Clone this wiki locally