CASE TÉCNICO / CONQUISTA LENDÁRIA

Evolução de um Servidor WebSocket para Padrão Profissional

Reestruturação completa de um servidor WebSocket server-side em Delphi: de uma implementação simples e limitada para uma biblioteca modular, segura, com 61 testes automatizados e totalmente compatível com a RFC 6455 — parsing byte a byte, sem modo "automático" de biblioteca nenhuma.

Delphi WebSocket RFC 6455 TCP/IP DUnitX Atecsoftwares · 2026

O desafio

A versão original (V1) funcionava apenas em condições perfeitas: o handshake aceitava praticamente qualquer requisição, a leitura de frames só entendia um caso específico do protocolo, e não havia validação de UTF-8, suporte a fragmentação nem tratamento de PING/PONG/CLOSE. Faltavam requisitos essenciais de produção:

A arquitetura da solução

A V1 vivia inteira dentro de uma única unit. A versão atual é uma biblioteca modular de 5 units, cada uma com responsabilidade única — infraestrutura pura, agnóstica de negócio:

WebSocket/
├── WebSocketServer.pas     Servidor (sobre Indy TCP) + API pública Ler/EnviarWS*
├── WebSocketHandshake.pas  Upgrade HTTP fortificado + telemetria
├── WebSocketFrames.pas     Parser/writer de frames — TEXT/BINARY/PING/PONG/CLOSE
├── WebSocketProtocol.pas   Constantes da RFC (opcodes, close codes, limites) + validadores
└── UTF8Automato.pas        DFA de Höhrmann para validação UTF-8 (RFC 3629)

Cada cliente é atendido em sua própria thread; timeout, keepalive e desconexão silenciosa devolvem o controle ao consumidor sem exceção — exceção significa protocolo violado, e nesse caso a lib já enviou o frame CLOSE com o status code correto antes de lançá-la. Quem consome a lib não sabe o que é um frame: só chama LerWSString / EnviarWSString.

O que foi feito

01

Handshake fortificado

Leitura fragmentada com limites rígidos (4 KB · 2 s · 20 fragmentos · 50 headers), headers proibidos bloqueados, chave validada byte a byte, resposta 426 para versão errada e negociação de subprotocolo/extensão conforme a §4.2.2.

02

Parser completo de frames

Validação de FIN, RSV e opcode, máscara obrigatória, comprimentos 7/16/64 bits com codificação mínima exigida, limite de 2 MB — e cada violação fecha a conexão com o status code correto da RFC (1002, 1007, 1009).

03

Protocolo integral

PONG automático ecoando o payload do PING — inclusive no meio de mensagem fragmentada. CLOSE validado (código e reason) e ecoado. Fragmentação suportada na leitura, com continuation e control frames intercalados.

04

UTF-8 por autômato (DFA)

Validador próprio com o DFA de Bjoern Höhrmann: O(n), 2 indexações de tabela por byte, zero branches, tabela de 400 bytes. Rejeita overlong, surrogates, codepoints > U+10FFFF e truncamentos — CLOSE 1007.

05

Observabilidade profissional

Logs estruturados por estágio ([SYS/WS-HS], [RX/WS-FRAME], [TX/WS-PONG]…) e telemetria do handshake — tempo até o 1º byte, duração, bytes e fragmentos, base para detectar ataques slowloris.

06

61 testes automatizados

13 testes unitários do autômato UTF-8 e 48 de integração em loopback real: um cliente TCP cru monta handshakes e frames byte a byte, e cada violação é conferida lendo o CLOSE devolvido no socket. O Sec-WebSocket-Accept é validado contra o vetor da própria RFC (§1.3).

Antes × Depois

Aspecto Antes (V1) Depois (V2)
Arquiteturatudo em uma unit5 units modulares
Segurançainexistentemúltiplas camadas + limites
RFC 6455parcialcompleta
Handshakebásicovalidado, protegido, com telemetria
Framesinterpretação erradaparser completo (7/16/64 bits)
Fragmentaçãonão suportadacontinuation + control intercalado
PING/PONG/CLOSEnão suportadoimplementado com eco
UTF-8sem validaçãoDFA de Höhrmann (1007)
Logsnenhumestruturados por estágio
Telemetriainexistentedetalhada (anti-slowloris)
Testesnenhum61 (unitários + loopback)
Robustezbaixaalta

Código: a evolução na prática

Trechos que demonstram a evolução técnica da V1 para a versão profissional. Os exemplos V2 são código real da lib em produção.

V1 Handshake simplificado

if ParsedHeaders.ContainsKey('Upgrade') and (ParsedHeaders['Upgrade'] = 'websocket') then
begin
  SecWebSocketKey := ParsedHeaders['Sec-WebSocket-Key'];
  Hash := TIdEncoderMIME.EncodeBytes(
    HashSHA1.HashString(SecWebSocketKey + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'));

  c.Write('HTTP/1.1 101 Switching Protocols'#13#10 +
          'Upgrade: websocket'#13#10 +
          'Connection: Upgrade'#13#10 +
          'Sec-WebSocket-Accept: ' + Hash + #13#10#13#10);

  c.Tag := 1;
end;

V2 Handshake fortificado com telemetria

// Leitura fragmentada com limites rígidos: 4 KB, 2 s, 20 fragmentos
if ManipuladorIO.InputBuffer.Size > 0 then begin
  Inc(QtdeFragmentos);

  if QtdeFragmentos > QTDE_MAX_FRAGMENTOS then
    raise TInnerWSException.CreateComInfo('Fragmentação excessiva detectada no handshake', 'Handshake');
  ...
end;

if ConstrutorRaw.Length > TAM_MAX_HANDSHAKE then
  raise TInnerWSException.CreateComInfo('Handshake maior que limite seguro (4KB). Possível ataque.', 'Handshake');

// Validação estrita da request line
if not SameText(Metodo, 'GET') then
  raise TInnerWSException.CreateComInfo('Método inválido no handshake (esperado GET)', 'Handshake');

if Pos(#0, CaminhoReq) > 0 then
  raise TInnerWSException.CreateComInfo('Caracter NUL no path', 'Handshake');

// Telemetria: base para detectar slowloris e fragmentação agressiva
TLogging.Escrever(Format('[SYS/WS-HS-TM] Telemetria: PrimeiroByte=%d ms | Total=%d ms | Bytes=%d | Fragmentos=%d',
  [DuracaoPrimeiroByte, DuracaoTotal, TotalBytesLidos, QtdeFragmentos]), nlInfo, '0', 'WS');

V1 Leitura de frames (parcial e incorreta)

if ReadByte = $81 then
begin
  l := ReadByte;
  DecodedSize := l - 128;

  Mask[0] := ReadByte;
  Mask[1] := ReadByte;
  Mask[2] := ReadByte;
  Mask[3] := ReadByte;

  inherited ReadBytes(TIdBytes(result), DecodedSize, False);
end;

V2 Decoder completo e seguro

Byte1 := ManipuladorIO.ReadByte;
Fin := (Byte1 and $80) <> 0;

// RSV bits devem ser 0 — extensões não suportadas (§5.2)
if (Byte1 and $70) <> 0 then
  FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Extensões WebSocket não suportadas');

CodigoOp := Byte1 and $0F;
Byte2 := ManipuladorIO.ReadByte;

// Cliente DEVE mascarar (§5.1)
if (Byte2 and $80) = 0 then
  FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Frame sem máscara (cliente deve mascarar)');

TamBase := Byte2 and $7F;

case TamBase of
  126: begin
    BytesExt := LerNBytes(ManipuladorIO, 2);
    TamPayload := (UInt64(BytesExt[0]) shl 8) or UInt64(BytesExt[1]);

    // RFC exige codificação mínima: length < 126 não pode usar formato estendido
    if TamPayload < 126 then
      FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Payload length com codificação estendida não mínima');
  end;
  127: begin
    BytesExt := LerNBytes(ManipuladorIO, 8);

    if (BytesExt[0] and $80) <> 0 then
      FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Payload length de 64 bits inválido');

    for J := 0 to 7 do
      TamPayload := (TamPayload shl 8) or UInt64(BytesExt[J]);

    if TamPayload < 65536 then
      FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Payload length com codificação estendida não mínima');
  end;
else
  TamPayload := TamBase;
end;

if TamPayload > WS_TAM_MAX_FRAME then
  FalharWebSocket(WS_CLOSE_MENSAGEM_GRANDE, 'Frame maior que limite permitido');

// Control frames: nunca fragmentados, payload ≤ 125 bytes (§5.5)
if CodigoOp in [WS_OPCODE_CLOSE, WS_OPCODE_PING, WS_OPCODE_PONG] then begin
  if not Fin then FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Control frame fragmentado é inválido');
  if TamPayload > WS_TAM_MAX_PING then FalharWebSocket(WS_CLOSE_PROTOCOLO, 'Control frame maior que limite (RFC: 125 bytes)');
end;

V2 Tratamento profissional de PING/PONG

if CodigoOp = WS_OPCODE_PING then begin
  TLogging.Escrever('[RX/WS-PING] PING recebido', nlInfo, '0', 'WS');

  Dados := LerPayloadDesmascarado(TamPayload);

  ManipuladorIO.Write(Byte($8A));           // opcode PONG (§5.5.3)
  ManipuladorIO.Write(Byte(TamPayload));

  if TamPayload > 0 then ManipuladorIO.Write(Dados);  // eco do payload

  // PING no meio de mensagem fragmentada? A leitura continua de onde parou
  if MensagemFragmentada then Continue;
  Exit;
end;

V2 Validação UTF-8 sem branches (DFA de Höhrmann)

// 2 indexações de tabela por byte. Sem if/else. O(n).
function TUTF8Automato.PushByte(B: Byte): Boolean;
var
  TypeVal: Cardinal;
begin
  TypeVal := UTF8d[B];                            // classifica o byte
  FState := UTF8d[256 + FState * 16 + TypeVal];   // transição da DFA
  Result := (FState <> UTF8_REJECT);
end;

// Uso no pipeline: frame TEXT que termina "pendente" é truncamento → CLOSE 1007
UTF8.Reset;
if not (UTF8.PushBuffer(Dados) and UTF8.IsValidFinal) then
  FalharWebSocket(WS_CLOSE_UTF8_INVALIDO, 'Payload UTF-8 inválido ou truncado');

Números do projeto

Métrica Valor
Units modulares5 — server, handshake, frames, protocolo, UTF-8
Testes automatizados61 — 13 unitários + 48 integração em loopback
Close codes implementados1002 · 1003 · 1007 · 1009 (+ faixa validada da §7.4)
Limite de frame/mensagem2 MB, com codificação mínima obrigatória
Validação UTF-8O(n), tabela de 400 bytes, zero branches
Proteções de handshake4 KB · 2 s · 20 fragmentos · 50 headers · headers proibidos

Resultado

O sistema evoluiu de um protótipo que "funcionava quando dava" para uma biblioteca de infraestrutura de nível profissional, que hoje sustenta a comunicação com dispositivos embarcados reais em produção — incluindo terminais de reconhecimento facial que se conectam via WebSocket. Essa evolução demonstra:

Quer conversar sobre os detalhes?

Caso tenha interesse em entender o código completo ou detalhes técnicos adicionais, estou disponível para conversar sobre o projeto.

jhonathan93.dev@gmail.com