Основная прокси-аутентификация для URL-адресов HTTPS возвращает HTTP/1.0 407 Требуется прокси-аутентификация

Я хочу использовать прокси с базовой аутентификацией (имя пользователя, пароль) для подключения (и только этого подключения) в Java. Следующий код работает для URL-адресов HTTP (например, 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();

Код не работает для URL-адресов HTTPS (например, https://www.google.com"), хотя! Я получаю java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.0 407 Proxy Authentication Required", когда пытаюсь получить доступ к URL-адресу HTTPS.

Этот код работает для HTTP и 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();

Проблема со вторым кодом заключается в том, что он устанавливает новый по умолчанию Authenticator, и я не хочу этого делать, потому что этот прокси используется только частью приложения, а другая часть приложения может быть используя другой прокси. Я не хочу устанавливать глобальное значение по умолчанию для всего приложения. Есть ли способ заставить 1-й код работать с HTTPS или использовать Authenticator без его установки по умолчанию?

Мне нужно использовать java.net.HttpURLConnection, потому что я переопределяю метод класса, который должен вернуть HttpURLConnection, поэтому я не могу использовать Apache HttpClient.

Ответы

Ответ 1

Вы можете расширить ProxiedHttpsConnection и самостоятельно обрабатывать все связанные с низким уровнем вещи.

Для подключения к HTTP-прокси на веб-сайт https необходимо выполнить следующие действия:

Примечание: связь с прокси-сервером и сервером http должна находиться в ASCII7.

  • Отправить CONNECT stackoverflow.com:443 HTTP/1.0\r\n прокси-серверу
  • Отправьте свою аутентификацию: Proxy-Authorization: Basic c2F5WW91SGF2ZVNlZW5UaGlzSW5UaGVDb21tZW50cw==\r\n.
  • Завершить первый запрос: \r\n
  • Прочитайте ответ от прокси-сервера, пока не увидите комбинацию "\ r\n\r\n".
  • Разберите первую строку ответа, полученную от прокси, и проверьте, начинается ли она с HTTP/1.0 200.
  • Запустите сеанс SSL поверх существующего соединения.
  • Отправьте начало http-запроса: GET /questions/3304006/persistent-httpurlconnection-in-java HTTP/1.0\r\n
  • Установите правильный заголовок хоста: Host: stackoverflow.com\r\n
  • Завершить запрос на http-сервер: \r\n
  • Прочитайте до \r\n и проанализируйте первую строку как сообщение о статусе
  • Прочитайте до конца потока для запроса тела

Когда мы хотим реализовать класс HttpUrlConnection, мы должны также рассмотреть несколько вещей:

  • В то время, когда класс построен, класс должен хранить данные для будущих подключений, но НЕ делать это напрямую
  • Любые методы можно вызывать в любом порядке
  • Закрытие OutputStream означает, что передача данных выполнена, а не завершение соединения.
  • Каждый api использует методы в другом порядке
  • HTTP-заголовки нечувствительны к регистру, java-карты чувствительны к регистру.

Быстро сказал, что есть только много подводных камней

В классе, который я разработал, он использует логические флаги для запоминания, если вызывается метод connect и методы afterPostClosure, он также имеет поддержку, если getInputStream() вызывается до закрытия OutputStream.

Этот класс также использует как можно меньшую упаковку поверх потоков, возвращаемых сокетом, чтобы не было действительно сложным.

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;
    }
}

Текущие ошибки с указанным выше кодом:

  • Потоки не закрываются при ошибках во время публикации
  • Потоки не закрываются во время ошибок с начальным контактом с прокси-сервером
  • Он не поддерживает перенаправления http
  • Он не поддерживает HTTP 1.1 такие вещи, как chunked и gzip-кодирование, но это не проблема, поскольку мы объявляем себя клиентом http1.0.

Вышеприведенный код может использоваться как:

    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);
        }
    }

Если вы собираетесь использовать это с помощью своего рода прокси-селектора, вы должны проверить протокол URL-адреса, чтобы убедиться, что его http или https, если его http, не используют этот класс, и вместо этого присоединяют заголовок вручную как:

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

Почему бы не использовать httpsUrlConnection.setSSLSocketFactory

В то время как java имеет этот метод, попытки использовать его покажут вам, почему это не сработает, java просто продолжает называть createSocket(Socket s, String host, int port, boolean autoClose) с уже открытым соединением, что делает невозможным создание прокси файла вручную.

Ответ 2

К сожалению, нет простого решения для того, чего вы пытаетесь достичь. Ваш первый код не работает с HTTPS, потому что вы напрямую настраиваете заголовок аутентификации. Поскольку клиент шифрует все данные, прокси-сервер не имеет возможности извлечь какую-либо информацию из запроса.

Фактически HTTPS и прокси-серверы работают противоположными способами. Прокси-сервер хочет видеть все данные, которые протекают между клиентом и конечным сервером, и принимать меры в зависимости от того, что он видит. С другой стороны, протокол HTTPS шифрует все данные, чтобы никто не мог видеть данные до тех пор, пока не достигнет конечного адресата. Алгоритм шифрования согласовывается между клиентом и конечным пунктом назначения, чтобы прокси-сервер не мог расшифровать какую-либо информацию, на самом деле он даже не знает, какой протокол использует клиент.

Чтобы использовать прокси-сервер в HTTPS-соединении, клиент должен установить туннель. Для этого он должен выдать команду CONNECT непосредственно прокси, например:

CONNECT www.google.com:443 HTTP/1.0

и отправьте учетные данные для аутентификации с помощью прокси-сервера.

Если соединение выполнено успешно, клиент может отправлять и получать данные через соединение. Прокси-сервер полностью слеп к данным. Данные только проходят через него на своем пути между клиентом и сервером.

Когда вы выполняете url.openConnection(proxy) по URL-адресу HTTP, он возвращает экземпляр HttpURLConnection при запуске по URL-адресу HTTPS, как в вашем втором коде, он перенастраивает экземпляр HttpsURLConnection.

Вы получаете код ошибки 407, потому что прокси-сервер не может извлекать данные аутентификации из отправленного вами заголовка. Посмотрев на стек исключений, мы увидим, что исключение выбрано в sun.net.www.protocol.http.HttpURLConnection.doTunneling(), которое выдает команду CONNECT для установки туннеля HTTPS через прокси. В исходном коде для sun.net.www.protocol.http.HttpURLConnection мы можем видеть:

/* 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;

Итак, кажется, что аутентификатор по умолчанию является единственным способом предоставления учетных данных прокси.

Чтобы сделать то, что вы хотите, вам нужно будет перейти на уровень соединения и обработать HTTP-протокол самостоятельно, потому что вам нужно поговорить с прокси-сервером напрямую с сервером Google.

Ответ 3

Можете ли вы использовать HttpsUrlConnection? Он расширяет HttpUrlConnection, поэтому отключение к HttpUrlConnection может быть нормально при возврате из класса.

Код аналогичен, вместо HttpUrlConnection используйте один с https в имени.

Используйте следующий код:

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

Источники:

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

[2] HttpURLConnection - "https://" vs. "http://" (фрагмент)

Ответ 4

Хорошо, вот что вам нужно сделать,

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;
    }
}

Используйте его как.

HttpURLConnection conn = new ProxySetup().proxySetup(URL)