Departamento de Engenharia Eletrotécnica
REDES INTEGRADAS DE TELECOMUNICAÇÕES II
2019 / 2020
Mestrado Integrado em Engenharia Eletrotécnica
e de Computadores
4º ano
8º semestre
1º Trabalho prático:
Procurador HTTP
Versão 1.1
http://tele1.dee.fct.unl.pt Luis Bernardo
ÍNDICE
1. OBJETIVOS............................................................................................ 3
2. COMPLEMENTOS SOBRE HTTP .............................................................. 3
2.1.1. O método GET 4
2.1.2. O método CONNECT 7
2.2 Autenticação ................................................................... 7
2.3 Controlo de caching ........................................................ 8
3. COMPLEMENTOS SOBRE JAVA ................................................................ 9
3.1. IPv6 ................................................................................ 9
3.2 Sockets TCP ................................................................... 10
3.2.1. A classe ServerSocket 10
3.2.2. A classe Socket 11
3.2.3. Comunicação em sockets TCP 12
3.2.4. Exemplo de aplicação - TinyFileServ 12
3.2.5. Exemplo de cliente - HTTPClient 13
3.3 Strings ............................................................................. 15
3.3.1. Separação em componentes 15
3.3.2. Datas compatíveis com o protocolo HTTP 15
3.3.3. URLs 16
3.4 Ficheiros .......................................................................... 16
3.5 Estruturas de dados adicionais ...................................... 17
3.6 Descodificação em base64............................................. 17
4. ESPECIFICAÇÕES .................................................................................. 18
4.1 Especificação do funcionamento .................................... 18
4.2 Tarefas a realizar ............................................................ 18
4.2.1 Procurador básico inverso 19
4.2.2 Procurador básico direto 20
4.2.3 Autenticação de clientes do procurador 20
© Luis Bernardo 2
4.2.4 Reutilização da ligação browser-procurador 20
4.2.5 Caching de pedidos 20
4.3 Testes .............................................................................. 20
4.4 Desenvolvimento do trabalho ......................................... 21
Postura dos Alunos ............................................................... 21
DATAS LIMITE............................................................................................ 21
BIBLIOGRAFIA ............................................................................................ 21
© Luis Bernardo 3
1. OBJETIVOS
Familiarização com os protocolos Web e com a programação de aplicações
em Java baseadas em sockets TCP. O trabalho consiste no desenvolvimento de um
procurador Web de camada dupla (dual-stack) multi-tarefa que suporta HTTP. Este procurador
só deve realizar o comando GET e um subconjunto limitado das funcionalidades do protocolo
HTTP (relacionadas com o caching de páginas e autenticação), permitindo a sua realização num
número reduzido de horas. Pretende-se que o procurador Web satisfaça um conjunto de requisitos:
• Seja compatível com HTTP 1.1;
• Funcione como procurador Web direto ou inverso (reverse proxy);
• Suporte pedidos HTTP (GET);
• Guarde em disco uma página sempre que as regras o permitam, evitando novas
ligações ao servidor Web quando existirem novos pedidos da mesma página, mas que
seja capaz de verificar a frescura da página guardada;
• Opcionalmente, pretende-se que seja configurado um servidor Apache, para se
realizarem os testes na máquina local ao trabalho desenvolvido, e seja utilizado o
analisador de protocolos Wireshark, para validar a comunicação entre o procurador e
o servidor web.
Este trabalho complementa a aprendizagem sobre Java realizada em ST e RIT1,
acrescentando novas classes da biblioteca, introduzidas na terceira secção deste documento.
Antes, na segunda secção é apresentado um resumo das caraterísticas do protocolo HTTP
relevantes para este trabalho. Na quarta secção é apresentada a especificação completa do
procurador a realizar. É também apresentada uma descrição do projeto Netbeans que é fornecido
e das classes que deve completar.
2. COMPLEMENTOS SOBRE HTTP
Os protocolos HTTP 1.0 e HTTP 1.1 definem uma interação do tipo pedido-resposta entre
um cliente e um servidor, onde as mensagens trocadas contêm texto legível. Neste trabalho devem
ser suportadas duas versões do protocolo: HTTP 1.0 [RFC1945] e HTTP 1.1 [RFC2616]. A
mensagem de pedido, do cliente para o servidor tem a seguinte estrutura (sp = espaço ; cr lf =
“\r\n” = mudança de linha). O URL (Uniform Resource Locator) corresponde ao nome de um
ficheiro local (GET num servidor/procurador Web indireto) ou um URL completo (e.g.
http://www.fct.unl.pt/) num GET para um procurador direto:
No trabalho vai ser realizado o método GET1, destinado a ler documentos através de um
procurador indireto e indireto HTTP.
1 Embora não seja usado no trabalho, o protocolo HTTP 1.x também define o método CONNECT, destinado a criar
um túnel através de um procurador direto para pedidos HTTPS.
Método sp URL sp Versão cr lf
Nome campo cabeçalho : valor cr lf
Nome campo cabeçalho : valor cr lf
cr lf
Corpo da mensagem
Pedido
Linhas cabeçalho …
© Luis Bernardo 4
2.1.1. O método GET
Um pedido GET pode incluir diversas linhas de cabeçalhos opcionais2, que definem a data
de acesso ao servidor (Date), o nome do servidor acedido (Host) (obrigatório quando o servidor
HTTP suporta máquinas virtuais), informação de autenticação para o procurador (Proxy-
Authorization), informação de autenticação para o servidor (Authorization), lista de procuradores
visitados (Via), o número máximo de procuradores que podem ser atravessados (Max-Forwards),
o tipo de browser e sistema operativo usado (User-Agent), os formatos de dados suportados
(Accept), a língua pretendida (Accept-Language), as codificações suportadas (Accept-Encoding),
o formato de carater suportado (Accept-Charset), um teste se o ficheiro foi modificado desde
último acesso (If-Modified-Since e If-None-Match), para HTTP1.1 a indicação se pretende manter
a ligação aberta com o procurador (Proxy-Connection) ou com o servidor (Connection) e durante
quanto tempo (Keep-Alive), o tipo dos dados no corpo da mensagem (Content-Type), o número
de bytes da mensagem (Content-Length), dados de sessão (Cookie ou Cookie2), etc. Depois,
separando o cabeçalho de um corpo de mensagem opcional, existe uma linha em branco (“\r\n”).
Os vários modos de funcionamento de um procurador estão descritos no RFC 7230. No modo
direto (ou simplesmente, proxy), reencaminha o pedido memorizando o percurso, podendo
introduzir algumas transformações ao conteúdo. No modo inverso (ou, gateway), o procurador
pode esconder a identidade do cliente, funcionando como um servidor web normal para o browser.
Um exemplo de pedido a um procurador direto (mensagem 1 representada na figura) é:
GET http://tele1.dee.fct.unl.pt/page.html HTTP/1.1
Host: tele1.dee.fct.unl.pt
User-Agent: Mozilla/5.0 Gecko/20080208 Fedora/2.0.0.12
Accept-language: pt-pt,pt
Keep-Alive: 300
Proxy-Connection: keep-alive // ligação browser-proxy
O pedido HTTP gerado pelo procurador direto ao servidor Web (em tele1.dee.fct.unl.pt)
poderia ser (mensagem 2): GET /page.html HTTP/1.1
Host: tele1.dee.fct.unl.pt
User-Agent: Mozilla/5.0 Gecko/20080208 Fedora/2.0.0.12
Accept-language: pt-pt;pt
Max-Forwards: 10
Via: 1.1 Demo_RIT2
Connection: close // ligação proxy-servidor
Um procurador indireto difere do procurador direto porque esconde a identidade do cliente
inicial, substituindo todos os campos do pedido com a identidade do cliente (e.g. User-Agent)
por valores neutros.
No caso de um procurador inverso, o pedido na mensagem 1 seria
2 Pode consultar a lista exaustiva de cabeçalhos do HTTP 1.1 em https://www.w3.org/Protocols/rfc2616/rfc2616-
sec14.html
© Luis Bernardo 5
GET /page.html HTTP/1.1
Host: tele1.dee.fct.unl.pt
User-Agent: Mozilla/5.0 Gecko/20080208 Fedora/2.0.0.12
Accept-language: pt-pt,pt
Keep-Alive: 300
Connection: keep-alive // ligação browser-proxy
O pedido 1 é igual ao pedido realizado a um servidor web. Como se pretende desenvolver um
procurador inverso que esconde a identidade do utilizador, a mensagem 2 gerada pelo procurador
deve suprimir os dados pessoais3. Poderia ser:
GET /page.html HTTP/1.1
Host: tele1.dee.fct.unl.pt
User-Agent: RIT2 Proxy Demo
Accept-language: pt-pt;pt
Connection: close // ligação proxy-servidor
As mensagens de resposta têm uma estrutura semelhante aos pedidos:
O campo mais importante da resposta é o código de estado, que define o que se passou com
o pedido. Os códigos de estado devolvidos podem ser:
Código
Tipo Exemplo de razões
1xx Informação Recebido pedido, continua processamento
2xx Sucesso Ação terminada com sucesso
3xx Redirecção Necessárias mais ações para completar
4xx Erro do cliente Pedido errado, não pode ser executado
5xx Erro do servidor Servidor falhou com pedido válido
Alguns exemplos de códigos de estado úteis para o trabalho são:
• 200 OK: Sucesso - informação retornada no corpo da mensagem;
• 301 Moved Permanently: Moveu-se para URL contido no campo de cabeçalho
'Location:';
• 304 Not Modified: Ficheiro não foi modificado;
• 400 Bad Request: Pedido não entendido pelo servidor;
• 401 Unauthorized: Dados de autenticação omissos ou inválidos para servidor;
• 403 Forbidden: O ficheiro pedido não pode ser acedido;
• 404 Not found: O ficheiro pedido não existe;
3 Recomenda-se que experimente com browsers reais, com o analisador de protocolos Wireshark, para
compreender quais os campos de cabeçalho mais relevantes.
Versão sp Código estado sp Frase cr lf
Nome campo cabeçalho : valor cr lf
Nome campo cabeçalho : valor cr lf
cr lf
Corpo da mensagem
Pedido
Linhas cabeçalho …
© Luis Bernardo 6
• 407 Proxy Authentication Required: Dados de autenticação omissos ou inválidos para
procurador;
• 501 Not implemented: Pedido não suportado pelo servidor.
As respostas incluem alguns cabeçalhos dos pedidos (Content-Type, Content-Length,
Connection, Proxy-Connection, Date, etc.), acrescentando outros opcionais específicos, como a
indicação da última modificação do ficheiro (Last-Modified), do valor de hash do ficheiro (ETag),
informação complementar sobre o pedido de informação de autenticação do servidor (WWW-
Authenticate) ou do procurador (Proxy-Authenticate), a definição de um estado no cliente (Set-
Cookie ou Set-Cookie2), e o controlo de cache (Cache-Control em HTTP 1.1 ou Pragma em
HTTP 1.0). Observe-se que o cabeçalho Content-Type é obrigatório sempre se devolverem dados,
indicando o tipo de dados MIME4, de forma ao browser saber como interpretar os dados. Ficheiros
HTML são codificados no formato “text/html”, enquanto ficheiros de imagens podem ser
codificados em “image/gif” ou “image/jpeg”, os dados binários como “application/octet-stream”,
etc. No caso de ficheiros HTML, também é importante o cabeçalho Content-Encoding, com o
formato de codificação. Recomenda-se que seja SEMPRE usado o formato "ISO-8859-1" em
todos os ficheiros de texto no procurador. Um exemplo de uma resposta de um servidor ao pedido
representado anteriormente do procurador é:
HTTP/1.1 200 OK
Date: Wed, 27 Feb 2020 12:00:00 GMT
Server: Apache/2.2.8 (Fedora)
Last-Modified: Thu, 28 Set 2019 19:00:00 GMT
ETag: “405990-4ac-4478e4405c6c0”
Content-Length: 6821
Connection: close
Content-Type: text/html; charset=ISO-8859-1
… { dados html } …
A resposta enviada pelo procurador (direto) ao browser a partir desta resposta poderia ser:
HTTP/1.1 200 OK
Date: Wed, 27 Feb 2008 12:00:00 GMT
Server: Apache/2.2.8 (Fedora)
Last-Modified: Thu, 28 Set 2003 19:00:00 GMT
ETag: “405990-4ac-4478e4405c6c0”
Content-Length: 6821
Content-Type: text/html; charset=ISO-8859-1
Cache-Control: public,max-age=600
Via: 1.1 Demo_RIT2
… { dados html } …
No protocolo HTTP 1.1 são usados os campos de cabeçalho (Connection e Keep-Alive) no
pedido a um servidor Web e na resposta para definir se a ligação TCP é fechada ou se se mantém
a ligação TCP aberta e por quanto tempo, após o pedido. Se tanto o pedido como a resposta
contiverem um campo de cabeçalho Connection com um valor diferente de close, a ligação TCP
não é desligada, podendo ser enviados vários pedidos através dessa ligação. Um exemplo de
valores para um cabeçalho de pedido para manter a ligação ao servidor durante 60 segundos é:
Connection: keep-alive
Keep-Alive: 60
4 Ver página 599 de [RFC2616] com lista de tipos
© Luis Bernardo 7
A resposta pode rejeitar (com Connection: close), ou definir um valor igual ou inferior ao
proposto (no caso 15 seg):
Connection: keep-alive
Keep-Alive: timeout=15, max=100
Relativamente a um procurador, a ligação é mantida aberta caso o pedido contenha um campo
de cabeçalho Proxy-Connection. Um exemplo de valores para um cabeçalho de pedido para
manter a ligação ao procurador durante 60 segundos seria:
Proxy-Connection: keep-alive
Keep-Alive: 60
Uma ligação HTTPS usa a mesma troca de mensagens que o protocolo HTTP [RFC2818],
exceto que esta troca ocorre sobre um canal cifrado SSL/TLS, em vez de ocorrer num canal aberto.
Como o campo de cabeçalho Keep-Alive não é usado por todos os browsers (e.g. Internet
Explorer), no trabalho não deve ser usado.
2.1.2. O método CONNECT
Outro modo de funcionamento de um procurador, não usado no trabalho, é como um túnel.
Um pedido CONNECT é específico de um procurador, e pede a criação de um túnel até um
servidor remoto. Para não quebrar a segurança na ligação SSL a um servidor remoto, o procurador
age como um túnel reenviando transparentemente todas as mensagens recebidas do browser ou
do servidor. Desta forma, toda a informação passa através do procurador cifrada.
Associado ao pedido CONNECT é comum enviar um subconjunto dos campos de cabeçalho
referidos na secção anterior, incluindo-se os campos User-Agent, Host, Proxy-Connection, e
Proxy-Authorization. Um exemplo de pedido num browser Firefox é:
CONNECT tele1.dee.fct.unl.pt:443 HTTP/1.1
User-Agent: Mozilla/5.0 Gecko/20080208 Fedora/2.0.0.12
Proxy-Connection: keep-alive
Host: tele1.dee.fct.unl.pt
Caso o pedido seja aceite, a resposta não inclui campos de cabeçalho, como está representado
a seguir. A partir do momento em que a resposta é enviada, tudo o que for recebido de qualquer
das ligações tem de ser enviado transparentemente entre o browser e o servidor.
HTTP/1.0 200 Connection Established
Caso não seja aceite, então deve ser enviado um código de erro (e.g. 407 Proxy Authentication
Required). Pode encontrar mais informações sobre a utilização do método CONNECT em
[RFC2817].
2.2 Autenticação
O suporte de autenticação de clientes numa ligação HTTP foi definido numa norma externa
[RFC2617] ao HTTP. São definidos dois modos de funcionamento: Basic e Digest. No modo
Basic o nome de utilizador e palavra de passe são enviados codificados em Base64, podendo ser
facilmente descodificados por qualquer pessoa nas redes atravessadas pelos pacotes IP da ligação.
No modo Digest, é utilizado um método criptográfico para cifrar estes dados. No trabalho vai ser
realizado o modo Basic.
Um pedido transporta dados de autenticação para o servidor através do campo Authorization.
Um servidor requer a autenticação do cliente devolvendo o valor 401 (Unauthorized) na resposta
© Luis Bernardo 8
e um campo WWW-Authenticate com a indicação do tipo de autenticação pretendido e o nome do
domínio de segurança. Um exemplo de resposta seria:
HTTP/1.1 401 Unauthorized
…
WWW-Authenticate: Basic Realm="RIT2 Server Demo domain"
…
Em resposta, o browser abre uma janela a pedir o nome de utilizador e a palavra de passe, e
codifica em base64 a cadeia de carateres resultante da concatenação do nome (uid) e da palavra
de passe (pwd), reenviando o pedido. Um exemplo, com a string "rit2:demo" codificada:
Authorization: Basic cml0MjpkZW1v
A autenticação com um procurador direto decorre de forma semelhante, exceto que o pedido
usa o campo Proxy-Authorization e a resposta devolve o valor 407 (Proxy Authentication
Required) e usa o campo Proxy-Authenticate.
2.3 Controlo de caching
Quando um browser ou um procurador recebem um ficheiro, costumam guardá-lo em
memória local, designada de cache. Para que o ficheiro possa ser guardado, ele não deverá ter
campos de autenticação, cookies, métodos POST ("cgi-bin" ou "?" no URL), ou outros
campos com controlo explícito de caching que o proíbam. Também não devem ser guardados
num procurador ficheiros vindos através de HTTPS. O HTTP define vários métodos para controlar
se o ficheiro continua atual. Os mais comuns usam os campos de cabeçalho “If-Modified-Since”
e "If-None-Match". No primeiro caso, no segundo pedido, acrescenta-se o cabeçalho com a data
da última modificação, no segundo caso o cabeçalho com a assinatura digital do ficheiro (recebida
no campo "ETag"). Caso o ficheiro seja atual responde-se com o código 304, e com os campos
Date, Server, ETag, e os campos de controlo de ligação, caso existam. Caso contrário, deve
ignorar-se o campo de cabeçalho, e devolver o novo ficheiro.
PEDIDO 1:
GET /somedir/page.html HTTP/1.0
…
RESPOSTA 1:
HTTP/1.0 200 OK
Last-Modified: Thu, 23 Oct 2018 12:00:00 GMT
ETag:"a4137-7ff-42068cd3"
…
PEDIDO 2:
GET /somedir/page.html HTTP/1.0
If-Modified-Since: Thu, 23 Oct 2018 12:00:00 GMT
If-None-Match: "a4137-7ff-42068cd3"
…
RESPOSTA 2:
HTTP/1.0 304 Not Modified
…
Outros métodos consistem na utilização de campos de controlo de caching. Em HTTP 1.0
pode ser usado o campo de cabeçalho “Pragma: no-cache” tanto nos pedidos (não usar para
resposta valores em cache) como nas respostas (não guardar em cache). Em HTTP 1.1 [RFC2616,
sec. 14] deve ser usado o campo de cabeçalho “Cache-Control” que pode ter um subconjunto de
vários valores possíveis. O valor "no-cache" tem o mesmo significado que anteriormente. O valor
"no-store" no pedido ou resposta significa que a resposta não deve ser guardada em memória não
volátil, ou ser apagado o mais depressa possível. O valor "max-age=n" define o tempo máximo
© Luis Bernardo 9
de vida da resposta, ou o tempo máximo que o ficheiro pode estar em cache para ser válido. O
valor "s-maxage=n" numa resposta indica qual é validade máxima de um ficheiro num
procurador. Num ficheiro obtido a partir de um procurador: o campo "Age" indica o tempo que
decorreu desde que o ficheiro foi obtido do servidor; o valor "private" indica que a resposta apenas
pode ser reutilizada para o cliente original; O valor "public" significa que pode ser partilhado (o
valor por omissão). O valor "must-revalidate" e "proxy-revalidation" indicam que o valor em
cache deve ser sempre revalidado antes de responder a pedidos, o segundo especificamente para
procuradores. O valor "no-transform" no pedido ou resposta indicam que as mensagens não
devem ser modificadas por nenhum procurador. Observe-se que alguns servidores substituem o
valor “max-age=6” pelo campo de cabeçalho “Expires: Thu, 23 Oct 2007 12:00:00 GMT“ – a
diferença é que o primeiro define o número de segundos de validade, o segundo a data limite de
validade do ficheiro, tanto para browsers como para procuradores.
Outro aspeto importante no desenho de um procurador prende-se com a gestão dos ficheiros
na cache. Geralmente usa-se um buffer finito com N posições, ordenado pelos últimos ficheiros
acedidos (o mais recentemente acedido é o primeiro, etc.). Caso se exceda a capacidade do buffer,
é excluído do buffer o ficheiro que tinha sido acedido há mais tempo.
3. COMPLEMENTOS SOBRE JAVA
Nesta secção admite-se que os alunos já leram e conhecem sobre a linguagem de programação
Java de ST e RIT1. Para facilitar, vão ser revistas algumas informações sobre as bibliotecas a usar.
Este trabalho vai ser desenvolvido no mesmo ambiente (NetBeans), que é distribuído
gratuitamente juntamente com o ambiente Java.
3.1. IPv6
O protocolo IPv6 é suportado de uma forma transparente na linguagem Java, a partir da versão
Java 1.4. A classe InetAddress tanto permite lidar com endereços IPv4 como IPv6. Esta classe
tem como subclasses Inet4Address e Inet6Address, que suportam respetivamente as funções
específicas relativas aos endereços IPv4 e IPv6. Esta arquitetura permite suportar endereços IPv6
de uma forma transparente nas restantes classes do pacote java.net, uma vez que as restantes
funções usam parâmetros da classe InetAddress nos argumentos e nas variáveis dos objetos das
classes.
É possível obter o endereço local da máquina IPv4 ou IPv6 usando a função da classe
getLocalHost:
try {
InetAddress addr = InetAddress.getLocalHost();
} catch (UnknownHostException e) {
// tratar excepção
}
O método getHostAddress permite obter o endereço em formato string. O nome
associado ao endereço pode ser obtido com o método getHostName.
De forma semelhante, os métodos InetAddress.getByName ou
InetAddress.getAllByName permitem obter os endereços (IPv4 ou IPv6) associados a um
nome de domínio.
© Luis Bernardo 10
3.2 Sockets TCP
Em Java, a interface para sockets TCP é realizada através de duas classes: ServerSocket e
Socket.
3.2.1. A classe ServerSocket
A classe ServerSocket define um objecto servidor que pode receber e manter várias ligações
abertas. Quando se cria um objecto, define-se o porto onde vai escutar. O método accept()
bloqueia o objecto até que seja recebida uma ligação ao porto, criando um objecto da classe Socket
que permite comunicar com o cliente.
Os construtores da classe ServerSocket permitem definir o porto, o número de novas ligações
pendentes que são aceites pelo objecto e o endereço IP (a interface) a que se faz a associação. Em
Java 1.4 surgiu um quarto construtor sem parâmetros, que não inicializa o porto, permitindo usar
a função setReuseAddress(), antes de definir o porto com a função bind().
// Public Constructors
public ServerSocket(int port) throws IOException;
public ServerSocket(int port, int backlog) throws IOException;
public ServerSocket(int port, int backlog, InetAddress bindAddr) throws
IOException;
public ServerSocket() throws IOException; // apenas para Java 1.4
As duas funções principais permitem receber ligações e terminar o servidor.
public Socket accept() throws IOException;
public void close() throws IOException;
Outras funções permitem ter acesso às variáveis do objecto:
public InetAddress getInetAddress();
public int getLocalPort();
public synchronized int getSoTimeout() throws IOException;
public synchronized void setSoTimeout(int timeout) throws
SocketException;
Para permitir comunicar com os clientes e aceitar novas ligações em paralelo é comum usar
múltiplas tarefas (threads) para lidar com cada ligação.
Aplicação Servidora
ServerSocket
Socket
Socket
Socket Aplicação Cliente
Socket
Aplicação Cliente
Socket
Nova ligação
© Luis Bernardo 11
3.2.2. A classe Socket
A classe Socket define um objecto de intercomunicação em modo feixe. Pode ser criado
através de um construtor ou a partir da operação accept. O construtor permite programar clientes:
especifica-se o endereço IP e porto a que se pretende ligar, e o construtor estabelece a ligação.
public Socket(String host, int port) throws UnknownHostException,
IOException;
public Socket(InetAddress address, int port) throws IOException;
public Socket(String host, int port, InetAddress localAddr,
int localPort) throws IOException;
public Socket(InetAddress address, int port, InetAddress localAddr,
int localPort) throws IOException;
As operações de escrita e leitura do socket são realizadas através de objectos do pacote java.io
(InputStream e OutputStream), descritos na próxima secção, retornados por duas funções da
classe. Existem ainda funções para fechar o socket e para obter informações sobre a identidade da
ligação.
public InputStream getInputStream() throws IOException;
public OutputStream getOutputStream() throws IOException;
public synchronized void close() throws IOException;
public InetAddress getInetAddress();
public InetAddress getLocalAddress();
public int getLocalPort();
public int getPort();
public isConnected(); // Java 1.4
public isClosed(); // Java 1.4
Várias operações de configuração dos parâmetros do protocolo TCP podem ser realizadas
através de métodos desta classe. As funções getReceiveBufferSize, setReceiveBufferSize,
getSendBufferSize e setSendBufferSize permitem modificar a dimensão dos buffers usados no
protocolo TCP. As funções getTCPNoDelay e setTCPNoDelay controlam a utilização do
algoritmo de Nagle (false = desligado). As funções getSoLinger e setSoLinger controlam o que
acontece quando se fecha a ligação: se está ligada o valor define o número de segundos que se
espera até tentar enviar o resto dos dados que estão no buffer TCP e ainda não foram enviados.
Caso esteja ativo, pode originar perda de dados não detetável pela aplicação.
public int getReceiveBufferSize() throws SocketException;
public synchronized void setReceiveBufferSize(int size) throws
SocketException;
public int getSendBufferSize() throws SocketException; // 1.4
public synchronized void setSendBufferSize(int size) throws
SocketException; // 1.4
public boolean getTcpNoDelay() throws SocketException;
public void setTcpNoDelay(boolean on) throws SocketException;
public int getSoLinger() throws SocketException;
public void setSoLinger(boolean on, int val) throws SocketException;
public synchronized int getSoTimeout() throws SocketException;
public synchronized void setSoTimeout (int timeout) throws
SocketException;
As funções getSoTimeout e setSoTimeout permitem configurar o tempo máximo que uma
operação de leitura pode ficar bloqueada, antes de ser cancelada. Caso o tempo expire é gerada
uma exceção SocketTimeoutException. Estas funções também existem para as classes
ServerSocket e DatagramSocket.
© Luis Bernardo 12
3.2.3. Comunicação em sockets TCP
Os objetos da classe Socket oferecem métodos para obter um objeto da classe InputStream
(getInputStream) para ler do socket, e para obter um objeto da classe OutputStream
(getOutputStream). No entanto, estas classes apenas suportam a escrita de arrays de bytes. Assim,
é comum usar outras classes do pacote java.io para envolver estas classes base, obtendo-se uma
maior flexibilidade:
Para ler strings a partir de um socket é possível trabalhar com:
• A classe InputStreamReader processa a cadeia de bytes interpretando-a como uma
sequência carateres (convertendo o formato de carater).
• A classe BufferedReader armazena os carateres recebidos a partir de um feixe do tipo
InputStreamReader, suportando o método readLine() para esperar pela receção de uma
linha completa.
Para escrever strings num socket é possível trabalhar com:
• A classe OutputStreamWriter processa a cadeia de carateres e codifica-a para uma
sequência de bytes.
• A classe PrintWriter suporta os métodos de escrita de strings e de variáveis de outros
formatos (print e println) para um feixe do tipo OutputStreamWriter.
• A classe PrintStream suporta os métodos de escrita de strings e de byte [] para um
feixe do tipo OutputStream.
Caso se pretendesse enviar objetos num formato binário (não legível), dever-se-ia usar as
classes DataInputStream e DataOutputStream.
Um exemplo de utilização destas classes seria o apresentado em seguida:
try { // soc representa uma variável do tipo Socket inicializada
// Cria feixe de leitura
InputStream ins = soc.getInputStream( );
BufferedReader in = new BufferedReader(
new InputStreamReader(ins, "8859_1" )); // Tipo de caracter
// Cria feixe de escrita
OutputStream out = soc.getOutputStream( );
PrintStream pout = new PrintStream(out);
// Em alternativa poder-se-ia usar PrintWriter:
// PrintWriter pout = new PrintWriter(
// new OutputStreamWriter(out, "8859_1"), true);
// Lê linha e ecoa-a para a saída
String in_string= in.readln();
pout.writeln("Recebi: "+ in_string);
}
catch (IOException e ) { ... }
3.2.4. Exemplo de aplicação - TinyFileServ
No exemplo seguinte é apresentado um servidor de ficheiros parcialmente compatível com o
protocolo HTTP (utilizável por browsers), que recebe o nome do ficheiro no formato (* nome-
completo *) e devolve o conteúdo do ficheiro, fechando a ligação após o envio do último caracter
do ficheiro. O servidor recebe como argumentos da linha de comando o número de porto a partir
da linha de comando e a diretoria raiz correspondente à diretoria "/" (e.g. “java TinyFileServ
20000 /home/rit2/www”). Para cada nova ligação, o servidor lança uma tarefa que envia o ficheiro
pedido e termina após o envio do ficheiro. O servidor é compatível com um browser: lê o segundo
campo do pedido (e.g. GET / HTTP/1.1). Não é enviado o código de resposta, mas os browsers
mais vulgares (Firefox, etc.) sabem interpretar o ficheiro recebido a partir dos primeiros carateres
© Luis Bernardo 13
e do nome do ficheiro. Este exemplo usa algumas classes descritas nas próximas secções deste
capítulo.
//file: TinyFileServd.java
import java.net.*;
import java.io.*;
import java.util.*;
public class TinyFileServd {
public static void main( String argv[] ) throws IOException {
ServerSocket ss = new ServerSocket(
Integer.parseInt(argv.length>0 ? argv[0] : "20000"));
while ( true )
new TinyFileConnection(ss.accept(),
(argv.length>1 ? argv[1] : "")).start( );
}
} // end of class TinyFileServd
class TinyFileConnection extends Thread {
private Socket client;
private String root;
TinyFileConnection ( Socket client, String root ) throws SocketException {
this.client = client;
this.root= root;
setPriority( NORM_PRIORITY + 1 ); // Higher the thread priority
}
public void run( ) {
try {
BufferedReader in = new BufferedReader(
new InputStreamReader(client.getInputStream( ), "8859_1" ));
OutputStream out = client.getOutputStream( );
PrintStream pout = new PrintStream(out, false, "8859_1");
String request = in.readLine( ); // Reads the first line
System.out.println( "Request: "+request );
StringTokenizer st= new StringTokenizer (request);
if (st.countTokens() != 3) return; // Invalid request
String code= st.nextToken(); // USES HTTP syntax
String file= st.nextToken(); // for requesting files
String ver= st.nextToken();
String filename= root+file+(file.equals("/")?"index.htm":"");
System.out.println("Filename= "+filename);
FileInputStream fis = new FileInputStream ( filename );
byte [] data = new byte [fis.available()]; // Fails for large files
fis.read( data ); // Read from file
out.write( data ); // Write to socket
out.flush( ); // Flush socket buffer
fis.close();
client.close( );
}
catch ( FileNotFoundException e ) {
System.out.println( "File not found" );
}
catch ( IOException e ) {
System.out.println( "I/O error " + e );
}
}
} // end of class TinyFileConnection
3.2.5. Exemplo de cliente - HTTPClient
No exemplo seguinte é apresentado uma aplicação cliente parcialmente compatível com o
protocolo HTTP (utilizável com um servidor Web), que recebe um URL (e.g.
http://tele1.dee.fct.unl.pt/rit2), estabelece ligação com o servidor, e escreve o conteúdo da resposta
do servidor num ficheiro. O cliente recebe como argumentos da linha de comando o URL e o
nome completo do ficheiro (e.g. “java HTTPClient http://tele1.dee.fct.unl.pt/rit2 rit2.htm”). O
cliente termina após receber o último carater do ficheiro. O cliente é compatível com um servidor
Web: salta todas as linhas de cabeçalho (escrevendo o seu conteúdo na consola) até receber uma
© Luis Bernardo 14
linha vazia. A partir dessa altura escreve tudo o que receber no ficheiro previamente aberto. Este
exemplo também usa algumas classes descritas nas próximas secções deste capítulo.
// exemplo baseado parcialmente em http://examples.oreilly.com/jenut/HttpClient.java
import java.io.*;
import java.net.*;
public class HTTPClient {
public static void main(String[ ] args) {
try {
if (args.length != 2)
throw new IllegalArgumentException("Wrong number of args");
// Get an output stream to write the URL contents to
BufferedWriter to_file = new BufferedWriter (new OutputStreamWriter
(new FileOutputStream (args[1]), "8859_1"));
// Parse URL
URL url = new URL(args[0]);
String protocol = url.getProtocol();
String host = url.getHost( );
int port = url.getPort( );
String path = url.getPath();
if (path == null || path.length( ) == 0) path = "/";
Socket socket;
if (protocol.equals("http")) {
if (port == -1) port = 80; // Default http port
socket = new Socket(host, port); // Falha para tele1.dee.fct.unl.pt!!!
}
else {
throw new IllegalArgumentException("URL must use http: protocol");
}
// Get input and output streams for the socket
BufferedReader from_server = new BufferedReader(
new InputStreamReader(socket.getInputStream( ), "8859_1" ));
PrintWriter to_server = new PrintWriter(socket.getOutputStream( ));
// Send the HTTP GET command to the web server, specifying the file
to_server.print("GET " + path + " HTTP/1.0\r\n" +
"Host: " + host + "\r\n" +
"Connection: close\r\n\r\n");
to_server.flush( ); // Send it right now!
// Read and print the HTTP headers the server returns
String str;
while((str= from_server.readLine()) != null) {
System.out.println(str);
if (str.length() == 0) // Header separator line
break;
}
char[ ] buffer = new char[8 * 1024];
int bytes_read;
// Now read the rest of the bytes and write to the file
while((bytes_read = from_server.read(buffer)) != -1)
to_file.write(buffer, 0, bytes_read);
// When the server closes the connection, we close our stuff, too
socket.close( );
to_file.close( );
}
catch (Exception e) { // Report any errors that arise
System.err.println(e);
System.err.println("Usage: java HttpClient <URL> <filename>");
}
}
}
© Luis Bernardo 15
3.3 Strings
Uma vez que o protocolo HTTP é baseado em strings, para realizar o servidor web vai ser
necessário interpretar comandos e gerar respostas em formato ASCII. Nesta secção são
apresentadas algumas das classes que podem ser usadas para desempenhar estas tarefas.
3.3.1. Separação em componentes
Um dos aspetos essenciais é a separação de uma string em componentes fundamentais. As
classes String e StringBuffer oferecem o método substring para selecionar uma parte da string:
String a= "Java is great";
String b= a.substring(5); // b is the string "is great"
String c= a.substring(0, 4); // c is the string "Java"
O método trim() remove os espaços e tabulações antes e depois da string.
A comparação de strings pode ser feita com os métodos equals ou equalsIgnoreCase.
A linguagem Java oferece a classe StringTokenizer para decompor uma string em substrings
separadas por espaços ou outros separadores. O método nextToken() permite percorrer todos os
componentes, enquanto os métodos hasMoreTokens() e countTokens() permitem saber quando se
termina. Por exemplo, o código seguinte permite descodificar a primeira linha de um cabeçalho
HTTP nos vários componentes. O construtor pode ter um segundo parâmetro opcional com uma
string com a lista de separadores (por omissão tem apenas o espaço por separador).
StringTokenizer st = new StringTokenizer( request );
// Pode-se acrescentar um parâmetro extra com o separador e.g. ":"
if ( (st.countTokens( ) != 3) { … erro … }
String rqCode= st.nextToken( );
String rqName= st.nextToken( );
String rqVersion = st.nextToken( );
if (rqCode.equals("GET") || rqCode.equals("POST")) {
if ( rqName.startsWith("/") )
rqName = rqName.substring( 1 ); // Removes first character
if ( rqName.endsWith("/") || rqName.equals("") )
rqName = rqName + "index.htm"; // Adds “index.htm” if is a directory
…
}
3.3.2. Datas compatíveis com o protocolo HTTP
O protocolo HTTP define um formato específico para a escrita de datas:
Thu, 23 Oct 2002 12:00:00 GMT
É possível escrever e ler variáveis com este formato utilizando uma variável da classe
DateFormat.
DateFormat httpformat=
new SimpleDateFormat ("EE, d MMM yyyy HH:mm:ss zz", Locale.UK);
httpformat.setTimeZone(TimeZone.getTimeZone("GMT"));
// Escrita de datas
out.println("A data atual é " + httpformat.format(dNow));
// Leitura de datas
try {
Date dNow= httpformat.parse(str);
} catch (ParseException e) {
System.out.println("Data inválida: " + e + "\n");
}
© Luis Bernardo 16
É possível comparar datas utilizando o método compareTo de objetos da classe Date. A
adição e subtração de intervalos de tempo a datas também são possíveis convertendo a data em
long utilizando o método getTime. Como o valor conta o número de milissegundos desde 1970,
basta somar ou subtrair o valor correspondente ao intervalo. Por exemplo, para avançar um dia
seria:
Date d= new Date(dNow.getTime() + (long)24*60*60*1000);
Outra alternativa é usar a função add da classe Calendar para realizar a operação:
Calendar now= Calendar.getInstance();
now.add(Calendar.DAY_OF_YEAR, +1);
Date d= now.getTime();
3.3.3. URLs
A leitura e validação de URLs podem ser feitas usando a classe java.net.URL. Um URL tem
a estrutura seguinte:
<protocolo>://<autoridade><path>?<query>#<fragment>
Esta classe tem vários construtores que recebem uma string com o URL completo e outro que
recebe o URL por parâmetros. Depois inclui funções que permitem obter os vários campos do
URL: // Construtores – devolvem excepção se url inválido:
public URL(String spec) throws MalformedURLException;
public URL(String protocol, String host, int port, String file)
throws MalformedURLException
// Exemplo de métodos desta classe:
URL url= new URL("http://[email protected]:8080/servlet/xxx?xpto=ola&xa=xa#2");
url.getProtocol() == "http"
url.getAuthority() == " [email protected]:8080" // "" se omitido
url.getUserInfo() == "lflb" // null se omitido
url.getPort() == 8080 // -1 se omitido
url.getDefaultPort() == 80
url.getFile() == "/servlet/xxx?xpto=ola&xa=xa" // "" se omitido
url.getHost() == "tele1.dee.fct.unl.pt" // "" se omitido
url.getPath() == "/servlet/xxx" // "" se omitido
url.getQuery() == "xpto=ola&xa=xa" // null se omitido
url.getRef() == "2" // null se omitido
A biblioteca Java inclui a classe java.net.URLConnection que permite simplificar a criação
de ligações para um URL. Neste trabalho pretende-se realizar essa ligação diretamente sobre
sockets TCP, para ter um controlo mais fino na comunicação.
3.4 Ficheiros
A classe java.io.File representa um ficheiro ou uma diretoria, e define um conjunto de
métodos para os manipular. O construtor recebe o nome completo de um ficheiro, permitindo
depois a classe saber se o ficheiro existe, se é ficheiro ou diretoria, o comprimento do ficheiro,
apagar o ficheiro, ou marcar o ficheiro para ser apagado quando o programa termina. Inclui ainda
métodos para criar ficheiros temporários com nomes únicos.
© Luis Bernardo 17
File f= new File ("/home/pc40/xpto.txt"); // Associa-se a ficheiro
long len= f.length(); // Comprimento do ficheiro
Date date= new Date(f.lastModified()); // Última modificação
if (f.exists()) … // Se existe
if (f.isFile()) … // Se é ficheiro
if (f.canRead()) … // Se é legível
f.delete(); // Apaga ficheiro
File temp= File.createTempFile("proxy", ".tmp"); // Cria ficheiro temporário com nome único
temp.deleteOnExit(); // Apaga ficheiro quando a aplicação termina
File.separator // '/' ou '\\' dependendo do sistema operativo
A leitura a partir de ficheiros de texto é geralmente realizada através da classe
FileInputStream. Recomenda-se que a escrita de ficheiros recebido a partir de servidores Web
seja feita através da classe BufferedWriter, por esta também permitir definir o tipo de caracteres
("ISO-8859-1" por omissão). Caso este tipo seja usado nos canais associados a sockets e a
ficheiros, o Java nunca faz conversão de tipos, permitindo transmitir dados arbitrários (imagens,
aplicações, etc.).
// Para leitura
FileInputStream f= new FileInputStream ( file );
// Para escrita
FileOutputStream fos= new FileOutputStream ( file );
OutputStreamWriter osr= new OutputStreamWriter(fos, "8859_1");
BufferedWriter os= new BufferedWriter(osr);
// Permitem ler e escrever 'char []' com os métodos 'read' e 'write'
// usando o método 'getBytes()' é possível converter um 'char []' em 'byte []'
3.5 Estruturas de dados adicionais
A linguagem Java suporta um conjunto de variado de estruturas de dados que permitem lidar
de uma forma eficaz com conjuntos de pares (nome de propriedade, valor de propriedade), onde
o campo nome_de propriedade é único. Uma das estruturas que permite lidar com este tipo de
dados é a classe Properties. Esta classe é usada para manter as variáveis de sistema. Uma variável
da classe Properties pode ser iniciada vazia ou a partir do conteúdo de um ficheiro de texto, pode-
se acrescentar ou remover elementos, pode-se pesquisar por nome de propriedade ou
exaustivamente, e pode-se exportar o conteúdo para um ficheiro.
Properties prop= new Properties();
try { prop.load(in) } catch (IOException e) {…} // Lê ficheiro
prop.setProperty(“nome”, “valor”); // define valor
String val= prop.getProperty(“nome”); // obtém valor
String val2= prop.getProperty(“nome2”, “omisso”); // obtém valor, se n existe
devolve “omisso”
for (Enumeration p= prop.propertyNames(); p.hasMoreElements();) // percorre lista
System.out.println(p.nextElement());
try { prop.store(new FileOutputStream(“file”), “Configuração:”); } // Grava ficheiro
catch (IOException e) { … }
3.6 Descodificação em base64
Para suportar a descodificação de campos de mensagens em formato base64, vai ser usada
uma biblioteca desenvolvida por Robert W. Harder e disponibilizada gratuitamente em
http://iharder.sourceforge.net/base64/. Para descodificar uma string codificada em base64 (e.g.
txt_enc) basta usar o método Base64.decode:
© Luis Bernardo 18
String txt_dec= new String(Base64.decode(txt_enc));
4. ESPECIFICAÇÕES
Pretende-se neste trabalho realizar um procurador Web que funcione no modo direto e inverso
e que permita explorar várias funcionalidades do protocolo HTTP. Não se pretende uma realização
completa do protocolo, mas uma realização “à medida”, com uma interface gráfica que permite
configurar o servidor de uma forma simples.
4.1 Especificação do funcionamento
O procurador Web deve suportar vários clientes em paralelo, recorrendo a tarefas individuais
para responder a cada cliente. Na figura abaixo está representada a interface gráfica fornecida com
o trabalho. A interface gráfica deverá permitir arrancar e parar o servidor (Active), definir o
número de porto do serviço HTTP (Port), o nome de utilizador e uma palavra de passe para as
páginas (Password), o tempo máximo de manutenção de ligações HTTP abertas (Keep-Alive).
O servidor deve receber pedidos no porto Port desde que o toggleButton Active esteja
selecionado. Caso a caixa Password esteja preenchida, deverá ser usada autenticação por nome
de utilizador e palavra de passe nos acessos HTTP no modo básico. O valor de Keep-Alive apenas
é usado caso se mantenha uma ligação aberta, em HTTP 1.1. Keep-Alive define o número de
segundo máximo de inatividade até que se desligue a ligação (0 significa que não se desliga).
Threads indica o número de ligações de clientes ativas num instante. Finalmente, o botão Clear
limpa a janela de texto.
Quando funciona como procurador inverso (reverse proxy), o campo Redir URL define para
que servidores web reais deve ser efetuada a ligação. Desta forma, é semelhante a um servidor
web clássico, pois envia um ficheiro – a diferença está que vai buscar o ficheiro ao servidor remoto
ou à cache, antes de o transmitir, em vez de usar ficheiros locais fixos.
O funcionamento como Procurador direto (forward proxy) segue o padrão normal com um
procurador.
Para facilitar o desenvolvimento, é fornecido o código de um procurador básico, embora
quase totalmente funcional, que integra e adapta o código dos exemplos 3.2.4 e 3.2.5 com a
interface gráfica, descrito na próxima secção.
4.2 Tarefas a realizar
Para facilitar o desenvolvimento do programa e tornar possível o desenvolvimento do
programa durante as dez horas previstas, é fornecido juntamente com o enunciado um programa
© Luis Bernardo 19
proxy incompleto, com a interface gráfica apresentada anteriormente, que já realiza parte das
funcionalidades pedidas. Cada grupo pode fazer todas as modificações que quiser ao programa
base, ou mesmo, desenhar uma interface gráfica de raiz. No entanto, recomenda-se que invistam
o tempo na correta realização do algoritmo de encaminhamento e no envio dos dados.
O projeto ProxyHttpd_students fornecido é composto por dois pacotes e dez classes. No
pacote HTTPFormat tem as seguintes classes:
• Headers.java (completa) – Implementa uma lista indexada que suporta vários
elementos para cada chave, usada para guardar campos de cabeçalho;
• HTTPReplyCode.java (completa) – Guarda a informação para primeira linha de uma
resposta HTTP (código de resposta, mensagem de resposta, e versão de HTTP);
• HTTPAnswer.java (a completar) – Descritor de resposta HTTP, que guarda a
informa, realiza métodos para leitura e escrita em sockets, e métodos de manipulação
e teste de campos de cabeçalho;
• HTTPQuery.java (a completar) – Descritor de pedido HTTP, que guarda a informa,
realiza métodos para leitura e escrita em sockets, e métodos de manipulação e teste
de campos de cabeçalho;
• Log.java (completa) – Interface para escrita de mensagens no écran.
No pacote Main tem as seguintes classes:
• Base64.java (completa) – Biblioteca para descodificar o formato Base 64;
• sHttpd.java (completa) – Classe que suporta a tarefa que recebe novas ligações TCP
no procurador;
• ProxyHttpd.java (quase completo) – Classe principal com interface gráfica (herda de
JFrame), que faz a gestão de sincronismo dos vários objetos usados (falta criar o
objeto cache de páginas);
• HTTPClient.java (a completar) – Classe auxiliar, que contendo um pedido e uma
resposta, realiza a comunicação entre o procurador e o servidor web;
• HTTPThread.java (a completar) – Classe responsável pela comunicação entre o
procurador e o browser, usando os métodos disponibilizados principalmente pelas
classes HTTPClient, HTTPQuery e HTTPAnswer. Realiza da gestão da ligação do
browser, autenticação e da reutilização de ligações.
No decorrer do trabalho, é recomendado que acrescente mais uma classe extra, para gerir a
cache de páginas web no procurador (indexadas pelo URL).
O programa fornecido inclui vários ficheiros completos, e os que não o estão, já realizam uma
parte significativa das funcionalidades necessárias. A ativação dos sockets e das thread de receção
de pedidos já é feita no programa fornecido. As restantes funcionalidades vão ser distribuídas
pelas classes a completar.
O desenvolvimento do trabalho é realizado em cinco fases: descritas de seguida.
4.2.1 Procurador básico inverso
A realização da comunicação entre o browser e o procurador e o procurador e o servidor estão
quase todas realizadas. Mas faltam algumas funcionalidades:
• Completar a função get_answer_from_server da classe HTTPClient, para enviar o
pedido para o servidor web;
• Completar a função parse_Answer da classe HTTPAnswer, para ler os campos de
cabeçalho;
• Completar a função send_Answer da classe HTTPAnswer, para enviar os campos de
cabeçalho;
© Luis Bernardo 20
• Modifique o código de maneira a modificar os cabeçalhos do pedido, de maneira a
esconder a identidade do browser, só quando for um pedido do tipo inverso.
4.2.2 Procurador básico direto
O funcionamento do procurador direto é semelhante ao do procurador inverso, diferindo
principalmente nos campos de cabeçalho que são enviados nos pedidos ao servidor web e que são
enviados para o browser.
4.2.3 Autenticação de clientes do procurador
A realização da validação dos clientes do procurador tem de ser integrada na função run, da
classe HTTPThread. Recomenda-se que completem e usem a função authentication_valid de
forma a validar se o cabeçalho de autenticação foi enviado com a palavra de passe correta.
boolean authentication_valid ( HTTPQuery q );
4.2.4 Reutilização da ligação browser-procurador
A realização da validação dos clientes do procurador tem de ser integrada na função run, da
classe HTTPThread. Recomenda-se que modifique a função, para criar um ciclo, que permite ler
vários pedidos e enviar as respostas. É necessário ler e escrever os valores dos campos de
cabeçalho recebidos e enviados que gerem a reutilização de ligação.
4.2.5 Caching de pedidos
A criação de uma cache de pedidos compreende a realização de várias tarefas:
• Criação de um objeto central onde vai ser criada a lista com pedidos/respostas
guardados;
• Modificação da função run, de maneira a guardar uma página na cache, quando os
campos de cabeçalho o permitirem;
• Modificação da função run, de maneira a verificar se a página já está em cache, antes
de ir fazer a ligação ao servidor, quando os campos de cabeçalho o permitirem;
• Criação e invocação de uma função que valide se o conteúdo guardado em cache ainda
está válido.
É deixada a liberdade aos alunos nesta tarefa. Mas, foram deixadas algumas funções
incompletas nas classes HTTPClient e HTTPQuery com sugestões de como podem testar se as
respostas são guardáveis, ou se é necessário validar a cópia em cache. Pretende-se que a realização
esteja correta, de acordo com o protocolo HTTP/1.1.
4.3 Testes
O servidor web deverá ser compatível com qualquer browser, com qualquer das opções
ligadas. Assim, recomenda-se que sigam as especificações indicadas na norma HTTP e evitem
adotar configurações específicas para alguns browsers. Recomenda-se que usem o Firefox pois
permite configurar o proxy na aplicação, e um analisador de protocolos (e.g. Wireshark), para
conseguirem monitorizar toda as trocas de mensagens HTTP, validando-as. Podem também usar
o comando telnet para criar ligações para o servidor e gerar pedidos manualmente.
No mínimo, o procurador deverá suportar pedidos (GET) como procurador direto e indireto,
e a autenticação de utilizadores. Para ambicionarem a uma boa nota, dever-se-á também
desenvolver a reutilização de ligações e principalmente, o sistema de cache de respostas.
© Luis Bernardo 21
Atendendo à grande difusão de realizações de servidores e procuradores Web disponíveis na
Internet, alerta-se os alunos que não devem usar código que não conheçam ou consigam
interpretar, pois a discussão vai focar todos os aspetos do código e da realização do servidor.
4.4 Desenvolvimento do trabalho
O trabalho vai ser desenvolvido em cinco semanas. Propõe-se que sejam definidas as
seguintes metas para a realização do trabalho:
1. antes da primeira aula deve ler a documentação e o código fornecido.
Recomenda-se também que instale um servidor web (e.g. google “install Apache
[sistema operativo]”), o Wireshark (https://www.wireshark.org/) e para quem usa
Windows, instalar o telnet (https://www.technipages.com/windows-10-enable-telnet);
2. no fim da primeira aula deve ter realizado as três primeiras subtarefas de 4.2.1 e ter
começado a realizar a última;
3. no fim da segunda aula deve estar a trabalhar na tarefa 4.2.3;
4. no fim da terceira aula deve ter concluído a tarefa 4.2.4;
5. no fim da quarta aula deve estar a trabalhar na tarefa 4.2.5, ainda com um teste
incompleto das várias variantes de campos de cabeçalho – comece por usar os mais
comuns (e.g. “Cache-Control” com “no-cache” ou “max-age=0”. Não se esqueça da
validação de cache, com os campos "If-Modified-Since" e "If-None-Match" do cliente,
e os campos "Last-Modified" e "ETag" do servidor;
6. no fim da última aula deve ter acabado todas as tarefas anteriores. Caso ainda tenha
tempo, enriqueça o 4.2.5, de forma a controlar o tempo em que uma página é guardada
em cache.
Postura dos Alunos
Cada grupo deve ter em consideração o seguinte:
• Não perca tempo com a estética de entrada e saída de dados
• Programe de acordo com os princípios gerais de uma boa codificação (utilização de
indentação, apresentação de comentários, uso de variáveis com nomes conformes às suas
funções...) e
• Proceda de modo a que o trabalho a fazer fique equitativamente distribuído pelos
membros do grupo.
DATAS LIMITE
A parte laboratorial é composta por dois trabalhos de avaliação. A duração prevista para os
dois trabalhos é de 5 semanas. A parte prática tem o seu início no dia 12 de março. A data limite
provisória para entrega do primeiro trabalho é o dia 19 de abril de 2020, às 12:00.
BIBLIOGRAFIA
[RFC1945] HTTP 1.0, http://www.ietf.org/rfc/rfc1945.txt
[RFC2616] HTTP 1.1, http://www.ietf.org/rfc/rfc2616.txt
[RFC2617] HTTP Authentication: Basic and Digest Access Authentication, http://www.ietf.org/rfc/rfc2617.txt
[RFC2817] Upgrading to TLS within HTTP/1.1, http://www.ietf.org/rfc/rfc2817.txt
[RFC2818] HTTP Over TLS, http://www.ietf.org/rfc/rfc2818.txt
[RFC7230] Hypertext Transfer Protocol (HTTP/1.1): Message Syntax and Routing,
https://httpwg.org/specs/rfc7230.html