HTTPURLConnection não segue redirecionamento de HTTP para HTTPS

97

Não consigo entender por que o Java HttpURLConnectionnão segue um redirecionamento HTTP de um URL HTTP para um HTTPS. Eu uso o seguinte código para obter a página em https://httpstat.us/ :

import java.net.URL;
import java.net.HttpURLConnection;
import java.io.InputStream;

public class Tester {

    public static void main(String argv[]) throws Exception{
        InputStream is = null;

        try {
            String httpUrl = "http://httpstat.us/301";
            URL resourceUrl = new URL(httpUrl);
            HttpURLConnection conn = (HttpURLConnection)resourceUrl.openConnection();
            conn.setConnectTimeout(15000);
            conn.setReadTimeout(15000);
            conn.connect();
            is = conn.getInputStream();
            System.out.println("Original URL: "+httpUrl);
            System.out.println("Connected to: "+conn.getURL());
            System.out.println("HTTP response code received: "+conn.getResponseCode());
            System.out.println("HTTP response message received: "+conn.getResponseMessage());
       } finally {
            if (is != null) is.close();
        }
    }
}

O resultado deste programa é:

URL original: http://httpstat.us/301
Conectado a: http://httpstat.us/301
Código de resposta HTTP recebido: 301
Mensagem de resposta HTTP recebida: movida permanentemente

Uma solicitação para http://httpstat.us/301 retorna a seguinte resposta (abreviada) (que parece absolutamente correta!):

HTTP/1.1 301 Moved Permanently
Cache-Control: private
Content-Length: 21
Content-Type: text/plain; charset=utf-8
Location: https://httpstat.us

Infelizmente, o Java HttpURLConnectionnão segue o redirecionamento!

Note que se você alterar a URL original para HTTPS ( https://httpstat.us/301 ), Java vai seguir o redirecionamento como esperado !?

Shcheklein
fonte
Olá, editei sua pergunta para maior clareza e para apontar que o redirecionamento para HTTPS em particular é o problema. Além disso, alterei o domínio bit.ly para um diferente, pois use bit.ly está na lista negra de perguntas. Espero que você não se importe, fique à vontade para reeditar.
sleske

Respostas:

119

Os redirecionamentos são seguidos apenas se usarem o mesmo protocolo. (Consulte o followRedirect()método na fonte.) Não há como desativar essa verificação.

Mesmo sabendo que ele espelha o HTTP, do ponto de vista do protocolo HTTP, o HTTPS é apenas algum outro protocolo desconhecido completamente diferente. Não seria seguro seguir o redirecionamento sem a aprovação do usuário.

Por exemplo, suponha que o aplicativo esteja configurado para executar a autenticação do cliente automaticamente. O usuário espera navegar anonimamente porque está usando HTTP. Mas se seu cliente seguir HTTPS sem perguntar, sua identidade é revelada ao servidor.

Erickson
fonte
60
Obrigado. Acabei de encontrar a confirmação: bugs.sun.com/bugdatabase/view_bug.do?bug_id=4620571 . A saber: "Após discussão entre engenheiros de rede Java, achamos que não devemos seguir automaticamente o redirecionamento de um protocolo para outro, por exemplo, de http para https e vice-versa, fazer isso pode ter sérias consequências de segurança. Portanto, a solução é para retornar as respostas do servidor para redirecionamento. Verifique o código de resposta e o valor do campo do cabeçalho de localização para obter informações de redirecionamento. É responsabilidade do aplicativo seguir o redirecionamento. "
Shcheklein
2
Mas ele segue redirecionando de http para http ou https para https? Mesmo isso estaria errado. Não é?
Sudarshan Bhat
7
@JoshuaDavis Sim, só se aplica a redirecionamentos para o mesmo protocolo. Um HttpURLConnectionnão seguirá automaticamente os redirecionamentos para um protocolo diferente, mesmo se o sinalizador de redirecionamento estiver definido.
Erickson
8
Os engenheiros de rede Java podem oferecer uma opção setFollowTransProtocol (true) porque, se precisarmos, iremos programá-la de qualquer maneira. Navegadores da web FYI, curl e wget e podem seguir redirecionamentos de HTTP para HTTPS e vice-versa.
supercobra
18
Ninguém configura o login automático em HTTPS e espera que o HTTP seja "anônimo". Isso é absurdo. É perfeitamente seguro e normal seguir redirecionamentos de HTTP para HTTPS (não o contrário). Esta é apenas uma API Java tipicamente ruim.
Glenn Maynard
54

HttpURLConnection por design não redirecionará automaticamente de HTTP para HTTPS (ou vice-versa). Seguir o redirecionamento pode ter consequências graves para a segurança. SSL (portanto, HTTPS) cria uma sessão exclusiva para o usuário. Esta sessão pode ser reutilizada para várias solicitações. Assim, o servidor pode rastrear todas as solicitações feitas de uma única pessoa. Esta é uma forma fraca de identidade e pode ser explorada. Além disso, o handshake SSL pode solicitar o certificado do cliente. Se enviado ao servidor, a identidade do cliente é fornecida ao servidor.

Como erickson aponta, suponha que o aplicativo esteja configurado para executar a autenticação do cliente automaticamente. O usuário espera navegar anonimamente porque está usando HTTP. Mas se seu cliente seguir HTTPS sem perguntar, sua identidade é revelada ao servidor.

O programador deve tomar medidas extras para garantir que credenciais, certificados de cliente ou id de sessão SSL não sejam enviados antes de redirecionar de HTTP para HTTPS. O padrão é enviá-los. Se o redirecionamento prejudicar o usuário, não siga o redirecionamento. É por isso que o redirecionamento automático não é compatível.

Com isso entendido, aqui está o código que seguirá os redirecionamentos.

  URL resourceUrl, base, next;
  Map<String, Integer> visited;
  HttpURLConnection conn;
  String location;
  int times;

  ...
  visited = new HashMap<>();

  while (true)
  {
     times = visited.compute(url, (key, count) -> count == null ? 1 : count + 1);

     if (times > 3)
        throw new IOException("Stuck in redirect loop");

     resourceUrl = new URL(url);
     conn        = (HttpURLConnection) resourceUrl.openConnection();

     conn.setConnectTimeout(15000);
     conn.setReadTimeout(15000);
     conn.setInstanceFollowRedirects(false);   // Make the logic below easier to detect redirections
     conn.setRequestProperty("User-Agent", "Mozilla/5.0...");

     switch (conn.getResponseCode())
     {
        case HttpURLConnection.HTTP_MOVED_PERM:
        case HttpURLConnection.HTTP_MOVED_TEMP:
           location = conn.getHeaderField("Location");
           location = URLDecoder.decode(location, "UTF-8");
           base     = new URL(url);               
           next     = new URL(base, location);  // Deal with relative URLs
           url      = next.toExternalForm();
           continue;
     }

     break;
  }

  is = conn.openStream();
  ...
Nathan
fonte
Esta é apenas uma solução que funciona para mais de 1 redirecionamento. Obrigado!
Roger Alien
Isso funciona perfeitamente para vários redirecionamentos (HTTPS API -> HTTP -> imagem HTTP)! Solução simples e perfeita.
EricH206
1
@Nathan - obrigado pelos detalhes, mas ainda não comprei. Por exemplo, se está sob o controle do cliente, se quaisquer credenciais ou certificados de cliente são enviados. Se doer, não faça isso (neste caso, não siga o redirecionamento).
Julian Reschke
1
Só não entendo a location = URLDecoder.decode(location...parte. Isso decodifica uma parte relativa codificada funcional (com espaço = + no meu caso) em uma não funcional. Depois de removê-lo, estava tudo bem para mim.
Niek
@Niek Não sei por que você não precisa disso, mas eu sim.
Nathan
26

Alguma coisa ligou HttpURLConnection.setFollowRedirects(false)por acaso?

Você sempre pode ligar

conn.setInstanceFollowRedirects(true);

se você quiser ter certeza de que não afetará o resto do comportamento do aplicativo.

Jon Skeet
fonte
Ooo ... não sabia disso ... Belo achado ... Eu estava prestes a pesquisar a classe caso houvesse uma lógica assim ... Faz sentido que ele retornasse aquele cabeçalho com a responsabilidade única diretor .... agora volte a responder às perguntas C #: P [estou brincando]
monksy
2
Observe que setFollowRedirects () deve ser chamado na classe, e não em uma instância.
karlbecker_com
3
@dldnh: Embora karlbecker_com esteja absolutamente certo sobre chamar setFollowRedirectso tipo, setInstanceFollowRedirectsé um método de instância e não pode ser chamado no tipo.
Jon Skeet
1
uggh, como eu interpretei mal isso. desculpe pela edição incorreta. também tentei reverter e não tenho certeza de como estraguei isso também.
dldnh
7

Conforme mencionado por alguns de vocês acima, setFollowRedirect e setInstanceFollowRedirects só funcionam automaticamente quando o protocolo redirecionado é o mesmo. ou seja, de http para http e https para https.

setFolloRedirect está no nível de classe e define isso para todas as instâncias da conexão url, enquanto setInstanceFollowRedirects é apenas para uma determinada instância. Dessa forma, podemos ter comportamentos diferentes para instâncias diferentes.

Encontrei um exemplo muito bom aqui http://www.mkyong.com/java/java-httpurlconnection-follow-redirect-example/

Shalvika
fonte
2

Outra opção pode ser usar o cliente Apache HttpComponents :

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

Código de amostra:

CloseableHttpClient httpclient = HttpClients.createDefault();
HttpGet httpget = new HttpGet("https://media-hearth.cursecdn.com/avatars/330/498/212.png");
CloseableHttpResponse response = httpclient.execute(httpget);
final HttpEntity entity = response.getEntity();
final InputStream is = entity.getContent();
Koray Tugay
fonte
-4

HTTPUrlConnection não é responsável por manipular a resposta do objeto. É o desempenho esperado, ele pega o conteúdo da URL solicitada. Cabe a você, usuário da funcionalidade, interpretar a resposta. Não é capaz de ler as intenções do desenvolvedor sem especificação.

monástico
fonte
7
Por que setInstanceFollowRedirects neste caso? ))
Shcheklein
Meu palpite é que foi um recurso sugerido para adicionar mais tarde, faz sentido .. meu comentário foi mais refletido em direção a ... a classe é projetada para pegar o conteúdo da web e trazê-lo de volta ... as pessoas podem querer obter mensagens não HTTP 200.
monksy