HTTP headers & cookies

Um breve exemplo com explicações elementares

Nosso servidor HTTP, até o momento, responde apenas um texto simples "Hello".

require 'socket'

socket = TCPServer.new(3000)
puts 'Listening to the port 3000...'

loop do
  client = socket.accept

  response =
"""
HTTP/1.1 200\r\n
\r\n
\r\n
Hello
"""

  client.puts(response.strip.gsub(/\n+/, "\n"))
  client.close
end

Ao acessarmos http://localhost:3000 no navegador, o resultado é este:

Entretanto o conteúdo apresentado é um texto simples. Se decidirmos escrever uma página inteira apenas com texto simples, seria irritante para o usuário. Por isto, precisamos formatar o conteúdo, marcar algumas partes com destaque e prover uma experiência melhor ao usuário do site.

Um pouco de HTML

HTML é uma linguagem de marcação para conteúdo hypertexto, que pode ter diferentes características de acordo com a marcação desejada.

Vamos supor que queremos responder com conteúdo HTML um contador de quantas vezes uma mesma pessoa visita a página. Vamos alterar a resposta HTTP para conter o corpo da mensagem em formato HTML:

# ...
  response =
"""
HTTP/1.1 200\r\n
\r\n
\r\n
<h1>Counter: 1</h1> 
"""
# ...

O texto com o counter (<h1>Counter: 1</h1>) deve ser destacado em forma de título na página. Ao entrarmos no site:

O conteúdo foi mostrado exatamente da forma como enviamos no socket, pois tudo é string de dados sendo transportada via socket TCP.

HTTP headers

Mas como fazer o navegador "interpretar" aquele conteúdo HTTP como sendo HTML? Devemos enviar este "metadado" em uma parte especial da mensagem HTTP, que se chama HTTP header.

# ...
  response =
"""
HTTP/1.1 200\r\n
Content-Type: text/html\r\n
\r\n
<h1>Counter: 1</h1> 
"""
# ...

Desta forma, estamos instruindo o HTTP client, no caso o web browser, que o conteúdo é do tipo HTML, assim o browser consegue renderizar o HTML corretamente:

Ok, mas o counter está sempre fixo no valor "1". Como podemos deixar isto dinâmico?

# ...
counter = 1       # <-- como fazer o counter ser dinâmico a ponto de ser enviado entre browser e server diversas vezes? 

  response =
"""
HTTP/1.1 200\r\n
Content-Type: text/html\r\n
\r\n
<h1>Counter: #{counter}</h1> 
"""
# ...

HTTP é stateless por definição

Vamos lembrar que, por estar condicionado a uma conexão TCP, o HTTP não guarda estado, isto é, a cada vez que um user fizer refresh à página, o server não consegue saber, a princípio, quem é o cliente, sempre tratando o pedido como se fosse um novo cliente.

# (...)
loop do
  client = socket.accept                        # <-- aguarda até chegar novo cliente

  client.puts("HTTP/1.1  (etc ....)")       # <-- envia resposta ao cliente
  client.close                                          # <-- fecha conexão com cliente e o ciclo de aguardo inicia novamente
end
# (...)

Para que o cliente seja "lembrado", é preciso que o server envie algum metadado ao cliente, para que este reencaminhe o metadado de volta ao server nos pedidos subsequentes.

A especificação HTTP contempla este cenário onde podemos ter algum tipo de "estado" entre conexões HTTP distintas de um mesmo cliente, sendo que web browsers e servidores web já implementam este envio mútuo de metadado de forma automática.

Já sabemos que para enviar um metadado, é através de HTTP headers, como vimos no exemplo do Content-Type.

Para o caso do "counter" ser enviado entre vários pedidos, o HTTP especifica o envio deste metadado através de headers que são HTTP Cookies.

HTTP Cookies

  1. o server envia um metadado qualquer através do header Set-Cookie

  2. o browser recebe a mensagem e verifica que há um header Set-Cookie, então pega o valor do cookie e coloca numa área específica da memória do navegador chamada cookie storage.

  3. para pedidos futuros neste mesmo site, o browser já sabe que tem que enviar o cookie de volta ao servidor, portanto, no request HTTP, inclui um header chamado Cookie com o valor armazenado.

  4. o server verifica se o browser enviou algum header Cookie, e caso tenha sido enviado, lê o valor do cookie para manipular/atualizar a informação de alguma forma e o ciclo se repete

Como funciona um típico sistema de Login na Web

Convém lembrar que um sistema de login web funciona exatamente desta forma, onde a primeira resposta do servidor é através de Set-Cookie que contém a identificação do user ou algo do gênero. Desta forma, o browser envia o cookie nos pedidos subsequentes e com isto temos um sistema de login onde temos a impressão que estamos "autenticados".

"Autenticados" com muitas aspas. É apenas uma sensação, pois a informação é sempre trocada através de headers em cima de um protocolo que NÃO guarda estado por definicão.

Para enviar um metadado ao cliente, já vimos que é preciso utilizar HTTP headers. Neste caso não será diferente. E a especificação HTTP contempla um header chamado Set-Cookie que permite "persis

Voltando ao nosso exemplo do counter

Vamos então ver como fica o response HTTP com o header Set-Cookie enviando o valor do counter:

# ...
counter = 1 

  response =
"""
HTTP/1.1 200\r\n
Content-Type: text/html\r\n
Set-Cookie: counter=#{counter}; path=/; HttpOnly\r\n
\r\n
<h1>Counter: #{counter}</h1> 
"""
# ...

A resposta HTTP que chega ao browser é esta:

Pelo que o browser guarda o cookie no cookie storage do próprio browser, ou seja, fica na memória do browser:

Por conta disto, se o user apagar as cookies, o próximo pedido ao server não vai ser o valor no header portanto para o server será como se fosse a "primeira vez" daquele cliente.

Vamos ver como fica o request HTTP enviado do browser ao servidor:

Nesta imagem, podemos ver que há diversos HTTP headers que o browser envia ao server, dentre eles o nosso cookie:

Cookie: counter=1

Yay!

Apesar de que conseguimos enviar do server ao browser, nosso server ainda não é capaz de ler os headers da mensagem, pois ainda não escrevemos este código.

Para isto, precisamos ler a mensagem através do socket TCP, uma linha de cada vez no socket:

client = socket.accept

first_line = client.gets  
second_line = client.gets

# and so on...

Já deu pra entender quantas linhas teríamos que escrever para ler a mensagem toda, pois não? Vamos então fazer um loop e ler todas as linhas enquanto houver mensagem no socket:

request = ''

while line = client.gets
  break if line == "\r\n"

  request +=  line
end

Só isto não basta, temos que agora conseguir encontrar um padrão Cookie: <qualquerChave>:<qualquerValor> no meio da mensagem. Para encontrar padrões, vamos utilizar expressões regulares:

cookie = {}

if cookie_match = request.match(/Cookie:\s(.*)=(.*)\r$/)
  cookie[cookie_match[1]] = cookie_match[2]
end

Vamos pular explicação de expressões regulares por agora, pode ser tema para outra sessão. Mas com este código conseguimos guardar numa hash todos os cookies vindo do request HTTP.

Próxima linha é buscar o valor do counter na hash cookie, caso esteja ausente (primeiro request de um cliente, por exemplo), o valor é 0. Caso contrário, é o valor encontrado no cookie.

A este valor, incrementamos o valor 1, dando assim a característica de um counter:

counter = cookie.fetch('counter', 0).to_i + 1

E pra finalizar, nosso response com os devidos headers:

  response =
"""
HTTP/1.1 200\r\n
Content-Type: text/html\r\n
Set-Cookie: counter=#{counter}; path=/; HttpOnly\r\n
\r\n
<h1>Counter: #{counter}</h1>
"""

Ao rodarmos o server e entrarmos 2 vezes na página:

Yay! Já temos nosso counter funcionando com HTTP cookies!

Conclusão

Esta sessão foi uma explicação de como funcionam HTTP headers, HTTP cookies e como podemos tirar proveito disto para envio mútuo de metadados para que sempre consigamos "lembrar" do cliente, mesmo utilizando um protocolo que não guarda estado.

Last updated