1 /* 2 * Copyright (c) 2010, 2019, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. 8 * 9 * This code is distributed in the hope that it will be useful, but WITHOUT 10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 12 * version 2 for more details (a copy is included in the LICENSE file that 13 * accompanied this code). 14 * 15 * You should have received a copy of the GNU General Public License version 16 * 2 along with this work; if not, write to the Free Software Foundation, 17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 18 * 19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 20 * or visit www.oracle.com if you need additional information or have any 21 * questions. 22 */ 23 24 /* 25 * @test 26 * @bug 6370908 8220663 27 * @summary Add support for HTTP_CONNECT proxy in Socket class 28 * @modules java.base/sun.net.www 29 * @run main HttpProxy 30 * @run main/othervm -Djava.net.preferIPv4Stack=true HttpProxy 31 * @run main/othervm -Djava.net.preferIPv6Addresses=true HttpProxy 32 */ 33 34 import java.io.IOException; 35 import java.io.InputStream; 36 import java.io.OutputStream; 37 import java.io.PrintWriter; 38 import static java.lang.System.out; 39 import java.net.InetAddress; 40 import java.net.InetSocketAddress; 41 import java.net.Proxy; 42 import java.net.ServerSocket; 43 import java.net.Socket; 44 import java.net.SocketAddress; 45 import java.util.ArrayList; 46 import java.util.List; 47 import sun.net.www.MessageHeader; 48 49 public class HttpProxy { 50 final String proxyHost; 51 final int proxyPort; 52 static final int SO_TIMEOUT = 15000; 53 54 public static void main(String[] args) throws Exception { 55 String host; 56 int port; 57 ConnectProxyTunnelServer proxy = null; 58 if (args.length == 0) { 59 // Start internal proxy 60 proxy = new ConnectProxyTunnelServer(); 61 proxy.start(); 62 host = "localhost"; 63 port = proxy.getLocalPort(); 64 out.println("Running with internal proxy: " + host + ":" + port); 65 } else if (args.length == 2) { 66 host = args[0]; 67 port = Integer.valueOf(args[1]); 68 out.println("Running against specified proxy server: " + host + ":" + port); 69 } else { 70 System.err.println("Usage: java HttpProxy [<proxy host> <proxy port>]"); 71 return; 72 } 73 74 try { 75 HttpProxy p = new HttpProxy(host, port); 76 p.test(); 77 } finally { 78 if (proxy != null) 79 proxy.close(); 80 } 81 } 82 83 public HttpProxy(String proxyHost, int proxyPort) { 84 this.proxyHost = proxyHost; 85 this.proxyPort = proxyPort; 86 } 87 88 void test() throws Exception { 89 InetSocketAddress proxyAddress = new InetSocketAddress(proxyHost, proxyPort); 90 Proxy httpProxy = new Proxy(Proxy.Type.HTTP, proxyAddress); 91 92 try (ServerSocket ss = new ServerSocket(0)) { 93 List<InetSocketAddress> externalAddresses = new ArrayList<>(); 94 externalAddresses.add( 95 new InetSocketAddress(InetAddress.getLocalHost(), ss.getLocalPort())); 96 97 if (!"true".equals(System.getProperty("java.net.preferIPv4Stack"))) { 98 byte[] bytes = new byte[] {0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1}; 99 var address = InetAddress.getByAddress(bytes); 100 externalAddresses.add( 101 new InetSocketAddress(address, ss.getLocalPort())); 102 } 103 104 for (SocketAddress externalAddress : externalAddresses) { 105 try (Socket sock = new Socket(httpProxy)) { 106 sock.setSoTimeout(SO_TIMEOUT); 107 sock.setTcpNoDelay(false); 108 109 out.println("Trying to connect to server socket on " + externalAddress); 110 sock.connect(externalAddress); 111 try (Socket externalSock = ss.accept()) { 112 // perform some simple checks 113 check(sock.isBound(), "Socket is not bound"); 114 check(sock.isConnected(), "Socket is not connected"); 115 check(!sock.isClosed(), "Socket should not be closed"); 116 check(sock.getSoTimeout() == SO_TIMEOUT, 117 "Socket should have a previously set timeout"); 118 check(sock.getTcpNoDelay() == false, "NODELAY should be false"); 119 120 simpleDataExchange(sock, externalSock); 121 } 122 } 123 } 124 } 125 } 126 127 static void check(boolean condition, String message) { 128 if (!condition) out.println(message); 129 } 130 131 static Exception unexpected(Exception e) { 132 out.println("Unexpected Exception: " + e); 133 e.printStackTrace(); 134 return e; 135 } 136 137 // performs a simple exchange of data between the two sockets 138 // and throws an exception if there is any problem. 139 void simpleDataExchange(Socket s1, Socket s2) throws Exception { 140 try (final InputStream i1 = s1.getInputStream(); 141 final InputStream i2 = s2.getInputStream(); 142 final OutputStream o1 = s1.getOutputStream(); 143 final OutputStream o2 = s2.getOutputStream()) { 144 startSimpleWriter("simpleWriter1", o1, 100); 145 startSimpleWriter("simpleWriter2", o2, 200); 146 simpleRead(i2, 100); 147 simpleRead(i1, 200); 148 } 149 } 150 151 void startSimpleWriter(String threadName, final OutputStream os, final int start) { 152 (new Thread(new Runnable() { 153 public void run() { 154 try { simpleWrite(os, start); } 155 catch (Exception e) {unexpected(e); } 156 }}, threadName)).start(); 157 } 158 159 void simpleWrite(OutputStream os, int start) throws Exception { 160 byte b[] = new byte[2]; 161 for (int i=start; i<start+100; i++) { 162 b[0] = (byte) (i / 256); 163 b[1] = (byte) (i % 256); 164 os.write(b); 165 } 166 } 167 168 void simpleRead(InputStream is, int start) throws Exception { 169 byte b[] = new byte [2]; 170 for (int i=start; i<start+100; i++) { 171 int x = is.read(b); 172 if (x == 1) 173 x += is.read(b,1,1); 174 if (x!=2) 175 throw new Exception("read error"); 176 int r = bytes(b[0], b[1]); 177 if (r != i) 178 throw new Exception("read " + r + " expected " +i); 179 } 180 } 181 182 int bytes(byte b1, byte b2) { 183 int i1 = (int)b1 & 0xFF; 184 int i2 = (int)b2 & 0xFF; 185 return i1 * 256 + i2; 186 } 187 188 static class ConnectProxyTunnelServer extends Thread implements AutoCloseable { 189 190 private final ServerSocket ss; 191 private volatile boolean closed; 192 193 public ConnectProxyTunnelServer() throws IOException { 194 ss = new ServerSocket(0); 195 } 196 197 @Override 198 public void run() { 199 try { 200 while (!closed) { 201 try (Socket clientSocket = ss.accept()) { 202 processRequest(clientSocket); 203 } 204 } 205 } catch (Exception e) { 206 if (!closed) { 207 out.println("Proxy Failed: " + e); 208 e.printStackTrace(); 209 } 210 } finally { 211 if (!closed) 212 try { ss.close(); } catch (IOException x) { unexpected(x); } 213 } 214 } 215 216 /** 217 * Returns the port on which the proxy is accepting connections. 218 */ 219 public int getLocalPort() { 220 return ss.getLocalPort(); 221 } 222 223 @Override 224 public void close() throws Exception { 225 closed = true; 226 ss.close(); 227 } 228 229 /* 230 * Processes the CONNECT request 231 */ 232 private void processRequest(Socket clientSocket) throws Exception { 233 MessageHeader mheader = new MessageHeader(clientSocket.getInputStream()); 234 String statusLine = mheader.getValue(0); 235 236 if (!statusLine.startsWith("CONNECT")) { 237 out.println("proxy server: processes only " 238 + "CONNECT method requests, received: " 239 + statusLine); 240 return; 241 } 242 243 // retrieve the host and port info from the status-line 244 InetSocketAddress serverAddr = getConnectInfo(statusLine); 245 246 //open socket to the server 247 try (Socket serverSocket = new Socket(serverAddr.getAddress(), 248 serverAddr.getPort())) { 249 Forwarder clientFW = new Forwarder(clientSocket.getInputStream(), 250 serverSocket.getOutputStream()); 251 Thread clientForwarderThread = new Thread(clientFW, "ClientForwarder"); 252 clientForwarderThread.start(); 253 send200(clientSocket); 254 Forwarder serverFW = new Forwarder(serverSocket.getInputStream(), 255 clientSocket.getOutputStream()); 256 serverFW.run(); 257 clientForwarderThread.join(); 258 } 259 } 260 261 private void send200(Socket clientSocket) throws IOException { 262 OutputStream out = clientSocket.getOutputStream(); 263 PrintWriter pout = new PrintWriter(out); 264 265 pout.println("HTTP/1.1 200 OK"); 266 pout.println(); 267 pout.flush(); 268 } 269 270 /* 271 * This method retrieves the hostname and port of the tunnel destination 272 * from the request line. 273 * @param connectStr 274 * of the form: <i>CONNECT server-name:server-port HTTP/1.x</i> 275 */ 276 static InetSocketAddress getConnectInfo(String connectStr) 277 throws Exception 278 { 279 try { 280 int starti = connectStr.indexOf(' '); 281 int endi = connectStr.lastIndexOf(' '); 282 String connectInfo = connectStr.substring(starti+1, endi).trim(); 283 // retrieve server name and port 284 endi = connectInfo.lastIndexOf(':'); 285 String name = connectInfo.substring(0, endi); 286 if (name.startsWith("[")) { 287 assert name.endsWith("]") : "name:" + name; 288 assert name.contains(":") : "name:" + name; 289 name = name.substring(1, name.length() - 1); 290 } 291 int port = Integer.parseInt(connectInfo.substring(endi+1)); 292 return new InetSocketAddress(name, port); 293 } catch (Exception e) { 294 out.println("Proxy received a request: " + connectStr); 295 throw unexpected(e); 296 } 297 } 298 } 299 300 /* Reads from the given InputStream and writes to the given OutputStream */ 301 static class Forwarder implements Runnable 302 { 303 private final InputStream in; 304 private final OutputStream os; 305 306 Forwarder(InputStream in, OutputStream os) { 307 this.in = in; 308 this.os = os; 309 } 310 311 @Override 312 public void run() { 313 try { 314 byte[] ba = new byte[1024]; 315 int count; 316 while ((count = in.read(ba)) != -1) { 317 os.write(ba, 0, count); 318 } 319 } catch (IOException e) { 320 unexpected(e); 321 } 322 } 323 } 324 }