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 }