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 }