web-dev-qa-db-fra.com

L'authentification proxy de base pour les URL HTTPS renvoie HTTP/1.0 Authentification proxy 407 requise

Je souhaite utiliser un proxy avec une authentification de base (nom d'utilisateur, mot de passe) pour une connexion (et uniquement cette connexion) en Java. Le code suivant fonctionne pour les URL HTTP (par exemple, " http://www.google.com "):

URL url = new URL("http://www.google.com");
HttpURLConnection httpURLConnection = null;
InetSocketAddress proxyLocation = new InetSocketAddress(proxyHost, proxyPort);
Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyLocation);
httpURLConnection = (HttpURLConnection) url.openConnection(proxy);
// Works for HTTP only! Doesn't work for HTTPS!
String encoded = new Sun.misc.BASE64Encoder().encodeBuffer((proxyUserName + ":" + proxyPassword).getBytes()).replace("\r\n", "");
httpURLConnection.setRequestProperty("Proxy-Authorization", "Basic " + encoded);
InputStream is = httpURLConnection.getInputStream();
InputStreamReader isr = new InputStreamReader(is); 
int data = isr.read();
while(data != -1){
  char c = (char) data;
  data = isr.read();
  System.out.print(c);
}
isr.close();

Le code ne fonctionne pas pour les URL HTTPS (par exemple, " https://www.google.com "), cependant! Je reçois Java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.0 407 Proxy Authentication Required" lorsque j'essaie d'accéder à une URL HTTPS.

Ce code fonctionne pour HTTP et HTTPS:

URL url = new URL("https://www.google.com");
HttpURLConnection httpURLConnection = null;
InetSocketAddress proxyLocation = new InetSocketAddress(proxyHost, proxyPort);
Proxy proxy = new Proxy(Proxy.Type.HTTP, proxyLocation);
httpURLConnection = (HttpURLConnection) url.openConnection(proxy);
// Works for HTTP and HTTPS, but sets a global default!
Authenticator.setDefault(new Authenticator() {
  protected PasswordAuthentication getPasswordAuthentication() {
    return new PasswordAuthentication(proxyUserName, proxyPassword.toCharArray());
  }
});
InputStream is = httpURLConnection.getInputStream();
InputStreamReader isr = new InputStreamReader(is); 
int data = isr.read();
while(data != -1){
  char c = (char) data;
  data = isr.read();
  System.out.print(c);
}
isr.close();

Le problème avec le deuxième code est qu’il définit une nouvelle valeur par défaut Authenticator et que je ne veux pas le faire, car ce proxy n’est utilisé que par une partie de l’application et une autre partie de l’application pourrait utiliser un proxy différent. Je ne veux pas définir une valeur par défaut globale pour l'ensemble de l'application. Existe-t-il un moyen de faire en sorte que le premier code fonctionne avec HTTPS ou d'utiliser un Authenticator sans le définir par défaut?

Je dois utiliser Java.net.HttpURLConnection, car je redéfinis une méthode d'une classe qui doit renvoyer un HttpURLConnection, je ne peux donc pas utiliser Apache HttpClient.

18
John

Vous pouvez étendre ProxiedHttpsConnection et gérer vous-même tout ce qui concerne les bas niveaux.

Les étapes suivantes doivent être effectuées pour établir une connexion via un proxy HTTP à un site Web https:

Remarque: la communication avec le proxy et le serveur http doit être dans ASCII7 .

  1. Envoyez CONNECT stackoverflow.com:443 HTTP/1.0\r\n au proxy
  2. Envoyez votre authentification: Proxy-Authorization: Basic c2F5WW91SGF2ZVNlZW5UaGlzSW5UaGVDb21tZW50cw==\r\n.
  3. Terminez la première demande: \r\n
  4. Lisez la réponse du proxy jusqu'à ce que la combinaison "\ r\n\r\n" apparaisse.
  5. Analysez la première ligne de la réponse que vous avez obtenue du proxy et vérifiez si elle commence par HTTP/1.0 200.
  6. Démarrer une session SSL en place sur la connexion existante.
  7. Envoyer le début d'une demande http: GET /questions/3304006/persistent-httpurlconnection-in-Java HTTP/1.0\r\n
  8. Définissez l'en-tête d'hôte approprié: Host: stackoverflow.com\r\n
  9. Terminez la demande sur le serveur http: \r\n
  10. Lire jusqu'au \r\n et analyser la première ligne en tant que message d'état
  11. Lire jusqu'à la fin du flux pour le corps de la demande

Lorsque nous voulons implémenter la classe HttpUrlConnection, nous devons également tenir compte de quelques points:

  • Au moment de la construction de la classe, la classe devrait stocker les données pour les connexions futures, mais NE PAS les faire directement
  • Toutes les méthodes peuvent être appelées dans n'importe quel ordre
  • La fermeture de la OutputStream signifie que le transfert de données est terminé et que la connexion ne doit pas se terminer
  • Chaque api utilise les méthodes dans un ordre différent
  • Les en-têtes HTTP ne sont pas sensibles à la casse, les cartes Java sont sensibles à la casse.

Rapidement dit, il y a juste de nombreux pièges

Dans la classe que j'ai conçue, elle utilise des indicateurs booléens pour se rappeler si la méthode connect et les méthodes afterPostClosure sont appelées. Elle est également prise en charge si getInputStream() est appelé avant la fermeture de OutputStream

Cette classe utilise également le moins de wrapping possible sur les flux renvoyés par le socket, pour éviter toute complexité.

public class ProxiedHttpsConnection extends HttpURLConnection {

    private final String proxyHost;
    private final int proxyPort;
    private static final byte[] NEWLINE = "\r\n".getBytes();//should be "ASCII7"

    private Socket socket;
    private final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private final Map<String, List<String>> sendheaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private final Map<String, List<String>> proxyheaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private final Map<String, List<String>> proxyreturnheaders = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
    private int statusCode;
    private String statusLine;
    private boolean isDoneWriting;

    public ProxiedHttpsConnection(URL url,
            String proxyHost, int proxyPort, String username, String password)
            throws IOException {
        super(url);
        socket = new Socket();
        this.proxyHost = proxyHost;
        this.proxyPort = proxyPort;
        String encoded = Base64.encode((username + ":" + password).getBytes())
                .replace("\r\n", "");
        proxyheaders.put("Proxy-Authorization", new ArrayList<>(Arrays.asList("Basic " + encoded)));
    }

    @Override
    public OutputStream getOutputStream() throws IOException {
        connect();
        afterWrite();
        return new FilterOutputStream(socket.getOutputStream()) {
            @Override
            public void write(byte[] b, int off, int len) throws IOException {
                out.write(String.valueOf(len).getBytes());
                out.write(NEWLINE);
                out.write(b, off, len);
                out.write(NEWLINE);
            }

            @Override
            public void write(byte[] b) throws IOException {
                out.write(String.valueOf(b.length).getBytes());
                out.write(NEWLINE);
                out.write(b);
                out.write(NEWLINE);
            }

            @Override
            public void write(int b) throws IOException {
                out.write(String.valueOf(1).getBytes());
                out.write(NEWLINE);
                out.write(b);
                out.write(NEWLINE);
            }

            @Override
            public void close() throws IOException {
                afterWrite();
            }

        };
    }

    private boolean afterwritten = false;

    @Override
    public InputStream getInputStream() throws IOException {
        connect();
        return socket.getInputStream();

    }

    @Override
    public void setRequestMethod(String method) throws ProtocolException {
        this.method = method;
    }

    @Override
    public void setRequestProperty(String key, String value) {
        sendheaders.put(key, new ArrayList<>(Arrays.asList(value)));
    }

    @Override
    public void addRequestProperty(String key, String value) {
        sendheaders.computeIfAbsent(key, l -> new ArrayList<>()).add(value);
    }

    @Override
    public Map<String, List<String>> getHeaderFields() {
        return headers;
    }

    @Override
    public void connect() throws IOException {
        if (connected) {
            return;
        }
        connected = true;
        socket.setSoTimeout(getReadTimeout());
        socket.connect(new InetSocketAddress(proxyHost, proxyPort), getConnectTimeout());
        StringBuilder msg = new StringBuilder();
        msg.append("CONNECT ");
        msg.append(url.getHost());
        msg.append(':');
        msg.append(url.getPort() == -1 ? 443 : url.getPort());
        msg.append(" HTTP/1.0\r\n");
        for (Map.Entry<String, List<String>> header : proxyheaders.entrySet()) {
            for (String l : header.getValue()) {
                msg.append(header.getKey()).append(": ").append(l);
                msg.append("\r\n");
            }
        }

        msg.append("Connection: close\r\n");
        msg.append("\r\n");
        byte[] bytes;
        try {
            bytes = msg.toString().getBytes("ASCII7");
        } catch (UnsupportedEncodingException ignored) {
            bytes = msg.toString().getBytes();
        }
        socket.getOutputStream().write(bytes);
        socket.getOutputStream().flush();
        byte reply[] = new byte[200];
        byte header[] = new byte[200];
        int replyLen = 0;
        int headerLen = 0;
        int newlinesSeen = 0;
        boolean headerDone = false;
        /* Done on first newline */
        InputStream in = socket.getInputStream();
        while (newlinesSeen < 2) {
            int i = in.read();
            if (i < 0) {
                throw new IOException("Unexpected EOF from remote server");
            }
            if (i == '\n') {
                if (newlinesSeen != 0) {
                    String h = new String(header, 0, headerLen);
                    String[] split = h.split(": ");
                    if (split.length != 1) {
                        proxyreturnheaders.computeIfAbsent(split[0], l -> new ArrayList<>()).add(split[1]);
                    }
                }
                headerDone = true;
                ++newlinesSeen;
                headerLen = 0;
            } else if (i != '\r') {
                newlinesSeen = 0;
                if (!headerDone && replyLen < reply.length) {
                    reply[replyLen++] = (byte) i;
                } else if (headerLen < reply.length) {
                    header[headerLen++] = (byte) i;
                }
            }
        }

        String replyStr;
        try {
            replyStr = new String(reply, 0, replyLen, "ASCII7");
        } catch (UnsupportedEncodingException ignored) {
            replyStr = new String(reply, 0, replyLen);
        }

        // Some proxies return http/1.1, some http/1.0 even we asked for 1.0
        if (!replyStr.startsWith("HTTP/1.0 200") && !replyStr.startsWith("HTTP/1.1 200")) {
            throw new IOException("Unable to tunnel. Proxy returns \"" + replyStr + "\"");
        }
        SSLSocket s = (SSLSocket) ((SSLSocketFactory) SSLSocketFactory.getDefault())
                .createSocket(socket, url.getHost(), url.getPort(), true);
        s.startHandshake();
        socket = s;
        msg.setLength(0);
        msg.append(method);
        msg.append(" ");
        msg.append(url.toExternalForm().split(String.valueOf(url.getPort()), -2)[1]);
        msg.append(" HTTP/1.0\r\n");
        for (Map.Entry<String, List<String>> h : sendheaders.entrySet()) {
            for (String l : h.getValue()) {
                msg.append(h.getKey()).append(": ").append(l);
                msg.append("\r\n");
            }
        }
        if (method.equals("POST") || method.equals("PUT")) {
            msg.append("Transfer-Encoding: Chunked\r\n");
        }
        msg.append("Host: ").append(url.getHost()).append("\r\n");
        msg.append("Connection: close\r\n");
        msg.append("\r\n");
        try {
            bytes = msg.toString().getBytes("ASCII7");
        } catch (UnsupportedEncodingException ignored) {
            bytes = msg.toString().getBytes();
        }
        socket.getOutputStream().write(bytes);
        socket.getOutputStream().flush();
    }

    private void afterWrite() throws IOException {
        if (afterwritten) {
            return;
        }
        afterwritten = true;
        socket.getOutputStream().write(String.valueOf(0).getBytes());
        socket.getOutputStream().write(NEWLINE);
        socket.getOutputStream().write(NEWLINE);
        byte reply[] = new byte[200];
        byte header[] = new byte[200];
        int replyLen = 0;
        int headerLen = 0;
        int newlinesSeen = 0;
        boolean headerDone = false;
        /* Done on first newline */
        InputStream in = socket.getInputStream();
        while (newlinesSeen < 2) {
            int i = in.read();
            if (i < 0) {
                throw new IOException("Unexpected EOF from remote server");
            }
            if (i == '\n') {
                if (headerDone) {
                    String h = new String(header, 0, headerLen);
                    String[] split = h.split(": ");
                    if (split.length != 1) {
                        headers.computeIfAbsent(split[0], l -> new ArrayList<>()).add(split[1]);
                    }
                }
                headerDone = true;
                ++newlinesSeen;
                headerLen = 0;
            } else if (i != '\r') {
                newlinesSeen = 0;
                if (!headerDone && replyLen < reply.length) {
                    reply[replyLen++] = (byte) i;
                } else if (headerLen < header.length) {
                    header[headerLen++] = (byte) i;
                }
            }
        }

        String replyStr;
        try {
            replyStr = new String(reply, 0, replyLen, "ASCII7");
        } catch (UnsupportedEncodingException ignored) {
            replyStr = new String(reply, 0, replyLen);
        }

        /* We asked for HTTP/1.0, so we should get that back */
        if ((!replyStr.startsWith("HTTP/1.0 200")) && !replyStr.startsWith("HTTP/1.1 200")) {
            throw new IOException("Server returns \"" + replyStr + "\"");
        }
    }

    @Override
    public void disconnect() {
        try {
            socket.close();
        } catch (IOException ex) {
            Logger.getLogger(ProxiedHttpsConnection.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    @Override
    public boolean usingProxy() {
        return true;
    }
}

Bogues actuels avec le code ci-dessus:

  • Les flux ne sont pas fermés en cas d'erreur pendant le post
  • Les flux ne sont pas fermés lors d'erreurs lors du premier contact avec le proxy
  • Il ne supporte pas les redirections http
  • Il ne prend pas en charge les éléments http 1.1 tels que l'encodage chunked et gzip, mais ce n'est pas un problème puisque nous nous annonçons en tant que client http1.0.

Le code ci-dessus peut être utilisé comme:

    ProxiedHttpsConnection n = new ProxiedHttpsConnection(
            new URL("https://stackoverflow.com:443/questions/3304006/persistent-httpurlconnection-in-Java"), 
            "proxy.example.com", 8080, "root", "flg83yvem#");
    n.setRequestMethod("GET");
    n.addRequestProperty("User-Agent", "Java test https://stackoverflow.com/users/1542723/ferrybig");
    //try (OutputStream out = n.getOutputStream()) {
    //  out.write("Hello?".getBytes());
    //}
    try (InputStream in = n.getInputStream()) {
        byte[] buff = new byte[1024];
        int length;
        while ((length = in.read(buff)) >= 0) {
            System.out.write(buff, 0, length);
        }
    }

Si vous allez utiliser cela avec une sorte de sélecteur de proxy, vous devriez vérifier le protocole de l'URL pour voir si son http ou https, si c'est http, n'utilisez pas cette classe, et attachez l'en-tête manuellement comme ceci:

httpURLConnection.setRequestProperty("Proxy-Authorization", "Basic " + encoded);

Pourquoi ne pas utiliser httpsUrlConnection.setSSLSLSocketFactory

Bien que Java utilise cette méthode, les tentatives d'utilisation vous montreront pourquoi cela ne fonctionne pas, Java continue d'appeler le createSocket(Socket s, String Host, int port, boolean autoClose) avec une connexion déjà ouverte, rendant impossible la création manuelle du proxy.

3
Ferrybig

Malheureusement, il n’existe pas de solution simple à ce que vous essayez d’atteindre. Votre 1er code ne fonctionne pas avec HTTPS car vous définissez directement l'en-tête d'authentification. Comme le client chiffre toutes les données, le serveur proxy n’a aucun moyen d’extraire des informations de la requête.

En fait, HTTPS et les serveurs proxy fonctionnent de manière opposée. Le serveur proxy veut voir toutes les données qui circulent entre le client et le serveur final et agir en fonction de ce qu'il voit. D'autre part, le protocole HTTPS chiffre toutes les données de sorte que personne ne puisse voir les données avant qu'elles atteignent la destination finale. L'algorithme de chiffrement est négocié entre le client et la destination finale de sorte que le serveur proxy ne puisse déchiffrer aucune information. En fait, il ne sait même pas quel protocole le client utilise.

Pour utiliser un serveur proxy sur une connexion HTTPS, le client doit établir un tunnel. Pour ce faire, il doit émettre une commande CONNECT directement au proxy, par exemple:

CONNECT www.google.com:443 HTTP/1.0

et envoyez les informations d'identification pour vous authentifier auprès du serveur proxy.

Si la connexion est établie, le client peut envoyer et recevoir des données via la connexion. Le serveur proxy est complètement aveugle aux données. Les données ne les transitent que par le chemin entre le client et le serveur.

Lorsque vous exécutez url.openConnection(proxy) sur une URL HTTP, il retourne une instance de HttpURLConnection. Lorsqu'il est exécuté sur une URL HTTPS comme dans votre deuxième code, il renvoie une instance de HttpsURLConnection.

Vous recevez le code d'erreur 407 car le serveur proxy ne peut pas extraire les informations d'authentification de l'en-tête que vous avez envoyé. En examinant la pile d'exceptions, nous pouvons voir que l'exception est levée à Sun.net.www.protocol.http.HttpURLConnection.doTunneling() qui émet la commande CONNECT pour établir le tunnel HTTPS via le proxy. Dans le code source de Sun.net.www.protocol.http.HttpURLConnection on peut voir:

/* We only have a single static authenticator for now.
 * REMIND:  backwards compatibility with JDK 1.1.  Should be
 * eliminated for JDK 2.0.
 */
private static HttpAuthenticator defaultAuth;

Il semble donc que l'authentificateur par défaut est le seul moyen de fournir les informations d'identification du proxy.

Pour faire ce que vous voulez, vous devez descendre au niveau de la connexion et gérer vous-même le protocole HTTP, car vous devez parler au serveur proxy, pas directement au serveur Google.

6
whbogado

Pouvez-vous utiliser HttpsUrlConnection? Il étend HttpUrlConnection, le transtypage vers HttpUrlConnection peut donc être correct lors du retour de la classe.

Le code est similaire, à la place de HttpUrlConnection, utilisez-en un avec https dans le nom.

Utilisez le code suivant:

if (testUrlHttps.getProtocol().toLowerCase().equals("https")) {
   trustAllHosts();
   HttpsURLConnection https = (HttpsURLConnection) url.openConnection();
   https.setHostnameVerifier(DO_NOT_VERYFY);
   urlCon = https;
} else {
   urlCon = (HttpURLConnection) url.openConnection();
}

Sources:

[1] https://docs.Oracle.com/javase/7/docs/api/javax/net/ssl/HttpsURLConnection.html

[2] HttpURLConnection - "https: //" vs. "http: //" (extrait)

2
mico

Ok c'est ce que vous devez faire,

public class ProxyAuth extends Authenticator {
    private PasswordAuthentication auth;

    ProxyAuth(String user, String password) {
        auth = new PasswordAuthentication(user, password == null ? new char[]{} : password.toCharArray());
    }

    protected PasswordAuthentication getPasswordAuthentication() {
        return auth;
    }
}

.

public class ProxySetup {
    public HttpURLConnection proxySetup(String urlInput)
    {
        URL url;
        try {
            url = new URL(urlInput);

            Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("10.66.182.100", 80)); // or whatever your proxy is
            HttpURLConnection uc = (HttpURLConnection)url.openConnection(proxy);
            System.setProperty("https.proxyHost", "10.66.182.100");
            System.setProperty("https.proxyPort", "80");
            System.setProperty("http.proxyHost", "10.66.182.100");
            System.setProperty("http.proxyPort", "80");
            String encoded = new String(Base64.encodeBase64(("domain\\Username" + ":" + "Password").getBytes()));

            uc.setRequestProperty("Proxy-Authorization", "Basic " + encoded);
            Authenticator.setDefault(new ProxyAuth("domain\\Username", "Password"));

            System.out.println("ProxySetup : proxySetup");
            return uc;
        } catch (Exception e) {
            // TODO Auto-generated catch block
            System.out.println("ProxySetup : proxySetup - Failed");
            e.printStackTrace();
        }
        return null;
    }
}

Utilisez-le comme.

HttpURLConnection conn = new ProxySetup().proxySetup(URL)
0
Abhishek Anand