一尘不染

HTTPS URL的基本代理身份验证返回HTTP / 1.0 407必需的代理身份验证

java

我想将具有基本身份验证(用户名,密码)的代理用于Java中的连接(并且仅此连接)。以下代码适用于HTTP URL(例如“
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();

但是,该代码不适用于HTTPS URL(例如“ https://www.google.com
”)!我收到java.io.IOException: Unable to tunnel through proxy. Proxy returns "HTTP/1.0 407 Proxy Authentication Required"的时候我尝试访问HTTPS URL。

此代码适用于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而我不想这样做,因为此代理仅由应用程序的一部分使用,而应用程序的不同部分可能正在使用其他代理。我不想为整个应用程序设置全局默认值。有没有一种方法可以让第一个代码与HTTPS一起使用,或者有一种方法可以在Authenticator不将其设置为默认值的情况下使用它?

我必须使用java.net.HttpURLConnection,因为我要重写必须返回an的类的方法HttpURLConnection,所以我不能使用Apache
HttpClient。


阅读 189

收藏
2020-12-03

共1个答案

一尘不染

您可以自己扩展ProxiedHttpsConnection和处理所有与底层相关的内容。

需要执行以下步骤以通过HTTP代理与https网站建立连接:

注意:与代理服务器和http服务器的通信应使用ASCII7

  1. 发送CONNECT stackoverflow.com:443 HTTP/1.0\r\n给代理
  2. 发送您的身份验证:Proxy-Authorization: Basic c2F5WW91SGF2ZVNlZW5UaGlzSW5UaGVDb21tZW50cw==\r\n
  3. 结束第一个请求: \r\n
  4. 阅读来自代理的响应,直到看到组合“ \ r \ n \ r \ n”。
  5. 解析您从代理获得的响应的第一行,并检查其是否以开头HTTP/1.0 200
  6. 在现有连接上就地启动SSL会话。
  7. 发送http请求的开始: GET /questions/3304006/persistent-httpurlconnection-in-java HTTP/1.0\r\n
  8. 设置正确的主机头: Host: stackoverflow.com\r\n
  9. 将请求结束到http服务器: \r\n
  10. 阅读直到\r\n并将第一行解析为状态消息
  11. 读取直到流结束以获取请求正文

当我们想要实现HttpUrlConnection类时,我们还需要考虑以下几点:

  • 在构造类时,该类应存储数据以备将来连接,但不要直接使其
  • 可以按任何顺序调用任何方法
  • 闭合OutputStream意味着数据传输已经完成,而不是意味着连接必须完成
  • 每个api以不同的顺序使用方法
  • HTTP标头不区分大小写,Java映射区分大小写。

迅速地说,有很多陷阱

在我设计的类中,它使用布尔标志来记住connect方法和afterPostClosure方法是否被调用,并且还具有getInputStream()OutputStream关闭之前调用if的支持。

此类还对套接字返回的流使用了尽可能少的包装,以防止过于复杂。

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重定向
  • 它不支持诸如分块和gzip编码之类的http 1.1,但这并不是问题,因为我们宣布自己为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)通过已打开的连接不断调用,从而无法手动进行代理操作。

2020-12-03