Pular para o conteúdo

Papers

The WebSocket Protocol

Ian Fette, Alexey Melnikov · IETF RFC 6455, 2011

O problema: simular comunicação bidirecional em cima de HTTP

O RFC parte de uma observação prática. Aplicações web que precisavam trocar dados nos dois sentidos entre navegador e servidor vinham entortando o HTTP para isso. A abordagem usual era fazer polling repetido no servidor (o long polling é uma variante) e enviar cada mensagem no sentido cliente-servidor como uma chamada HTTP separada. O documento lista os custos dessa abordagem. O servidor precisa usar várias conexões TCP por cliente: uma para enviar informação ao cliente e uma nova para cada mensagem que chega do cliente. Cada mensagem do cliente para o servidor carrega o overhead de um conjunto completo de cabeçalhos HTTP. E o script do cliente precisa fazer o trabalho de casar cada requisição enviada com a resposta recebida que corresponde a ela. O RFC apresenta o WebSocket como alternativa mais simples: uma única conexão TCP que carrega o tráfego nos dois sentidos. Nas palavras do próprio documento, o protocolo permite comunicação bidirecional entre um cliente que roda código não confiável num ambiente controlado e um host remoto que optou por receber comunicações desse código. Ele reaproveita o modelo de segurança baseado em origin que os navegadores já aplicam, e consiste em um handshake de abertura seguido de uma camada fina de framing de mensagens sobre o TCP. O objetivo declarado é oferecer às aplicações de navegador comunicação bidirecional com servidores sem depender de abrir várias conexões HTTP (como no long polling). O RFC o apresenta como substituto das técnicas bidirecionais que usam HTTP como transporte.

O handshake de abertura: uma requisição HTTP que faz upgrade da conexão

Uma conexão WebSocket nasce como uma requisição GET comum, em HTTP/1.1 ou superior, o que permite que ela atravesse infraestrutura que já entende HTTP. O cliente envia cabeçalhos dizendo que quer trocar de protocolo: Upgrade com o valor websocket, Connection com o valor Upgrade, Sec-WebSocket-Version 13 e um Sec-WebSocket-Key com um valor aleatório em base64 (que, decodificado, tem 16 bytes). Clientes navegadores também enviam obrigatoriamente um cabeçalho Origin; para os demais, ele é opcional. O cliente pode ainda listar subprotocolos que sabe falar. Se o servidor aceita, responde com status 101 Switching Protocols e seus próprios cabeçalhos Upgrade e Connection. Ele acrescenta ainda o Sec-WebSocket-Accept, que calcula concatenando um GUID fixo, definido no RFC, à chave do cliente, aplicando SHA-1 ao resultado e codificando o digest em base64. O cliente confere esse valor, o que mostra que o servidor realmente entendeu o handshake WebSocket. Depois disso, a mesma conexão TCP passa a carregar frames WebSocket em vez de HTTP. O servidor também pode recusar: pode responder 400 para uma requisição malformada, 403 para uma origin que não permite, ou 426 Upgrade Required junto com um cabeçalho Sec-WebSocket-Version listando as versões que suporta. Se o servidor escolhe um dos subprotocolos oferecidos, ele indica a escolha no cabeçalho de resposta Sec-WebSocket-Protocol.

Framing e masking: mensagens, fragmentos e máscaras do cliente para o servidor

Depois do handshake, os dados trafegam em frames. Cada frame tem um bit FIN que marca o último fragmento de uma mensagem, três bits reservados que devem ficar em zero a menos que uma extensão os defina, e um opcode de 4 bits. Os opcodes incluem 0x1 para texto, 0x2 para binário e 0x0 para a continuação de uma mensagem fragmentada; 0x8, 0x9 e 0xA são os frames de controle tratados na próxima seção. O tamanho do payload usa uma codificação variável: valores de 0 a 125 são o próprio tamanho, 126 significa que os próximos dois bytes trazem um tamanho de 16 bits, e 127 significa que os próximos oito bytes trazem um tamanho de 64 bits. Deve-se usar a codificação mais curta possível. Uma mensagem pode ser dividida em vários frames: o primeiro tem FIN zerado e opcode diferente de zero, os do meio usam opcode 0 com FIN zerado, e o último usa opcode 0 com FIN ligado. Sem extensões, a mensagem equivale a uma única mensagem cujo payload é a concatenação, em ordem, dos payloads dos fragmentos. Os fragmentos devem chegar em ordem e não podem ser intercalados com fragmentos de outra mensagem, a menos que se tenha negociado uma extensão capaz de interpretar a intercalação; frames de controle podem aparecer entre eles. Todo frame enviado do cliente para o servidor deve ser mascarado: bit MASK igual a 1 e uma chave de 4 bytes, com cada byte do payload passando por XOR com o byte da chave na posição i mod 4. Servidores não devem mascarar seus frames. Um servidor que recebe um frame sem máscara deve fechar a conexão, e um cliente que recebe um frame mascarado deve fazer o mesmo.

Frames de controle: ping, pong e o handshake de fechamento

O protocolo define três frames de controle. Todos têm payload limitado a 125 bytes ou menos e nenhum pode ser fragmentado; além disso, podem ser encaixados no meio de uma mensagem fragmentada. Um Ping pode carregar dados da aplicação. Quando um Ping chega, o receptor deve responder com um Pong que repete os mesmos dados, a menos que já tenha recebido um Close. Um Pong também pode ser enviado sem solicitação, e nenhuma resposta é esperada para ele. Um Ping pode ser enviado a qualquer momento entre o estabelecimento e o fechamento da conexão, e o RFC indica que ele pode servir como keepalive ou como forma de verificar se o endpoint remoto continua respondendo. O fechamento também é feito por mensagem. Um endpoint que quer encerrar a conexão envia um frame Close. O corpo é opcional; se existir, deve começar com um código de status de dois bytes, opcionalmente seguido de texto UTF-8 explicando o motivo. Depois de enviar o Close, a aplicação não deve enviar mais frames de dados, e o par deve responder com o seu próprio Close caso ainda não tenha enviado um. Quando os dois lados enviaram e receberam Close, cada um considera a conexão fechada. O servidor deve fechar o TCP imediatamente; o cliente deve, em regra, esperar que o servidor feche primeiro, mas pode fechar a qualquer momento depois disso, por exemplo se o servidor demorar. Entre os códigos de status definidos estão 1000 (fechamento normal), 1001 (endpoint saindo, como um servidor sendo desligado ou um navegador deixando a página), 1002 (erro de protocolo), 1003 (tipo de dado não suportado) e 1009 (mensagem grande demais).

Modelo de segurança: checagem de origin, masking contra ataques a proxies e TLS

O RFC apoia a segurança em três ideias separadas. Primeiro, o modelo de origin. Os navegadores sempre enviam um cabeçalho Origin no handshake. Um servidor que quer atender apenas certos sites deve, em regra, comparar esse cabeçalho com as origins esperadas e, se a origin não for aceitável, responder ao handshake com 403 Forbidden. O RFC é explícito sobre o limite dessa checagem: o objetivo dela é impedir scripts maliciosos rodando em um navegador confiável. Um cliente que não é navegador pode colocar qualquer string em Origin, então o servidor não deve tratar o cabeçalho como prova de quem está chamando e não deve confiar que nenhuma entrada seja válida. Segundo, o masking. O RFC descreve um ataque em que um script usa uma conexão com upgrade para emitir bytes que parecem uma requisição HTTP, fazendo um proxy intermediário guardar em cache uma resposta envenenada para outros usuários. Mascarar todo frame do cliente para o servidor com uma chave nova e imprevisível impede que o script controle os bytes que vão pela rede. Se a próxima chave fosse previsível, o atacante poderia mascarar antecipadamente a mensagem com ela, e o mascaramento feito pelo cliente a desfaria, deixando na rede algo que parece uma requisição HTTP. Terceiro, confidencialidade e integridade vêm de rodar o protocolo sobre TLS usando URIs wss, e as implementações devem suportar TLS. O protocolo não define autenticação de cliente própria; o servidor pode usar o que um servidor HTTP comum usaria, como cookies, autenticação HTTP ou autenticação TLS. Implementações com limites próprios de tamanho de frame ou de mensagem remontada devem se proteger contra quem os exceda e, em regra, impor um limite a esses tamanhos.

Veja as ideias em ação

Pratique na escola de System Design os desenhos que usam este paper.