1 /*
   2  * Copyright (c) 2015, 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 import java.net.*;
  25 import java.io.*;
  26 import java.util.*;
  27 import java.security.*;
  28 import java.util.concurrent.CopyOnWriteArrayList;
  29 import static java.nio.charset.StandardCharsets.UTF_8;
  30 import static java.util.Arrays.asList;
  31 import static java.util.stream.Collectors.toList;
  32 
  33 /**
  34  * A minimal proxy server that supports CONNECT tunneling. It does not do
  35  * any header transformations. In future this could be added.
  36  * Two threads are created per client connection. So, it's not
  37  * intended for large numbers of parallel connections.
  38  */
  39 public class ProxyServer extends Thread implements Closeable {
  40 
  41     ServerSocket listener;
  42     int port;
  43     volatile boolean debug;
  44     private final Credentials credentials;  // may be null
  45 
  46     private static class Credentials {
  47         private final String name;
  48         private final String password;
  49         private Credentials(String name, String password) {
  50             this.name = name;
  51             this.password = password;
  52         }
  53         public String name() { return name; }
  54         public String password() { return password; }
  55     }
  56 
  57     /**
  58      * Create proxy on port (zero means don't care). Call getPort()
  59      * to get the assigned port.
  60      */
  61     public ProxyServer(Integer port) throws IOException {
  62         this(port, false);
  63     }
  64 
  65     public ProxyServer(Integer port,
  66                        Boolean debug,
  67                        String username,
  68                        String password)
  69         throws IOException
  70     {
  71         this(port, debug, new Credentials(username, password));
  72     }
  73 
  74     public ProxyServer(Integer port,
  75                        Boolean debug)
  76         throws IOException
  77     {
  78         this(port, debug, null);
  79     }
  80 
  81     public ProxyServer(Integer port,
  82                        Boolean debug,
  83                        Credentials credentials)
  84         throws IOException
  85     {
  86         this.debug = debug;
  87         listener = new ServerSocket();
  88         listener.setReuseAddress(false);
  89         listener.bind(new InetSocketAddress(InetAddress.getLoopbackAddress(), port));
  90         this.port = listener.getLocalPort();
  91         this.credentials = credentials;
  92         setName("ProxyListener");
  93         setDaemon(true);
  94         connections = new CopyOnWriteArrayList<Connection>();
  95         start();
  96     }
  97 
  98     public ProxyServer(String s) {
  99         credentials = null;
 100     }
 101 
 102     /**
 103      * Returns the port number this proxy is listening on
 104      */
 105     public int getPort() {
 106         return port;
 107     }
 108 
 109     /**
 110      * Shuts down the proxy, probably aborting any connections
 111      * currently open
 112      */
 113     public void close() throws IOException {
 114         if (debug) System.out.println("Proxy: closing server");
 115         done = true;
 116         listener.close();
 117         for (Connection c : connections) {
 118             c.close();
 119             c.awaitCompletion();
 120         }
 121     }
 122 
 123     CopyOnWriteArrayList<Connection> connections;
 124 
 125     volatile boolean done;
 126 
 127     public void run() {
 128         if (System.getSecurityManager() == null) {
 129             execute();
 130         } else {
 131             // so calling domain does not need to have socket permission
 132             AccessController.doPrivileged(new PrivilegedAction<Void>() {
 133                 public Void run() {
 134                     execute();
 135                     return null;
 136                 }
 137             });
 138         }
 139     }
 140 
 141     public void execute() {
 142         int id = 0;
 143         try {
 144             while (!done) {
 145                 Socket s = listener.accept();
 146                 id++;
 147                 Connection c = new Connection(s, id);
 148                 if (debug)
 149                     System.out.println("Proxy: accepted new connection: " + s);
 150                 connections.add(c);
 151                 c.init();
 152             }
 153         } catch(Throwable e) {
 154             if (debug && !done) {
 155                 System.out.println("Proxy: Fatal error, listener got " + e);
 156                 e.printStackTrace();
 157             }
 158         }
 159     }
 160 
 161     /**
 162      * Transparently forward everything, once we know what the destination is
 163      */
 164     class Connection {
 165 
 166         private final int id;
 167         Socket clientSocket, serverSocket;
 168         Thread out, in;
 169         volatile InputStream clientIn, serverIn;
 170         volatile OutputStream clientOut, serverOut;
 171 
 172         final static int CR = 13;
 173         final static int LF = 10;
 174 
 175         Connection(Socket s, int id) throws IOException {
 176             this.id = id;
 177             this.clientSocket= s;
 178             this.clientIn = new BufferedInputStream(s.getInputStream());
 179             this.clientOut = s.getOutputStream();
 180         }
 181 
 182         byte[] readHeaders(InputStream is) throws IOException {
 183             byte[] outbuffer = new byte[8000];
 184             int crlfcount = 0;
 185             int bytecount = 0;
 186             int c;
 187             while ((c=is.read()) != -1 && bytecount < outbuffer.length) {
 188                 outbuffer[bytecount++] = (byte)c;
 189                 if (debug) System.out.write(c);
 190                 // were looking for CRLFCRLF sequence
 191                 if (c == CR || c == LF) {
 192                     switch(crlfcount) {
 193                         case 0:
 194                             if (c == CR) crlfcount ++;
 195                             break;
 196                         case 1:
 197                             if (c == LF) crlfcount ++;
 198                             break;
 199                         case 2:
 200                             if (c == CR) crlfcount ++;
 201                             break;
 202                         case 3:
 203                             if (c == LF) crlfcount ++;
 204                             break;
 205                     }
 206                 } else {
 207                     crlfcount = 0;
 208                 }
 209                 if (crlfcount == 4) {
 210                     break;
 211                 }
 212             }
 213             byte[] ret = new byte[bytecount];
 214             System.arraycopy(outbuffer, 0, ret, 0, bytecount);
 215             return ret;
 216         }
 217 
 218         boolean running() {
 219             return out.isAlive() || in.isAlive();
 220         }
 221 
 222         private volatile boolean closing;
 223         public synchronized void close() throws IOException {
 224             closing = true;
 225             if (debug)
 226                 System.out.println("Proxy: closing connection {" + this + "}");
 227             if (serverSocket != null)
 228                 serverSocket.close();
 229             if (clientSocket != null)
 230                 clientSocket.close();
 231         }
 232 
 233         public void awaitCompletion() {
 234             try {
 235                 if (in != null)
 236                     in.join();
 237                 if (out!= null)
 238                     out.join();
 239             } catch (InterruptedException e) { }
 240         }
 241 
 242         int findCRLF(byte[] b) {
 243             for (int i=0; i<b.length-1; i++) {
 244                 if (b[i] == CR && b[i+1] == LF) {
 245                     return i;
 246                 }
 247             }
 248             return -1;
 249         }
 250 
 251         // Checks credentials in the request against those allowable by the proxy.
 252         private boolean authorized(Credentials credentials,
 253                                    List<String> requestHeaders) {
 254             List<String> authorization = requestHeaders.stream()
 255                     .filter(n -> n.toLowerCase(Locale.US).startsWith("proxy-authorization"))
 256                     .collect(toList());
 257 
 258             if (authorization.isEmpty())
 259                 return false;
 260 
 261             if (authorization.size() != 1) {
 262                 throw new IllegalStateException("Authorization unexpected count:" + authorization);
 263             }
 264             String value = authorization.get(0).substring("proxy-authorization".length()).trim();
 265             if (!value.startsWith(":"))
 266                 throw new IllegalStateException("Authorization malformed: " + value);
 267             value = value.substring(1).trim();
 268 
 269             if (!value.startsWith("Basic "))
 270                 throw new IllegalStateException("Authorization not Basic: " + value);
 271 
 272             value = value.substring("Basic ".length());
 273             String values = new String(Base64.getDecoder().decode(value), UTF_8);
 274             int sep = values.indexOf(':');
 275             if (sep < 1) {
 276                 throw new IllegalStateException("Authorization no colon: " +  values);
 277             }
 278             String name = values.substring(0, sep);
 279             String password = values.substring(sep + 1);
 280 
 281             if (name.equals(credentials.name()) && password.equals(credentials.password()))
 282                 return true;
 283 
 284             return false;
 285         }
 286 
 287         public void init() {
 288             try {
 289                 byte[] buf;
 290                 while (true) {
 291                     buf = readHeaders(clientIn);
 292                     if (findCRLF(buf) == -1) {
 293                         if (debug)
 294                             System.out.println("Proxy: no CRLF closing, buf contains:["
 295                                     + new String(buf, UTF_8) + "]" );
 296                         close();
 297                         return;
 298                     }
 299 
 300                     List<String> headers = asList(new String(buf, UTF_8).split("\r\n"));
 301                     // check authorization credentials, if required by the server
 302                     if (credentials != null && !authorized(credentials, headers)) {
 303                         String resp = "HTTP/1.1 407 Proxy Authentication Required\r\n" +
 304                                       "Content-Length: 0\r\n" +
 305                                       "Proxy-Authenticate: Basic realm=\"proxy realm\"\r\n\r\n";
 306 
 307                         clientOut.write(resp.getBytes(UTF_8));
 308                     } else {
 309                         break;
 310                     }
 311                 }
 312 
 313                 int p = findCRLF(buf);
 314                 String cmd = new String(buf, 0, p, "US-ASCII");
 315                 String[] params = cmd.split(" ");
 316 
 317                 if (params[0].equals("CONNECT")) {
 318                     doTunnel(params[1]);
 319                 } else {
 320                     doProxy(params[1], buf, p, cmd);
 321                 }
 322             } catch (Throwable e) {
 323                 if (debug) {
 324                     System.out.println("Proxy: " + e);
 325                     e.printStackTrace();
 326                 }
 327                 try {close(); } catch (IOException e1) {}
 328             }
 329         }
 330 
 331         void doProxy(String dest, byte[] buf, int p, String cmdLine)
 332             throws IOException
 333         {
 334             try {
 335                 URI uri = new URI(dest);
 336                 if (!uri.isAbsolute()) {
 337                     throw new IOException("request URI not absolute");
 338                 }
 339                 dest = uri.getAuthority();
 340                 // now extract the path from the URI and recreate the cmd line
 341                 int sp = cmdLine.indexOf(' ');
 342                 String method = cmdLine.substring(0, sp);
 343                 cmdLine = method + " " + uri.getPath() + " HTTP/1.1";
 344                 int x = cmdLine.length() - 1;
 345                 int i = p;
 346                 while (x >=0) {
 347                     buf[i--] = (byte)cmdLine.charAt(x--);
 348                 }
 349                 i++;
 350 
 351                 commonInit(dest, 80);
 352                 OutputStream sout;
 353                 synchronized (this) {
 354                     if (closing) return;
 355                     sout = serverOut;
 356                 }
 357                 // might fail if we're closing but we don't care.
 358                 sout.write(buf, i, buf.length-i);
 359                 proxyCommon();
 360 
 361             } catch (URISyntaxException e) {
 362                 throw new IOException(e);
 363             }
 364         }
 365 
 366         synchronized void commonInit(String dest, int defaultPort) throws IOException {
 367             if (closing) return;
 368             int port;
 369             String[] hostport = dest.split(":");
 370             if (hostport.length == 1) {
 371                 port = defaultPort;
 372             } else {
 373                 port = Integer.parseInt(hostport[1]);
 374             }
 375             if (debug)
 376                 System.out.printf("Proxy: connecting to (%s/%d)\n", hostport[0], port);
 377             serverSocket = new Socket(hostport[0], port);
 378             serverOut = serverSocket.getOutputStream();
 379 
 380             serverIn = new BufferedInputStream(serverSocket.getInputStream());
 381         }
 382 
 383         synchronized void proxyCommon() throws IOException {
 384             if (closing) return;
 385             out = new Thread(() -> {
 386                 try {
 387                     byte[] bb = new byte[8000];
 388                     int n;
 389                     while ((n = clientIn.read(bb)) != -1) {
 390                         serverOut.write(bb, 0, n);
 391                     }
 392                     closing = true;
 393                     serverSocket.close();
 394                     clientSocket.close();
 395                 } catch (IOException e) {
 396                     if (!closing && debug) {
 397                         System.out.println("Proxy: " + e);
 398                         e.printStackTrace();
 399                     }
 400                 }
 401             });
 402             in = new Thread(() -> {
 403                 try {
 404                     byte[] bb = new byte[8000];
 405                     int n;
 406                     while ((n = serverIn.read(bb)) != -1) {
 407                         clientOut.write(bb, 0, n);
 408                     }
 409                     closing = true;
 410                     serverSocket.close();
 411                     clientSocket.close();
 412                 } catch (IOException e) {
 413                     if (!closing && debug) {
 414                         System.out.println("Proxy: " + e);
 415                         e.printStackTrace();
 416                     }
 417                 }
 418             });
 419             out.setName("Proxy-outbound");
 420             out.setDaemon(true);
 421             in.setDaemon(true);
 422             in.setName("Proxy-inbound");
 423             out.start();
 424             in.start();
 425         }
 426 
 427         void doTunnel(String dest) throws IOException {
 428             if (closing) return; // no need to go further.
 429             commonInit(dest, 443);
 430             // might fail if we're closing, but we don't care.
 431             clientOut.write("HTTP/1.1 200 OK\r\n\r\n".getBytes());
 432             proxyCommon();
 433         }
 434 
 435         @Override
 436         public String toString() {
 437             return "Proxy connection " + id + ", client sock:" + clientSocket;
 438         }
 439     }
 440 
 441     public static void main(String[] args) throws Exception {
 442         int port = Integer.parseInt(args[0]);
 443         boolean debug = args.length > 1 && args[1].equals("-debug");
 444         System.out.println("Debugging : " + debug);
 445         ProxyServer ps = new ProxyServer(port, debug);
 446         System.out.println("Proxy server listening on port " + ps.getPort());
 447         while (true) {
 448             Thread.sleep(5000);
 449         }
 450     }
 451 }