1 /* 2 * Copyright (c) 2001, 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 * 26 * This class includes a proxy server that processes HTTP CONNECT requests, 27 * and tunnels the data from the client to the server, once the CONNECT 28 * request is accepted. 29 * The proxy server processes only one transaction, i.e a CONNECT request 30 * followed by the corresponding tunnel data. 31 */ 32 33 import java.io.*; 34 import java.net.*; 35 import javax.net.ssl.*; 36 import javax.net.ServerSocketFactory; 37 import sun.net.www.*; 38 39 public class ProxyTunnelServer extends Thread { 40 41 private static ServerSocket ss = null; 42 /* 43 * holds the registered user's username and password 44 * only one such entry is maintained 45 */ 46 private String userPlusPass; 47 48 // client requesting for a tunnel 49 private Socket clientSocket = null; 50 51 /* 52 * Origin server's address and port that the client 53 * wants to establish the tunnel for communication. 54 */ 55 private InetAddress serverInetAddr; 56 private int serverPort; 57 58 /* 59 * denote whether the proxy needs to authorize 60 * CONNECT requests. 61 */ 62 static boolean needAuth = false; 63 64 public ProxyTunnelServer() throws IOException { 65 if (ss == null) { 66 ss = (ServerSocket) ServerSocketFactory.getDefault(). 67 createServerSocket(0); 68 } 69 } 70 71 public void needUserAuth(boolean auth) { 72 needAuth = auth; 73 } 74 75 /* 76 * register users with the proxy, by providing username and 77 * password. The username and password are used for authorizing the 78 * user when a CONNECT request is made and needAuth is set to true. 79 */ 80 public void setUserAuth(String uname, String passwd) { 81 userPlusPass = uname + ":" + passwd; 82 } 83 84 public void run() { 85 try { 86 clientSocket = ss.accept(); 87 processRequests(); 88 } catch (Exception e) { 89 System.out.println("Proxy Failed: " + e); 90 e.printStackTrace(); 91 try { 92 ss.close(); 93 } 94 catch (IOException excep) { 95 System.out.println("ProxyServer close error: " + excep); 96 excep.printStackTrace(); 97 } 98 } 99 } 100 101 /* 102 * Processes the CONNECT requests, if needAuth is set to true, then 103 * the name and password are extracted from the Proxy-Authorization header 104 * of the request. They are checked against the one that is registered, 105 * if there is a match, connection is set in tunneling mode. If 106 * needAuth is set to false, Proxy-Authorization checks are not made 107 */ 108 private void processRequests() throws Exception { 109 110 InputStream in = clientSocket.getInputStream(); 111 MessageHeader mheader = new MessageHeader(in); 112 String statusLine = mheader.getValue(0); 113 114 if (statusLine.startsWith("CONNECT")) { 115 // retrieve the host and port info from the status-line 116 retrieveConnectInfo(statusLine); 117 if (needAuth) { 118 String authInfo; 119 if ((authInfo = mheader.findValue("Proxy-Authorization")) 120 != null) { 121 if (authenticate(authInfo)) { 122 needAuth = false; 123 System.out.println( 124 "Proxy: client authentication successful"); 125 } 126 } 127 } 128 respondForConnect(needAuth); 129 130 // connection set to the tunneling mode 131 if (!needAuth) { 132 doTunnel(); 133 /* 134 * done with tunneling, we process only one successful 135 * tunneling request 136 */ 137 ss.close(); 138 } else { 139 // we may get another request with Proxy-Authorization set 140 in.close(); 141 clientSocket.close(); 142 restart(); 143 } 144 } else { 145 System.out.println("proxy server: processes only " 146 + "CONNECT method requests, recieved: " 147 + statusLine); 148 } 149 } 150 151 private void respondForConnect(boolean needAuth) throws Exception { 152 153 OutputStream out = clientSocket.getOutputStream(); 154 PrintWriter pout = new PrintWriter(out); 155 156 if (needAuth) { 157 pout.println("HTTP/1.1 407 Proxy Auth Required"); 158 pout.println("Proxy-Authenticate: Basic realm=\"WallyWorld\""); 159 pout.println(); 160 pout.flush(); 161 out.close(); 162 } else { 163 pout.println("HTTP/1.1 200 OK"); 164 pout.println(); 165 pout.flush(); 166 } 167 } 168 169 private void restart() throws IOException { 170 (new Thread(this)).start(); 171 } 172 173 /* 174 * note: Tunneling has to be provided in both directions, i.e 175 * from client->server and server->client, even if the application 176 * data may be unidirectional, SSL handshaking data flows in either 177 * direction. 178 */ 179 private void doTunnel() throws Exception { 180 181 Socket serverSocket = new Socket(serverInetAddr, serverPort); 182 ProxyTunnel clientToServer = new ProxyTunnel( 183 clientSocket, serverSocket); 184 ProxyTunnel serverToClient = new ProxyTunnel( 185 serverSocket, clientSocket); 186 clientToServer.start(); 187 serverToClient.start(); 188 System.out.println("Proxy: Started tunneling......."); 189 190 clientToServer.join(); 191 serverToClient.join(); 192 System.out.println("Proxy: Finished tunneling........"); 193 194 clientToServer.close(); 195 serverToClient.close(); 196 } 197 198 /* 199 * This inner class provides unidirectional data flow through the sockets 200 * by continuously copying bytes from the input socket onto the output 201 * socket, until both sockets are open and EOF has not been received. 202 */ 203 class ProxyTunnel extends Thread { 204 Socket sockIn; 205 Socket sockOut; 206 InputStream input; 207 OutputStream output; 208 209 public ProxyTunnel(Socket sockIn, Socket sockOut) 210 throws Exception { 211 this.sockIn = sockIn; 212 this.sockOut = sockOut; 213 input = sockIn.getInputStream(); 214 output = sockOut.getOutputStream(); 215 } 216 217 public void run() { 218 int BUFFER_SIZE = 400; 219 byte[] buf = new byte[BUFFER_SIZE]; 220 int bytesRead = 0; 221 int count = 0; // keep track of the amount of data transfer 222 223 try { 224 while ((bytesRead = input.read(buf)) >= 0) { 225 output.write(buf, 0, bytesRead); 226 output.flush(); 227 count += bytesRead; 228 } 229 } catch (IOException e) { 230 /* 231 * The peer end has closed the connection 232 * we will close the tunnel 233 */ 234 close(); 235 } 236 } 237 238 public void close() { 239 try { 240 if (!sockIn.isClosed()) 241 sockIn.close(); 242 if (!sockOut.isClosed()) 243 sockOut.close(); 244 } catch (IOException ignored) { } 245 } 246 } 247 248 /* 249 *************************************************************** 250 * helper methods follow 251 *************************************************************** 252 */ 253 254 /* 255 * This method retrieves the hostname and port of the destination 256 * that the connect request wants to establish a tunnel for 257 * communication. 258 * The input, connectStr is of the form: 259 * CONNECT server-name:server-port HTTP/1.x 260 */ 261 private void retrieveConnectInfo(String connectStr) throws Exception { 262 263 int starti; 264 int endi; 265 String connectInfo; 266 String serverName = null; 267 try { 268 starti = connectStr.indexOf(' '); 269 endi = connectStr.lastIndexOf(' '); 270 connectInfo = connectStr.substring(starti+1, endi).trim(); 271 // retrieve server name and port 272 endi = connectInfo.indexOf(':'); 273 serverName = connectInfo.substring(0, endi); 274 serverPort = Integer.parseInt(connectInfo.substring(endi+1)); 275 } catch (Exception e) { 276 throw new IOException("Proxy recieved a request: " 277 + connectStr); 278 } 279 serverInetAddr = InetAddress.getByName(serverName); 280 } 281 282 public int getPort() { 283 return ss.getLocalPort(); 284 } 285 286 /* 287 * do "basic" authentication, authInfo is of the form: 288 * Basic <encoded username":"password> 289 * reference RFC 2617 290 */ 291 private boolean authenticate(String authInfo) throws IOException { 292 boolean matched = false; 293 try { 294 authInfo.trim(); 295 int ind = authInfo.indexOf(' '); 296 String recvdUserPlusPass = authInfo.substring(ind + 1).trim(); 297 // extract encoded (username:passwd 298 if (userPlusPass.equals( 299 new String( 300 (new sun.misc.BASE64Decoder()). 301 decodeBuffer(recvdUserPlusPass) 302 ))) { 303 matched = true; 304 } 305 } catch (Exception e) { 306 throw new IOException( 307 "Proxy received invalid Proxy-Authorization value: " 308 + authInfo); 309 } 310 return matched; 311 } 312 }