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