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 }