// Proxy.java version 2.6.2 21 January 2009 // Copyright 1999 - 2009 Logica plc. All rights reserved // Portions copyright 1996 O'Reilly & Associates, Inc // Portions copyright 1999 - 2002 Charles Wicksteed // May be freely used within Logica. Contact me for other license conditions. // Charles Wicksteed charles.wicksteedc@logica.com // Modified 10 Feb 2009 to serve bad status from cache // and to remove random numeric suffixes from swf URLs // Modified 26 Mar 2009 to print host name when can't connect // Modified 27 Mar 2009 to remove Cache-Control: as well as Pragma: no-cache public class Proxy extends Thread { public int debug_level = 4; int quit_index = 0; public DebugServer debug_server; Proxy root; public String proxyHost; // host name of next proxy to use, optional public int proxyPort = 80; // port on next proxy to use, optional public String nonProxyHosts; // "|"-separated list of direct hosts, with "*" public boolean plainHostDirect; // whether to skip proxy for plain host names public String browserId; // browser identification string, optional public String saynotmodified; // control If-Modified, optional String propertyFile; int inPort; // port to listen on -- requests come in from the browser here int debugPort; java.util.Vector blockList; // list of blocked URLs public boolean fixcrlf; // fix bare line feeds (Unix line endings) on debug output public boolean fixctrlchars; // fix control characters on debug public boolean removepragma; public boolean printallcontenttypes; public boolean printallpostdata; public boolean sizesFlag; public boolean printtimestamps; public String allowedClients; public String translateHosts; public String cacheDir; public String cachemode; public boolean blockall; public boolean serveFromCacheOnly; public boolean blockRedirects; public static void fail (Exception e, String msg) { System.err.println(msg + ": " + e); System.exit(1); } public Proxy(String f) { // store in instance variable propertyFile = f; readProperties(); // a server to send debug info back to a telnet client // and accept debug level and quit commands debug_server = new DebugServer(this, debugPort); // the server that does the work new Server(this, inPort); // so that I can do output with same syntax everywhere root = this; // this object just runs the local console debug thread this.start(); } public void run() { int b = 0; int eofCount = 0; while (eofCount < 10) { try { b = System.in.read(); } catch (java.io.IOException e) { fail(e, "System in"); } process_char(b); // Ctrl-Break, which is used to get a thread dump or an hprof dump on Windows, // makes it return -1 once every time you do it! // On genuine end-of-file, it will return -1 repeatedly. if (b == -1) { eofCount++; if (debug_level == 9) System.out.println("Console loop EOF count = " + eofCount); } else { // Ordinary character has been read, reset the count eofCount = 0; } } // read returns -1 on EOF. // Exit this thread to stop it looping if run as daemon // with /dev/null as input stream. // Sleep for long enough for the other threads to start. // Twice in case it gets interrupted. try { Thread.sleep(500); } catch (Exception e) { ; } try { Thread.sleep(500); } catch (Exception e) { ; } System.out.println("Console loop exit"); } public void process_char(int b) { if (b >= 0x30 && b <= 0x39) { debug_level = b - 0x30; root.debug_server.println(0, "Debug level set to " + debug_level, 0); } if (("quit").charAt(quit_index) == (char) b) { quit_index++; if (quit_index >= 4) { root.debug_server.println(0, "Quitting", 0); System.exit(1); } } // if quit char if ((char)b == 's') { // stop // For use when opening a doubtful email, block all traffic until // you press "g" for go. blockall = true; root.debug_server.println(0, "Block all set to " + blockall, 0); } if ((char)b == 'g') { // go blockall = false; serveFromCacheOnly = false; root.debug_server.println(0, "Block all set to " + blockall, 0); } if ((char)b == 'c') { // serve from cache only // For testing really // press "g" for go. serveFromCacheOnly = true; root.debug_server.println(0, "Serving from cache only, press g to reverse", 0); } if ((char)b == 'm') { // memory String before = Runtime.getRuntime().freeMemory() / 1024 + "K / " + Runtime.getRuntime().totalMemory() / 1024; Runtime.getRuntime().gc(); root.debug_server.println(0, "GC: " + before + "K -> " + Runtime.getRuntime().freeMemory() / 1024 + "K / " + Runtime.getRuntime().totalMemory() / 1024 + "K", 0); } if ((char)b == 'r') { // block redirects -- for use when clicking on a bit.ly link blockRedirects = !blockRedirects; root.debug_server.println(0, (blockRedirects ? "B" : "Not b") + "locking redirects", 0); } if ((char)b == 'h') { // read the properties again -- easiest way to change without stopping readProperties(); String help = "9 - connection start and end, host and port, misc debug info, no content\r\n" + "8 - returned content\r\n" + "7 - original request headers, bytes returned, first part of POST data\r\n" + "6 - modified request headers, dot for each block\r\n" + "5 - returned headers\r\n" + "4 - URL and status\r\n" + "3 - full URL\r\n" + "2 - final letter of URL\r\n" + "1 - errors only\r\n" + "\r\n" + "Server listening on port " + inPort + "\r\n" + "Chained proxy server=" + proxyHost + "\r\n" + "proxyPort=" + proxyPort + "\r\n" + "nonProxyHosts=" + nonProxyHosts + "\r\n" + "plainHostDirect=" + plainHostDirect + "\r\n" + "debug port=" + debugPort + "\r\n" + "browserId=" + browserId + "\r\n" + "saynotmodified=" + saynotmodified + "\r\n" + "fixcrlf=" + fixcrlf + "\r\n" + "fixctrlchars=" + fixctrlchars + "\r\n" + "removepragma=" + removepragma + "\r\n" + "printallcontenttypes=" + printallcontenttypes + "\r\n" + "printallpostdata=" + printallpostdata + "\r\n" + "sizesFlag=" + sizesFlag + "\r\n" + "printtimestamps=" + printtimestamps + "\r\n" + "allowedClients=" + allowedClients + "\r\n" + "translateHosts=" + translateHosts + "\r\n" + "cacheDir=" + cacheDir + "\r\n" + "cachemode=" + cachemode + "\r\n" + "blockall=" + blockall + "\r\n" + "Debug level=" + debug_level + "\r\n" + "sizeitem fields are status contentType bytes time-to-first-byte(ms) time-to-last-byte(ms) url\r\n" + ""; root.debug_server.println(0, help, 0); } // help } // process_char public static void main(String[] args) { // parameter = property file name if (args.length != 1) { String usage = "Usage: java proxy property-file\r\n" + "Typical property file contains:\r\n" + " inPort=2929\r\n" + " debugPort=2828\r\n" + " # the following are optional\r\n" + " proxyHost=proxy.logica.com\r\n" + " browserId=Charles special\r\n" + " blockFile=C:/My Documents/CHARLES/misc03/proxy/blocklist.txt\r\n" + " fixcrlf=true\r\n" + " fixctrlchars=true\r\n" + ""; System.out.println(usage); System.exit(1); } // make an instance of this class, passing in property file name Proxy r = new Proxy(args[0]); } void readProperties() { java.io.FileInputStream fi = null; // open file try { fi = new java.io.FileInputStream(propertyFile); } catch (java.io.IOException e) { System.out.println("Can't open "+propertyFile+": "+e); System.exit(1); } java.util.Properties p = new java.util.Properties(); // read file try { p.load(fi); } catch (java.io.IOException e) { System.out.println("Can't read properties: "+e); System.exit(1); } try { fi.close(); } catch (java.io.IOException e) { } // input port try { inPort = Integer.parseInt(p.getProperty("inPort")); } catch (Exception e) { System.out.println("Input port: "+e); System.exit(1); } // debug port try { debugPort = Integer.parseInt(p.getProperty("debugPort")); } catch (Exception e) { System.out.println("Debug port: "+e); System.exit(1); } // where to send requests on to (may be null) proxyHost = p.getProperty("proxyHost"); try { proxyPort = Integer.parseInt(p.getProperty("proxyPort")); } catch (Exception e) { } if (proxyHost != null) { if (proxyPort == 80) System.out.println("Using chained proxy server " + proxyHost); else System.out.println("Using chained proxy server " + proxyHost + ":" + proxyPort); } // To exclude servers from being proxied, list them // under the nonProxyHosts parameter separated // by "|" delimiter: // nonProxyHosts=localhost|*.mycompany.com nonProxyHosts = p.getProperty("nonProxyHosts"); // It is often useful to have a rule that we connect directly to // the host (ie don't go via the chained proxy) if the host name is // plain, ie does not have a domain specified, // eg http://myhost/splig not http://myhost.logicacmg.com/splig. // Set this parameter to true to turn on this behaviour. // Most people will want this, but it is optional for backward compatibility. // Without this, you have to list all the plain hosts that you use // in your nonProxyHosts property. plainHostDirect = getBooleanProperty(p, "plainHostDirect"); // lie about which browser we are (may be null) browserId = p.getProperty("browserId"); // Control whether we speed up image loading by always saying "304" // when the browser says "If-Modified-Since:" // The string is a pattern of URLs to match saynotmodified = p.getProperty("saynotmodified"); // public boolean fixcrlf; // fix bare line feeds (Unix line endings) on debug output // public boolean fixctrlchars; // fix control characters on debug // put "fixcrlf=true" in property file // any other value counts as false fixcrlf = getBooleanProperty(p, "fixcrlf"); fixctrlchars = getBooleanProperty(p, "fixctrlchars"); removepragma = getBooleanProperty(p, "removepragma"); printallcontenttypes = getBooleanProperty(p, "printallcontenttypes"); printallpostdata = getBooleanProperty(p, "printallpostdata"); sizesFlag = getBooleanProperty(p, "sizesFlag"); printtimestamps = getBooleanProperty(p, "printtimestamps"); allowedClients = p.getProperty("allowedClients"); translateHosts = p.getProperty("translateHosts"); cacheDir = p.getProperty("cacheDir"); cachemode = p.getProperty("cachemode"); // the list of blocked URLs String blockFile = p.getProperty("blockFile"); // prepare the list in a temporary variable to avoid a race java.util.Vector tempList = new java.util.Vector(); if (blockFile != null) { java.io.DataInputStream di = null; // open file try { di = new java.io.DataInputStream(new java.io.FileInputStream(blockFile)); } catch (java.io.IOException e) { System.out.println("Can't open "+blockFile+": "+e); } if (di != null) try { String line; while ((line = di.readLine()) != null) { line = line.trim(); // remove leading and trailing space // ignore blank lines and comments if (line.length() > 0 && !line.startsWith("#")) tempList.addElement(line); } di.close(); } catch (java.io.IOException e) { System.out.println("Error reading "+blockFile+": "+e); } } // blockFile != null // make the list live (empty if no file) blockList = tempList; } // readProperties boolean getBooleanProperty(java.util.Properties p, String key) { // put "propname=true" (or True, TRUE etc) in property file. // any other value, or omitting the name from the properties file, // counts as false return p.getProperty(key) != null && new Boolean(p.getProperty(key).trim()).booleanValue(); } public synchronized void sizesLog (int ref, String url, int status, String contentType, int bytes, long resptime1, long resptime2, String cacheFileName) { if (!sizesFlag) return; root.debug_server.println(ref, "sizeitem\t" + status + "\t" + contentType + "\t" + bytes + "\t" + resptime1 + "\t" + resptime2 + "\t" + url + (cacheFileName != null ? "\t" + cacheFileName : ""), 0); // may be useful, last 30 chars only -- url.substring(Math.max(0, url.length() - 30)) } // sizesLog } // class Proxy //====================================================================== class Server extends Thread { java.net.ServerSocket listen_socket; Proxy root; public Server(Proxy p, int port) { root = p; try { listen_socket = new java.net.ServerSocket(port); } catch (java.io.IOException e) { Proxy.fail(e, "Exception creating server socket"); } root.debug_server.println(0, "Server listening on port " + port, 3); this.start(); } public void run() { int nextrefnum = 1000000; try { while (true) { // On some JVMs, with modern threading mechanisms, we sometimes get to // here before root.debug_server has been set. if (root.debug_server != null) root.debug_server.println(0, "Server about to accept", 9); java.net.Socket in_socket = listen_socket.accept(); root.debug_server.println(0, "Accepted, spawning connection", 9); new Connection(root, in_socket, nextrefnum); if ((nextrefnum += 1000) > 1999000) nextrefnum = 1000000; } } catch (java.io.IOException e) { Proxy.fail(e, "Exception in server loop"); } } // run } // class Server //====================================================================== class Connection extends Thread { Proxy root; java.net.Socket client_socket; java.io.InputStream client_in; // from client to here java.io.OutputStream client_out = null; // from here to client java.io.OutputStream server_out = null; // from here to server boolean OK = true; String doing; String new_headers; boolean clientKeepAlive; String hostWithPort; String hostName; int hostPort; String currentHost = "/"; // some illegal value meaning none boolean ifmodified; int refnum; public String url; ResponseHandler responseHandler = null; boolean send_headers = true; // set to false if SSL tunnel without proxy boolean ssl_tunnel = false; // set to true if SSL tunnel String method = null; String data_part = null; int content_length = 0; public long time_of_request; public Connection(Proxy p, java.net.Socket i, int r) { root = p; client_socket = i; refnum = r; // We should do more of this in "run" and less in the constructor... // though difficult to get cleanly out of "run" without an early return. String client_address = i.getInetAddress().toString(); root.debug_server.println(refnum, "Client address " + client_address, 5); if (p.allowedClients != null && !Glob.matchAny(client_address, p.allowedClients)) { root.debug_server.println(refnum, "Client address not on list (" + p.allowedClients + ")", 0); try { client_socket.close(); } catch (java.io.IOException e) { ; } return; } try { client_in = i.getInputStream(); } catch (java.io.IOException e) { root.debug_server.println(refnum, "Exception getting client input stream:" + e, 0); OK = false; } try { client_out = i.getOutputStream(); } catch (java.io.IOException e) { root.debug_server.println(refnum, "Exception getting client output stream: " + e, 0); OK = false; } if (OK) this.start(); } public void run() { doing = "nothing"; try { // handle several requests, if the connection is kept open (HTTP 1.1) // Jumps out via an exception when connection is closed by client // or via the "while" condition if we close the connection because, // say, we are talking to an HTTP 1.0 server with no content length. while (client_socket != null) { read_headers(); // read request headers from browser if (new_headers.indexOf("pzqbproxyfail") != -1) { throw(new Exception("pzqbproxyfail - proxy bailing out")); } if (isBlocked()) { // the "/*" etc in the error message is so as to avoid a JavaScript // or CSS syntax error in case it's JavaScript or CSS that is blocked sendResponseFromHere("403 Charles blocked", "/* URL " + url + " is blocked */\r\n"); } else { if (ifmodified && root.saynotmodified != null && Glob.matchAny(url, root.saynotmodified)) { // say not modified sendResponseFromHere("304 Charles use local", ""); } else { // Serve from file cache if configured // If no cache is configured, fetch it from the server. // If cachemode flag is writeonly, fetch it from the server. // Otherwise serve it from the cache: if it doesn't have a copy, // fetch it from the server. // The call to serveFromCache() in the condition actually serves it, // if found in the cache, and returns true if successful. // The cachemode of writeonly is used for debugging a session -- when this // is set, the proxy never serves anything from the cache, but it // keeps a copy of every response for looking at later. // This is useful for debugging a web application where often many // requests have the same URL, with different POST data, and you // really want every request to go through to the server. if (root.cacheDir == null || "writeonly".equals(root.cachemode) || !serveFromCache()) { if (root.serveFromCacheOnly) { sendResponseFromHere( "403 Charles blocked", "// URL " + url + " is blocked\r\n"); } else { // Fetch the page from the server (or chained proxy) // this function now includes making a response handler // to fetch the response from the server init_connection(); } } } } // not blocked refnum++; } // while client_socket != null // If we ended the loop normally, not via an exception, then the refnum // has been incremented for the next iteration. // This is misleading for the final debug statements, so put it back refnum--; } catch (ClientClosedConnectionException e) { // expect this during normal running root.debug_server.println(refnum, "Client closed connection", 9); } catch (Exception e) { String errmsg = "Proxy exception in Connection " + doing + ": " + e; root.debug_server.println(refnum, errmsg, 0); if (client_out != null) // (also closes client connection) send_error(errmsg); } try { // close connection to server (may already be closed?) if (server_out != null) { doing = "closing server output stream 2"; server_out.close(); server_out = null; } } catch (Exception e) { String errmsg = "Proxy exception " + doing + ": " + e; root.debug_server.println(refnum, errmsg, 0); } root.debug_server.println(refnum, "Connection closed", 9); } // run // read request headers from browser void read_headers() throws java.io.IOException, ClientClosedConnectionException { byte[] buffer = new byte[1024 * 20]; String strbuf = ""; int bytes_read; int slash_pos = 0; int url_pos = 0; int p_pos = 0; int host_pos = 0; doing = "reading header"; // read the header of the request while (strbuf.indexOf("\r\n\r\n") == -1) { try { bytes_read = client_in.read(buffer); } catch (java.io.IOException e) { // sometimes we get an exception, sometimes it returns -1 // when the connection is closed root.debug_server.println(refnum, "Exception on client read", 9); bytes_read = -1; } // catch if (bytes_read < 0) { // client has closed the connection before we got the full request // this will be the normal path with persistent connections // when the client has finished getting all the images etc // not normal if we have already read some of the request if (strbuf.length() > 0) root.debug_server.println(refnum, "Client closed connection, bytes read = " + strbuf.length(), 0); else root.debug_server.println(refnum, "Client closed connection, bytes read = 0", 9); doing = "closing client socket"; client_socket.close(); client_socket = null; if (server_out != null) { // close outgoing side of old connection to server from last time // we won't be sending another request down this connection doing = "closing server output stream 1"; server_out.close(); root.debug_server.println(refnum, "Server output stream closed", 9); } // normal exit from loop after a number of requests on same connection throw new ClientClosedConnectionException(); } root.debug_server.print(refnum, new String(buffer, 0, // hibyte 0, // offset bytes_read), // count -- old data stays in buffer 7); // debug level // add new string to end of current one strbuf = strbuf + new String(buffer, 0, // hibyte 0, // offset bytes_read); // count } // while not CRLFCRLF // got the header, get the page // There may be some POST data in the buffer after the end // of the headers. // Chop it off and put it in a separate String // because we want to change the headers. // So split the headers string into two parts. // The blank line stays with the headers. // The data part may be empty. int header_length = strbuf.indexOf("\r\n\r\n") + 4; data_part = strbuf.substring(header_length); strbuf = strbuf.substring(0, header_length); if (data_part.length() > 0) { // new line after (first part of) POST content root.debug_server.println(0, "", 7); } // On a persistent connection, IE sometimes sends a spurious extra // blank line after the POST data // It will appear here before the header of the current request // Throw it away while (strbuf.length() > 0 && (strbuf.charAt(0) == '\n' || strbuf.charAt(0) == '\r')) { root.debug_server.println(refnum, "Removed leading char " + (int)strbuf.charAt(0), 0); strbuf = strbuf.substring(1); } // GET http://www.yahoo.co.uk/... // HEAD http://www.yahoo.co.uk/... // POST http://www.yahoo.co.uk/... // CONNECT server.example.com:80 HTTP/1.1 int method_end = strbuf.indexOf(" "); method = strbuf.substring(0, method_end); // for extracting URL for logging url_pos = method_end + 1; // get the content length if any, eg if a POST String cl1 = "Content-length: "; String cl2 = "Content-Length: "; int cl_pos = strbuf.indexOf(cl1); if (cl_pos == -1) { // try the other spelling (for IE) cl_pos = strbuf.indexOf(cl2); } content_length = 0; if (cl_pos != -1) { try { content_length = Integer.parseInt( strbuf.substring(cl_pos + cl1.length(), strbuf.indexOf("\r", cl_pos + cl1.length()))); } catch(Exception e) {} } root.debug_server.println(refnum, "Content-length: " + content_length, 9); if (method.equals("CONNECT")) { ssl_tunnel = true; // CONNECT server.example.com:443 HTTP/1.1 int host_end = strbuf.indexOf(" ", method_end + 1); hostWithPort = strbuf.substring(method_end + 1, host_end); host_pos = method_end + 1; // slash_pos is the character after the host name slash_pos = host_end; } else { // GET, HEAD, POST etc // sort out chained proxy // GET http://www.yahoo.co.uk/... // 012345678 String protocol = strbuf.substring(method_end + 1, method_end + 8); if (!protocol.equals("http://")) { throw(new java.io.IOException( "Only http supported\n" + "Header length = " + strbuf.length() + "\n" + "[" + (strbuf.length() > 70 ? strbuf.substring(0, 70) + "..." : strbuf) + "]\n")); } // HTTP // slash following host name slash_pos = strbuf.indexOf("/", method_end + 8); hostWithPort = strbuf.substring(method_end + 8, slash_pos); host_pos = method_end + 8; } // get plain host name without port, and the port separate // if we are using a chained proxy, these get overwritten below hostPort = 80; int colon_pos = hostWithPort.indexOf(":"); if (colon_pos == -1) { // no port hostName = hostWithPort; } else { try { hostPort = Integer.parseInt(hostWithPort.substring(colon_pos + 1)); } catch(Exception e) {} hostName = hostWithPort.substring(0, colon_pos); } // set host name and port to connect to, and adjust the headers // this depends on whether we are using a chained proxy or not. // Go direct if plainHostDirect flag is set and the host name is plain. // Plain host names are those which do not contain dots. if (root.proxyHost != null && !Glob.matchAny(hostName, root.nonProxyHosts) && !(root.plainHostDirect && hostName.indexOf(".") == -1)) { // we are using a chained proxy // headers unchanged, ie including the http://host-name after GET or POST // or HEAD. CONNECT is also left unchanged. new_headers = strbuf; // if we have a host name with a local translation to IP address, // substitute it String ipAddress = translateHost(hostName); if (ipAddress != null) { new_headers = new_headers.substring(0, host_pos) + ipAddress + (hostPort != 80 ? ":" + hostPort : "") + new_headers.substring(slash_pos); } // hostWithPort is used to check when a new persistent connection is needed if (root.proxyPort == 80) hostWithPort = root.proxyHost; else hostWithPort = root.proxyHost + ":" + root.proxyPort; // these are used to open the actual connection hostName = root.proxyHost; hostPort = root.proxyPort; } else { // we are not using chained proxy // ie connect direct // adjust the headers: remove the http://host-name after GET, HEAD or POST // (no headers are used for CONNECT) new_headers = method + " " + strbuf.substring(slash_pos); // We support Keep-Alive connections if the client has asked for them // Change Proxy-Connection to Connection // Proxy-Connection had to be invented for compatibility with older proxies // which didn't support keep-alive and would otherwise look as if they did. // So newer proxies like this one have to act intelligently. // Firefox uses "keep-alive". Ought really to parse by the standard... String pcka = "Proxy-Connection: Keep-Alive"; p_pos = new_headers.toLowerCase().indexOf(pcka.toLowerCase()); if (p_pos != -1) { // Change the header new_headers = new_headers.substring(0, p_pos) + "Connection: Keep-Alive" + new_headers.substring(p_pos + pcka.length()); } // p_pos != -1 if (method.equals("CONNECT")) { // direct connection for SSL, send nothing send_headers = false; } } // else not using chained proxy String remove = null; if (root.removepragma) { remove = "Pragma: no-cache"; p_pos = new_headers.indexOf(remove); if (p_pos != -1) { // + 2 for the \r\n new_headers = new_headers.substring(0, p_pos) + new_headers.substring(p_pos + remove.length() + 2); } // p_pos != -1 remove = "Cache-Control: max-age=0"; p_pos = new_headers.indexOf(remove); if (p_pos != -1) { // + 2 for the \r\n new_headers = new_headers.substring(0, p_pos) + new_headers.substring(p_pos + remove.length() + 2); } // p_pos != -1 } // removepragma remove = "Accept-Encoding: gzip, deflate"; p_pos = new_headers.indexOf(remove); if (p_pos != -1) { // + 2 for the \r\n new_headers = new_headers.substring(0, p_pos) + new_headers.substring(p_pos + remove.length() + 2); } // p_pos != -1 // Various spacings in different versions of Firefox // One day I will use a regular expression library remove = "Accept-Encoding: gzip,deflate"; p_pos = new_headers.indexOf(remove); if (p_pos != -1) { // + 2 for the \r\n new_headers = new_headers.substring(0, p_pos) + new_headers.substring(p_pos + remove.length() + 2); } // p_pos != -1 // lie about which browser we are using // (if we have been asked to in the properties file) if (root.browserId != null) { String btag = "User-Agent: "; int br_pos = new_headers.indexOf(btag); if (br_pos != -1) { // splice in the new string new_headers = new_headers.substring(0, br_pos + btag.length()) + root.browserId + new_headers.substring(new_headers.indexOf("\r", br_pos)); } } // if If-Modified header field, say it's not been modified, for speed // stops it refetching adverts ifmodified = (new_headers.indexOf("If-Modified-Since:") != -1); root.debug_server.print(refnum, new_headers, 6); // extract the URL which we have to fetch int space_pos = strbuf.indexOf(" ", url_pos); url = strbuf.substring(url_pos, space_pos); // print URL or last letter if (root.debug_level == 2) { // print last letter of URL (f for gif, g for jpeg, m or l for html, 3 for port 443) root.debug_server.print(0, strbuf.substring(space_pos - 1, space_pos), 2); } if (root.debug_level >= 3 && root.debug_level <= 5) { // print URL -- higher debug levels print the request header anyway root.debug_server.println(refnum, url, 3); } // get the HTTP version // ... HTTP/1.1 int ver_pos = strbuf.indexOf("HTTP/1.", space_pos); String HTTPVersion = "0"; // default if garbled if (ver_pos != -1) { HTTPVersion = strbuf.substring(ver_pos + 7, ver_pos + 8); } root.debug_server.println(refnum, "HTTP version = " + HTTPVersion, 9); // Keep-Alive if HTTP 1.1 or if specially asked for clientKeepAlive = strbuf.indexOf("Proxy-Connection: Keep-Alive") != -1 || !HTTPVersion.equals("0"); root.debug_server.println(refnum, "client Keep-Alive = " + clientKeepAlive, 9); } // read_headers boolean isBlocked() { boolean plus = false; if (root.blockall) { return true; } // check if the URL is blocked for (int i = 0; i < root.blockList.size(); i++) { // special case -- if a line starts with a "+" then the // meaning is reversed, and the request is blocked if // the URL does not match any of the lines in the file if (((String)root.blockList.elementAt(i)).startsWith("+")) plus = true; // default delimiter is space java.util.StringTokenizer stok = new java.util.StringTokenizer((String)root.blockList.elementAt(i)); boolean matched = true; // unless proved otherwise while (stok.hasMoreTokens()) { String temp = stok.nextToken(); if (url.indexOf(temp) < 0) { // doesn't match this part, so doesn't count as a match matched = false; } // Too much debug output if not actually debugging this part of the code // root.debug_server.println(refnum, "Matching " + temp + ": " + matched, 9); } // did it match every fragment? if (matched) return(!plus); // matched this blocked URL, so blocked } // for // didn't match any line, so not blocked return(plus); } // isBlocked // Translate host to IP address // Useful when going via a proxy but you want to look up the host locally // This is very inefficient, as it reads the file every time, // so don't set the configuration parameter unless you have to. // But modern operating systems have very efficient file caches. // [[ To make it more efficient, could use a hashtable as a cache, or read the hosts // file as a properties file, and scan that instead. // In both cases, we would clear the cache when the main properties are reloaded // on "h" command. ]] String translateHost(String host) { if (root.translateHosts != null) { java.io.DataInputStream di = null; // open file try { di = new java.io.DataInputStream(new java.io.FileInputStream(root.translateHosts)); } catch (java.io.IOException e) { System.out.println("Can't open "+root.translateHosts+": "+e); } if (di != null) try { String line; while ((line = di.readLine()) != null) { line = line.trim(); // remove leading and trailing space // ignore blank lines and comments if (line.length() > 0 && !line.startsWith("#")) { // change tabs to spaces line = Stringsubs.subs(line, '\t', " "); // is the host in this line? int pos = (line + " ").indexOf(" " +host+ " "); if (pos != -1) { // found -- IP address is first word di.close(); return line.substring(0, line.indexOf(" ")); } } } di.close(); } catch (java.io.IOException e) { System.out.println("Error reading "+root.translateHosts+": "+e); } } // root.translateHosts != null // no translation return null; } void init_connection() throws java.io.IOException { boolean justOpened = false; boolean sent_ok = false; // Sometimes the server is so fast that it replies before // we have set the start time, and we get a stupid result. // We set it again later, so that the time to send the request data // is not counted in the response time. time_of_request = System.currentTimeMillis(); // for HTTP 1.1, need to retry // we have to try the old connection, and, // if the server has given up on us, quietly open a new connection while (!sent_ok) { if (server_out == null || !currentHost.equals(hostWithPort) || (responseHandler != null && !responseHandler.allok)) { // no current connection, or to different place if (server_out != null) { // tell response handler not to write any more to the client // and not to close the client connection // but keep reading from server until it closes responseHandler.stopnow = true; // and we are going to need a new one responseHandler = null; // close current connection root.debug_server.println(refnum, "Closing connection to " + currentHost, 9); doing = "closing server output stream"; server_out.close(); server_out = null; } // get ready to open new connection root.debug_server.println(refnum, "Host " + hostName + ", port " + hostPort, 9); doing = "opening connection to server " + hostName + ":" + hostPort; // Support for numeric addresses // Only needed in JDK 1.0.2 // Only works in JDK 1.0.2 // You will need the source file java/net/InetAddressFix.java, // which I wrote to fix the problem that the JDK 1.0.2 libraries // do not understand IP addresses as host names. // java.net.InetAddressFix.fix(hostName); // Open connection to host java.net.Socket server_socket = new java.net.Socket(hostName, hostPort); doing = "getting server output stream"; server_out = server_socket.getOutputStream(); currentHost = hostWithPort; justOpened = true; // start the response handler responseHandler = new ResponseHandler(root, this, client_out, server_socket, clientKeepAlive, ssl_tunnel); responseHandler.start(); // the server_socket belongs to the responseHandler now server_socket = null; } // if no current connection // tell the responseHandler what method is being used for this request // assumes that requests don't overlap with previous response responseHandler.method = method; // tell the responseHandler what refnum to use for debug messages // for this request responseHandler.refnum = refnum; if (send_headers) { try { doing = "sending request to server"; int buf_length = 1024 * 20; byte[] buffer = new byte[buf_length]; // write the request header int bytes_to_go = new_headers.length(); int bytes_done = 0; while (bytes_to_go > 0) { // write one chunk at a time, max buf_length int this_chunk = (bytes_to_go > buf_length ? buf_length : bytes_to_go); new_headers.getBytes(bytes_done, bytes_done + this_chunk, buffer, 0); bytes_done += this_chunk; bytes_to_go -= this_chunk; server_out.write(buffer, 0, this_chunk); } // while bytes_to_go > 0 sent_ok = true; // leave the server_socket open for the next request } // try catch (java.io.IOException e) { // send failed -- perhaps the server gave up on us some time ago if (justOpened) { // genuine failure -- report failure to client throw e; } root.debug_server.println(refnum, "Old connection dead, retrying open", 9); // close old connection, go round and open connection again doing = "closing server output stream 3"; server_out.close(); server_out = null; } // catch java.io.IOException } // if send_headers else { // not sending headers -- SSL direct (not via chained proxy) // instead tell the client to go ahead // can't call sendResponseFromHere() because it closes the connection // not using try / catch as above -- unlikely to have old connection // to re-use (anyway, see what happens) String s = "HTTP/1.1 200 OK\r\n\r\n"; doing = "sending connect-ok to client"; byte[] buffer = new byte[50]; s.getBytes(0, s.length(), buffer, 0); client_out.write(buffer, 0, s.length()); root.debug_server.println(refnum, "Connected", 4); sent_ok = true; } // else SSL direct (not sending headers) } // while !sent_ok // Send the first chunk of POST data if any // There cannot be more than 20K because we stop reading once // we have seen the end of the headers, and the read buffer is 20K. // No debug printing -- will have already been printed while reading // if level 7 or above, but still note that it has gone, because // the modified headers will have been printed meanwhile. int bytes_sent = data_part.length(); if (bytes_sent > 0) { doing = "sending first chunk of POST data to server"; byte[] buffer = new byte[bytes_sent]; data_part.getBytes(0, bytes_sent, buffer, 0); server_out.write(buffer, 0, bytes_sent); root.debug_server.println(refnum, bytes_sent+" POST bytes to server (1)", 7); data_part = null; } if (ssl_tunnel || bytes_sent < content_length) { // Send SSL bytes or POST data from client to server. // Perhaps better not in this routine, but a separate one. // leave for now // the responseHandler tunnels the bytes from server to client // here, we tunnel the bytes from client to server // and don't stop until // (SSL) either connection gives an error or EOF // (POST data) all the data has been sent // Chunked encoding not supported yet! String reason = (ssl_tunnel ? "SSL" : "POST"); byte[] buffer = new byte[1024 * 20]; while (ssl_tunnel || bytes_sent < content_length) { int bytes_read; doing = "reading "+reason+" data from client"; try { bytes_read = client_in.read(buffer); } catch (java.io.IOException e) { // sometimes we get an exception, sometimes it returns -1 // when the connection is closed root.debug_server.println(refnum, "Exception on client read "+reason+" data", 9); bytes_read = -1; } // catch if (bytes_read < 0) { root.debug_server.println(refnum, "End of file on read from client", 7); doing = "closing client socket"; client_socket.close(); client_socket = null; // tell response handler not to write any more to the client // and not to close the client connection // but keep reading from server until it closes responseHandler.stopnow = true; // server_out gets closed by our caller return; } if (root.debug_level == 8 && root.printallcontenttypes) { root.debug_server.print(refnum, new String(buffer, 0, 0, bytes_read), 8); } else if (root.debug_level >= 7) { if (root.printallpostdata && !ssl_tunnel) { root.debug_server.print(refnum, new String(buffer, 0, 0, bytes_read), 7); } else root.debug_server.println(refnum, bytes_read+" "+reason+" bytes to server", 7); } else if (root.debug_level == 6) { root.debug_server.print(0, ">", 6); } doing = "sending "+reason+" data to server"; server_out.write(buffer, 0, bytes_read); bytes_sent += bytes_read; } // while // (SSL) unreachable, exits by "return" or IO exception // (POST) drops through when all sent // new line after POST content root.debug_server.println(0, "", 7); } // if ssl_tunnel || post data // This is really the right place to set this, // when we are sure that the request has been sent time_of_request = System.currentTimeMillis(); } // init_connection void send_error(String error_string) { try { sendResponseFromHere("502 Bad Gateway", "// " + error_string + "\r\n"); } catch (java.io.IOException e) { // debug level 9 -- often the client has closed the connection root.debug_server.println(refnum, "Exception writing error message:" + e, 9); } } // send_error void sendResponseFromHere(String code, String msg) throws java.io.IOException { doing = "sending " + code + " " + msg; String s = "HTTP/1.1 " + code + "\r\n" + (msg.length() > 0 ? "Content-Type: text/plain\r\n" + "Content-Length: " + msg.length() + "\r\n" : "") + "\r\n" + msg; root.debug_server.println(refnum, s, 4); byte[] buffer = new byte[s.length()]; s.getBytes(0, s.length(), buffer, 0); client_out.write(buffer, 0, s.length()); if (!clientKeepAlive) { // close connection to client doing = "closing client socket when response from here"; client_socket.close(); client_out = null; } } // sendResponseFromHere boolean serveFromCache() throws java.io.IOException { // see if the page is in the cache String filename = hashURL(url) + "-1000"; java.io.File infoFile = new java.io.File(root.cacheDir, filename + ".txt"); java.io.File cacheFile = new java.io.File(root.cacheDir, filename + ".bin"); if (infoFile.exists()) { root.debug_server.println(refnum, "Serving from cache", 9); doing = "reading cached file info"; java.io.DataInputStream di = new java.io.DataInputStream(new java.io.FileInputStream(infoFile)); String contentType = di.readLine(); String junkURL = di.readLine(); String statusLine = di.readLine(); // The status line is added by hand to force bad status for testing applications // readLine() returns null on end of file if (statusLine == null) statusLine = "HTTP/1.1 200 OK"; else root.debug_server.println(refnum, "Deliberately serving bad status: " + statusLine, 3); di.close(); java.io.FileInputStream is = new java.io.FileInputStream(cacheFile); // send headers back String s = statusLine + "\r\n" + "Content-Length: " + cacheFile.length() + "\r\n" + "Content-Type: " + contentType + "\r\n\r\n"; byte[] buffer = new byte[1024 * 60]; s.getBytes(0, s.length(), buffer, 0); doing = "sending header before cached file"; client_out.write(buffer, 0, s.length()); doing = "sending cached file"; int len; while ((len = is.read(buffer)) > 0) { client_out.write(buffer, 0, len); } is.close(); if (!clientKeepAlive) { // close connection to client doing = "closing client socket when response from here"; client_socket.close(); client_out = null; } root.sizesLog(refnum, url, 200 /*status_code*/, contentType, (int) cacheFile.length(), 0, // time 0, // time filename); return true; } root.debug_server.println(refnum, "Not found in cache", 9); return false; } // serveFromCache // Hash a URL to make a file name // I made up this algorithm to be fast but give a very low chance // of two URLs mapping to the same file name. // The code assumes that there never are any clashes. // I hope it is good enough -- you can't always tell with these things. // At one time I used a multiplier of 31, // taken from the Java String hashcode() function, // and that version mapped these two onto the same file name: // bq1234567890123456789012345678cR // cR1234567890123456789012345678bq // though that is obviously a contrived example. // The current multiplier is a good one: // I won't explain how I derived it! // To make it possible to test the function standalone, // this has been split into two parts. String hashURL(String u) { // Remove random numeric suffix, for myguide course materials int qpos = 0; if ((qpos = u.indexOf("?")) != -1) { try { int dummy = Integer.parseInt(u.substring(qpos + 1)); // a valid int, remove it u = u.substring(0, qpos); } catch (Exception e) { ; } } // Do the hashing String s = hashURL2(u); root.debug_server.println(refnum, "Hashed URL " + s + " <- " + u, 9); return s; } // The core of the hashURL, static with no debug, for testing static String hashURL2(String u) { int nameLen = 30; StringBuffer sb = new StringBuffer(nameLen); int a[] = new int[nameLen]; // Accumulate a sum, to spread variation over more characters int x = 0; // If the last characters differ by 26, the output is the same // until the difference has had a chance to propagate around "x". // Also it spreads any difference in the last character of the URL // over more output characters in the output. String xu = u + "zzzzz"; for (int i = 0; i < xu.length(); i++) { x = x * 1008949713 + xu.charAt(i); a[i % nameLen] += x; } for (int i = 0; i < nameLen; i++) { sb.append((char)((Math.abs(a[i]) % 26) + 'a')); } return sb.toString(); } // Test harness for HashURL function public static void main(String[] args) { java.io.DataInputStream my_in = new java.io.DataInputStream(System.in); String u = ""; System.out.println("Type q to quit"); try { while (!u.equals("q")) { System.out.print("> "); System.out.flush(); u = my_in.readLine(); System.out.println("Hashed URL " + hashURL2(u) + " <- " + u); } } catch (Exception e) { System.out.println(e); } } } // class Connection //====================================================================== class ResponseHandler extends Thread { Proxy root; Connection parent; java.net.Socket server_socket = null; java.io.OutputStream client_out; // from here to client java.io.InputStream server_in; // from server to here String doing; boolean clientKeepAlive; public int refnum = 0; int chunkBytesToGo; String chunkSize; boolean chunkInTrailers; boolean chunkSkipHeaders; boolean OK; public boolean allok = true; public boolean stopnow = false; boolean ssl_tunnel; public String method = "unknown"; long time_of_first_byte; long time_of_last_byte; java.io.FileOutputStream cacheOut = null; String cacheFileName = null; public ResponseHandler(Proxy p, Connection c, java.io.OutputStream o, java.net.Socket s, boolean ka, boolean ssl) { root = p; parent = c; client_out = o; server_socket = s; clientKeepAlive = ka; ssl_tunnel = ssl; } public void run() { try { doing = "getting server input stream"; server_in = server_socket.getInputStream(); root.debug_server.println(refnum, "Class of input stream " + server_in.getClass().toString(), 9); // loop to handle several responses while (allok) copy_data(); } catch (Exception e) { String errmsg = "Proxy exception in ResponseHandler " + doing + ": " + e; root.debug_server.println(refnum, errmsg, 0); if (!stopnow && client_out != null) // (also closes client connection) parent.send_error(errmsg); allok = false; } try { // close connection to server doing = "closing server socket 4"; server_socket.close(); } catch (Exception e) { String errmsg = "Proxy exception " + doing + ": " + e; root.debug_server.println(refnum, errmsg, 0); } } // run // copy the data for one response void copy_data() throws java.io.IOException { // plenty of memory, and fast lines -- let's use a bigger buffer byte[] buffer = new byte[1024 * 60]; String headers = ""; int bytes_read; boolean in_headers = true; int content_length = -1; // init to keep compiler happy String transfer_encoding; String connection_header = ""; String content_type = ""; // init to keep compiler happy boolean print_content = false; // init to keep compiler happy int bytes_done = 0; int header_length = 0; boolean chunkedYes = false; int status_code = 506; // unused bad code for case when bad integer if (ssl_tunnel) { // for SSL, don't look at any headers, just be transparent from the start in_headers = false; content_type = "connect"; print_content = (root.debug_level == 8 && root.printallcontenttypes); // leave content_length = -1, chunkedYes = false from above } // loop reading blocks from the server until we have processed // all the content for a single response, or the connection is closed. while (true) { doing = "reading from server"; try { bytes_read = server_in.read(buffer); } catch (java.io.IOException e) { // sometimes we get an exception, sometimes it returns -1 // when the connection is closed root.debug_server.println(refnum, "Exception on server read", 9); bytes_read = -1; } // catch if (bytes_read < 0) { // Could be: // if (bytes_done == 0) // probably just an old connection that the server closed earlier // or we closed it in order to go to a different host // else // end of file. Server has closed the connection // must close connection to client -- HTTP 1.0 style // In either case, we do more or less the same. root.debug_server.println(refnum, "Server closed connection after " + bytes_done + " bytes", 7); // we may have been told to stop -- new request gone to different server // in which case we quietly pack up, without sending anything to client if (!stopnow) { // The server may have closed the connection before we have seen the // end of the headers (eg if our algorithm for detecting the end of the // headers is flawed). // In this case, send the headers to the client. // We normally keep the headers, and don't send them to the client, // until we have seen the end of the headers so that we can modify them. if (in_headers && bytes_done > 0) { // Send headers to the client byte[] send_buffer = new byte[headers.length()]; headers.getBytes(0, headers.length(), send_buffer, 0); doing = "writing headers to client after connection closed"; client_out.write(send_buffer, 0, headers.length()); doing = "debug printing"; root.debug_server.println(refnum, "Server closed connection before end of headers seen after " + bytes_done + " bytes", 0); } doing = "closing client output stream"; client_out.close(); client_out = null; } // don't call copy_data again // and tell parent not to use this connection again allok = false; break; // exit "while (true)" } // bytes_read < 0 if (bytes_done == 0) time_of_first_byte = System.currentTimeMillis(); bytes_done += bytes_read; doing = "debug printing"; if (in_headers) { // append this chunk to accumulated response headers headers = headers + new String(buffer, 0, // hibyte 0, // offset bytes_read); // see if end of headers has been received now // slow to do this whatever the debug level, // but the debug level may change mid-page int end_pos = headers.indexOf("\r\n\r\n"); header_length = end_pos + 4; if (end_pos == -1) { // some servers send Unix-style line endings end_pos = headers.indexOf("\n\n"); header_length = end_pos + 2; } if (end_pos == -1) { // some servers, eg Real Helix, sometimes send a mixture of // standard and Unix-style line endings // eg the last line of headers ends with just \n // followed by a blank line consisting of \r\n. end_pos = headers.indexOf("\n\r\n"); header_length = end_pos + 3; } if (end_pos == -1) { // in headers but not seen end -- doesn't happen at all often! // just do some debug printing to // show that something is happening... // Don't send anything to client yet -- may need to modify headers // See headers even if blank line never arrives. if (root.debug_level >= 8) { String str = new String(buffer, 0, // hibyte 0, // offset bytes_read); root.debug_server.print(refnum, str, 8); } else if (root.debug_level == 7) { root.debug_server.println(refnum, bytes_read + " bytes of header", 7); } else if (root.debug_level == 6) { root.debug_server.print(0, ".", 6); } } // in headers but not seen end yet else { // end_pos != -1 // end of headers has been received // get the Content-Length content_length = -1; try { content_length = Integer.parseInt( getHeaderField(headers, "Content-Length", end_pos)); } catch(Exception e) {} // HTTP/1.1 304 Not Modified int space_pos = headers.indexOf(" "); try { status_code = Integer.parseInt( headers.substring(space_pos + 1, headers.indexOf(" ", space_pos + 1))); } catch(Exception e) {} root.debug_server.println(refnum, "HTTP status = " + status_code, 9); if (status_code == 304 || method.equals("HEAD") || status_code == 204) { // ignore Content-Length, // which is length of body which is not being sent // act as if length == 0 content_length = 0; } // get the Transfer-Encoding transfer_encoding = getHeaderField(headers, "Transfer-Encoding", end_pos); chunkedYes = "chunked".equals(transfer_encoding); root.debug_server.println(refnum, "Transfer-Encoding: [" + transfer_encoding + "], chunked = " + chunkedYes, 9); if (chunkedYes) { // where the message body starts in this buffer // ie the number of bytes from the start of the buffer // to the start of the next (first) chunk chunkBytesToGo = header_length - (bytes_done - bytes_read); // the line containing the chunk size string chunkSize = ""; chunkInTrailers = false; chunkSkipHeaders = true; } // chunkedYes // get the "Connection: close" header connection_header = getHeaderField(headers, "Connection", end_pos); root.debug_server.println(refnum, "Connection: [" + connection_header + "]", 9); // get the Content-Type content_type = getHeaderField(headers, "Content-Type", end_pos); if (content_type == null) content_type = "unknown"; // Open cache file for writing initCache(status_code, content_type); // print text only, at level 8 only // Ideally we should recalculate this while the content is arriving // so that we can turn it on and off during a long download (eg a jar file), // but that would involve some repetition of code. print_content = (root.debug_level == 8 && (content_type.startsWith("text") || content_type.startsWith("application/x-javascript") || root.printallcontenttypes)); root.debug_server.println(refnum, "Content-Type: [" + content_type + "]", 9); if (print_content) { // print everything String str = new String(buffer, 0, // hibyte 0, // offset bytes_read); root.debug_server.print(refnum, str, 8); } else if (root.debug_level >= 8) { // print this block up to end of headers String str = new String(buffer, 0, // hibyte 0, // offset header_length - (bytes_done - bytes_read)); root.debug_server.print(refnum, str, 8); } else if (root.debug_level >= 5) { // print just headers root.debug_server.println(refnum, headers.substring(0, end_pos), 5); } if (root.debug_level == 7 && bytes_done > header_length) { // any content arrived in this block in addition to end of headers // header_length includes the blank line, end_pos doesn't root.debug_server.println(refnum, (bytes_done - header_length) + " bytes (1)", 7); } if (root.debug_level == 4) { // print just status int first_line_end = headers.indexOf("\n"); root.debug_server.println(refnum, headers.substring(0, first_line_end), 4); } // There may be some page data in the buffer after the end // of the headers. // Or, if the header is a "100 Continue", more headers. // We want to change the headers and/or split off the Continue. // So split the headers string into two parts. // The blank line stays with the headers. // The data part may be empty. String data_part = headers.substring(header_length); headers = headers.substring(0, header_length); // After 100 Continue expect a real header if (headers.startsWith("HTTP/1.1 1")) { // Send this header to the client byte[] send_buffer = new byte[header_length]; headers.getBytes(0, header_length, send_buffer, 0); doing = "writing to client"; client_out.write(send_buffer, 0, header_length); doing = "debug printing"; // throw away this header headers = data_part; // pretend we never saw it bytes_done -= header_length; // leave in_headers true // go round and fetch some more data // (assumes that the next headers are not already in the buffer) } else { // Not a 100 Continue // Modify headers if required then send headers and data to client in_headers = false; // Handle Microsoft NTLM authentication // If we get "HTTP/1.1 401 Unauthorized" // we must reassure the browser that we are not going to mix // requests from different clients on the same server session. if (status_code == 401) { // Find a WWW-Authenticate header // Assumes we don't have this string in the value part of a header... int p_pos = headers.indexOf("WWW-Authenticate:"); if (p_pos != -1) { // Insert new header headers = headers.substring(0, p_pos) + "Proxy-Support: Session-Based-Authentication\r\n" + headers.substring(p_pos); // Note that we do not change the variable header_length, // which is used along with bytes_done to work out when we // have seen all the content. // So from now on we use headers.length() if we need the // actual length of the modified headers. if (root.debug_level >= 5) { // print modified headers // includes the blank line, but that's OK root.debug_server.println(refnum, headers, 5); } } // WWW-Authenticate } // status_code == 401 if (root.blockRedirects && (status_code == 302 || status_code == 301)) { // Change to 500 say int p_pos = headers.indexOf("" + status_code); headers = headers.substring(0, p_pos) + "500" + headers.substring(p_pos + 3); root.debug_server.println(refnum, "Redirect blocked: status " + status_code + " changed to 500", 3); } // Send possibly-modified headers + data if any to client // Send headers to the client byte[] send_buffer = new byte[headers.length()]; headers.getBytes(0, headers.length(), send_buffer, 0); doing = "writing to client"; client_out.write(send_buffer, 0, headers.length()); if (data_part.length() > 0) { // Send first part of data to the client send_buffer = new byte[data_part.length()]; data_part.getBytes(0, data_part.length(), send_buffer, 0); doing = "writing first part of data to client"; client_out.write(send_buffer, 0, data_part.length()); root.debug_server.println(refnum, "Written " + data_part.length() + " bytes to client", 9); // If chunked, we write to the cache later // in the call to chunkedEncodingFinished() // to which we pass the whole buffer if (!chunkedYes && cacheOut != null) { cacheOut.write(send_buffer, 0, data_part.length()); } } doing = "debug printing"; } // else not a 100 Continue } // seen end of headers } // in_headers else { // not in headers // debug printing, then send data to client if (print_content) { // keep all this text together synchronized(root.debug_server) { // re-print the ref number if we have been interrupted by a different thread root.debug_server.printrefcond(refnum); String str = new String(buffer, 0, // hibyte 0, // offset bytes_read); // print everything root.debug_server.print(0, str, 8); } // synchronized } else // print_content { // not in headers, and not printing content if (root.debug_level >= 7) { root.debug_server.println(refnum, bytes_read + " bytes (2)", 7); } else if (root.debug_level == 6) { root.debug_server.print(0, ".", 6); } } // not print_content if (!chunkedYes && cacheOut != null) { cacheOut.write(buffer, 0, bytes_read); } // we may have been told to stop -- new request gone to different server // in which case we throw away this data to avoid mixing it with the // response data from the newer request // but we keep reading from the socket until it closes if (!stopnow) { doing = "writing to client"; client_out.write(buffer, 0, bytes_read); doing = "debug printing"; } } // not in_headers // For HTTP 1.1 we must use the Content-Length or chunked encoding // to decide if we have finished // Do it here, because we need to look at the chunked data if ( !in_headers && ( (content_length != -1 && bytes_done >= content_length + header_length) || (chunkedYes && chunkedEncodingFinished(buffer, bytes_read)))) { // we have read all the content root.debug_server.println(refnum, "All data read", 9); // check the total, mainly to flag any bug in this program if (!chunkedYes && content_length != -1 && bytes_done > content_length + header_length) { root.debug_server.println(refnum, "Too much data read " + bytes_done + " expecting " + (content_length + header_length), 0); } if (cacheOut != null) { cacheOut.close(); cacheOut = null; } if ("close".equals(connection_header) || !clientKeepAlive) { // server is going to close the connection // or client is version 1.0 with no Keep-Alive and expects us to close the connection doing = "closing client output stream"; client_out.close(); client_out = null; allok = false; // connection to server remains open until the server closes it } // else leave the connection to the client open -- HTTP 1.1 style break; // exit "while (true)" } // if finished } // while (true) -- reading blocks from server // End of file on read from server or correct length has been read // HTTP 1.1 version -- don't close connection to server here any more if (bytes_done > 0 && root.debug_level == 6) { root.debug_server.println(0, "]", 6); } time_of_last_byte = System.currentTimeMillis(); // if no content length, or server closes connection before all data sent, // cache file will still be open if (cacheOut != null) { cacheOut.close(); cacheOut = null; } // Print timestamp at end // Don't print timestamp last time round when server closes connection if (bytes_done > 0) root.debug_server.println(refnum, "End of data", 7); // We get a zero-byte chunk when an HTTP 1.1 connection is closed after timing out if (bytes_done > 0) { root.sizesLog(refnum, parent.url, status_code, content_type, bytes_done - header_length, time_of_first_byte - parent.time_of_request, time_of_last_byte - parent.time_of_request, cacheFileName); } } // copy_data String getHeaderField(String headers, String fieldName, int end_pos) { fieldName += ": "; // assume just one space int field_pos = headers.toLowerCase().indexOf(fieldName.toLowerCase()); if (field_pos != -1 && field_pos < end_pos) { int field_end = headers.indexOf("\r", field_pos); if (field_end == -1) { // Unix style -- must be a \n somewhere, or end_pos would not be set field_end = headers.indexOf("\n", field_pos); } return headers.substring(field_pos + fieldName.length(), field_end); } return null; } // getHeaderField boolean chunkedEncodingFinished(byte[] buffer, int bytes_read) throws java.io.IOException { // look at the given block of data and decide if we have come to the // end of the content in chunked encoding // the data for the cache starts at the beginning // if chunkBytesToGo > 2 int cacheStart = 0; root.debug_server.println(refnum, "bytes_read " + bytes_read + " chunkBytesToGo " + chunkBytesToGo + " chunkInTrailers " + chunkInTrailers, 9); // first see if we are in the final trailers part of the message // and deal with that, as it's simpler if (chunkInTrailers) { // add these trailers to the chunkSize string chunkSize += new String(buffer, 0, // hibyte 0, // offset bytes_read); // count -- old data stays in buffer int endpos = chunkSize.indexOf("\r\n\r\n"); if (endpos != -1) { // the end of the trailers was in this buffer // check if (endpos + 4 != chunkSize.length()) { root.debug_server.println(refnum, "Too much chunked data read CRLFCRLF at " + endpos + " length " + chunkSize.length(), 0); } // that's it -- no more data to be read, chunkedEncodingFinished return true; } // need more buffers before we see the end, not finished return false; } // chunkInTrailers // not in the trailers // traverse the chunk(s) in this buffer while (true) { root.debug_server.println(refnum, "bytes_read " + bytes_read + " chunkBytesToGo " + chunkBytesToGo + " chunkInTrailers " + chunkInTrailers + " cacheStart " + cacheStart, 9); // in the main body of the message, ie the chunks themselves // chunkBytesToGo is the offset of the end of the current chunk // from the beginning of this buffer // chunkBytesToGo can be zero, meaning we are between chunks // ie somewhere in, or right at the start of, the chunk-size line if (bytes_read <= chunkBytesToGo) { // this buffer contains less than the rest of the current chunk // or exactly to the end of the current chunk // note what we have seen, ie adjust chunkBytesToGo to be ready // for the next buffer, and say we have not finished chunkBytesToGo -= bytes_read; // chunkSkipHeaders is always false here: // this can't happen first time through if (cacheOut != null) { root.debug_server.println(refnum, "Cache write 1 from " + cacheStart + " len " + (bytes_read - cacheStart), 9); cacheOut.write(buffer, cacheStart, bytes_read - cacheStart); } return false; } // Write the rest of the current chunk to the cache if (chunkBytesToGo - 2 - cacheStart > 0 && !chunkSkipHeaders && cacheOut != null) { root.debug_server.println(refnum, "Cache write 2 from " + cacheStart + " len " + (chunkBytesToGo - 2 - cacheStart), 9); cacheOut.write(buffer, cacheStart, chunkBytesToGo - 2 - cacheStart); } // Just skip the first time through // Set the flag to say never skip again chunkSkipHeaders = false; // we have dealt with the first chunkBytesToGo bytes in this buffer // which takes us exactly to the end of a chunk (past the trailing CRLF) // add the next chunk size line (+ the data of the next chunk if any, // which we don't care about) // to the accumulated string, in case the chunk size line is split // between two (or more!) buffers chunkSize += new String(buffer, 0, // hibyte chunkBytesToGo, // offset bytes_read - chunkBytesToGo); // count of bytes // see if the chunkSize line is now complete int linelen = chunkSize.indexOf("\r\n"); if (linelen != -1) { // we have the whole line // now, it is allowed to have extensions after a semicolon... int cslen = chunkSize.indexOf(";"); if (cslen == -1 || cslen > linelen) { // no semicolon in the line, the cs number goes to the end of the line cslen = linelen; } // chunk size is a hex integer // Apache adds a trailing space (in case of extensions I guess) int cs = 0; try { cs = Integer.parseInt(chunkSize.substring(0, cslen).trim(), 16); } catch(Exception e) { root.debug_server.println(refnum, "Chunk size: " + e, 0); } root.debug_server.println(refnum, "Chunk size [" + chunkSize.substring(0, cslen) + "]", 9); root.debug_server.println(refnum, "Chunk size decimal " + cs, 9); if (cs == 0) { // the start of the last chunk, almost finished int endpos = chunkSize.indexOf("\r\n\r\n"); if (endpos != -1) { // the end of the trailers was in this buffer // check if (endpos + 4 != chunkSize.length()) { root.debug_server.println(refnum, "Too much chunked data read CRLFCRLF at " + endpos + " length " + chunkSize.length(), 0); } // that's it -- no more data to be read, chunkedEncodingFinished return true; } // read more buffers // accumulate all the trailers in the chunkSize string // until we see CRLFCRLF ie a blank line // the whole of the rest of this buffer is already // in the chunkSize string chunkInTrailers = true; return false; } // cs == 0 // chunk size != 0 // the start of a new chunk // the start of this block of data cacheStart = chunkBytesToGo + linelen + 2; // chunkBytesToGo is the offset of the end of the current chunk // from the beginning of this buffer // 2 for the CRLF after cs string, 2 for CRLF after the data chunkBytesToGo += linelen + 2 + cs + 2; // clear the string for next time -- we always append chunkSize = ""; // go round the loop again // there may be more than one chunk in the buffer } // if (linelen != -1) ie we have seen the whole chunk size line else { // this buffer ended half-way through the chunk-size line // go and get another return false; } } // while true } // chunkedEncodingFinished void initCache(int st, String ct) throws java.io.IOException { // readonly mode means serve it from the cache if we have it, // but never save anything. // This allows you to substitute your own data for particular pages // or images, for debugging. if (root.cacheDir == null || "readonly".equals(root.cachemode) || st != 200) return; String filenameBase = parent.hashURL(parent.url); // Find an unused file name synchronized (root) { int counter = 1000; // Apologies for the code repetition -- not enough to warrant a separate method // or a boolean variable cacheFileName = filenameBase + "-" + counter; java.io.File infoFile = new java.io.File(root.cacheDir, cacheFileName + ".txt"); while (infoFile.exists()) { counter++; cacheFileName = filenameBase + "-" + counter; infoFile = new java.io.File(root.cacheDir, cacheFileName + ".txt"); } java.io.PrintStream po = new java.io.PrintStream(new java.io.FileOutputStream(infoFile)); po.println(ct); po.println(parent.url); if (po.checkError()) { throw new java.io.IOException("Charles proxy error writing to cache info file"); } po.close(); } // synchronized root.debug_server.println(refnum, "Creating cache file " + cacheFileName, 9); java.io.File cacheFile = new java.io.File(root.cacheDir, cacheFileName + ".bin"); cacheOut = new java.io.FileOutputStream(cacheFile); } } // class ResponseHandler //====================================================================== class DebugServer extends Thread { Proxy root; java.net.ServerSocket listen_socket; java.net.Socket debug_socket; java.io.OutputStream debug_out = null; byte[] buffer = new byte[1024 * 80]; int last_ref = 0; public DebugServer(Proxy p, int port) { root = p; try { listen_socket = new java.net.ServerSocket(port); } catch (java.io.IOException e) { Proxy.fail(e, "Exception creating debug server socket"); } System.out.println("Debug server listening on port " + port); this.start(); } public void run() { try { while (true) { // On some JVMs, with modern threading mechanisms, we sometimes get to // here before root.debug_server has been set. if (root.debug_server != null) root.debug_server.println(0, "Proxy Server running, type h for help", 0); java.net.Socket debug_socket = listen_socket.accept(); root.debug_server.println(0, "Debug Server Accepted", 9); try { debug_out = debug_socket.getOutputStream(); } catch (java.io.IOException e) { System.out.println("Exception getting debug output stream: " + e); } // create another new thread here to read the stream // for debug level settings. // When it gets one, it has to set the variable in the parent. new DebugListener(root, debug_socket); } } catch (java.io.IOException e) { Proxy.fail(e, "Exception in debug server loop"); } } // run public synchronized void print(int ref, String s, int level) { if (root.debug_level < level) { return; } // print current date and time String timestamp = ""; if (root.printtimestamps) { timestamp = "[" + new java.util.Date().toString() + "]"; } if (ref > 0) { s = "[" + level + "-" + ref + "]" + timestamp + s; last_ref = ref; } // Change bare LF -> CRLF // do this before turning the string into a byte array // only really needed for telnet clients, but harmless on console if (debug_out != null && root.fixcrlf) { // change Unix line endings to DOS // remove all CRs if any s = Stringsubs.subs(s, '\r', ""); // add them back s = Stringsubs.subs(s, '\n', "\r\n"); } // get string into byte array in case (a) need to fix ctrls, or (b) debug out, or both int count = s.length(); s.getBytes(0, count, buffer, 0); // old method using stringsubs: too slow! // could make Stringsubs.subs faster by not copying the string if char not present if (false) { // make control chars printable to avoid beeps and switching char sets // putting in eg ^ and G for a control-G character // 1 -> ^A, 2 -> ^B etc for (int i = 0; i < 32; i++) { if (i != 10 && i != 13) // Leave LF and CR as is s = Stringsubs.subs(s, (char)i, "^" + (char)(i + 'A' - 1)); } // DEL s = Stringsubs.subs(s, (char)127, "^?"); } if (root.fixctrlchars) { // make control chars printable to avoid beeps and switching char sets // putting in eg ^ and G for a control-G character // Keep TAB because it is used in the sizeitem line // and won't screw up the terminal emulator. // this is faster than using stringsubs // byte is signed, -128 .. 127 for (int i = count - 1; i >= 0; i--) { if ((buffer[i] < 32 && buffer[i] >= 0 // control chars && buffer[i] != 9 && buffer[i] != 10 && buffer[i] != 13) // less TAB, LF and CR || buffer[i] == 127) // plus DEL { // make space for one extra byte System.arraycopy( buffer, //src i, // src_position buffer, // dst i + 1, // dst_position count - i); // length count++; // put in eg ^ and G for a control-G character, ^? for DEL buffer[i] = (byte)'^'; buffer[i + 1] = (buffer[i + 1] == 127 ? (byte)'?' : (byte)(buffer[i + 1] + 'A' - 1)); } } // for each byte // turn the buffer back into a string for the console // but keep the buffer for sending to debug_out s = new String(buffer, 0, 0, count); } // if (root.fixctrlchars) System.out.print(s); if (debug_out != null) { try { debug_out.write(buffer, 0, count); } catch (java.io.IOException e) { System.out.println("Exception writing debug message:" + e); debug_out = null; } } } // print public void println(int ref, String s, int level) { this.print(ref, s + "\r\n", level); } // conditional re-print of ref number public void printrefcond(int ref) { if (ref != last_ref) { this.print(0, "\r\n", 0); this.print(ref, "", 0); } } } // class DebugServer class DebugListener extends Thread { Proxy root; java.io.InputStream debug_in = null; boolean OK = true; public DebugListener(Proxy p, java.net.Socket i) { root = p; try { debug_in = i.getInputStream(); } catch (java.io.IOException e) { root.debug_server.println(0, "Exception getting debug input stream:" + e, 0); OK = false; } if (OK) this.start(); } public void run() { try { int b = 0; // read returns -1 on EOF. while (b != -1) { b = debug_in.read(); root.process_char(b); } // while } catch (java.io.IOException e) { System.out.println("Exception getting debug character: " + e); } // End of file on read from telnet // let this thread exit } // run } // DebugListener class Stringsubs { public Stringsubs() { } public static String subs(String s, char from, String to) { // speed-up if (s.indexOf(from, 0) == -1) { return s; } StringBuffer out = new StringBuffer(s.length() + 10); int oldpos = 0; int pos; while ((pos = s.indexOf(from, oldpos)) >= 0) { out.append(s.substring(oldpos, pos)) .append(to); oldpos = pos + 1; } // and the rest out.append(s.substring(oldpos)); return(out.toString()); } public static String subs(String s, String from, String to) { // speed-up if (s.indexOf(from, 0) == -1) { return s; } StringBuffer out = new StringBuffer(s.length() + 10); int oldpos = 0; int pos; while ((pos = s.indexOf(from, oldpos)) >= 0) { out.append(s.substring(oldpos, pos)) .append(to); oldpos = pos + from.length(); } // and the rest out.append(s.substring(oldpos)); return(out.toString()); } } // class Stringsubs class Glob { static boolean debug = false; // test harness public static void main(String[] args) { java.io.DataInputStream my_in = new java.io.DataInputStream(System.in); String s1, s2; // Turn on debug for testing debug = true; try { while (true) { System.out.print("target > "); System.out.flush(); s1 = my_in.readLine(); System.out.print("pattern > "); System.out.flush(); s2 = my_in.readLine(); System.out.println(s1 + " " + s2 + " " + matchAny(s1, s2)); } } catch (Exception e) { System.out.println(e); } } public static boolean matchAny(String target, String pattern) { // check against a string of the form "*.logica.com|*.logica.co.uk" if (target == null || pattern == null) { return false; } java.util.StringTokenizer stok = new java.util.StringTokenizer(pattern, "|"); while (stok.hasMoreTokens()) { String tok = stok.nextToken(); if (match(target, tok)) { return true; } } return false; } public static boolean match(String target, String pattern) { // the real functionality is in match2 so we can trace the return value easily! if (debug) System.out.println("[" + target + "] /" + pattern + "/ -> "); boolean res = match2(target, pattern); if (debug) System.out.println("-> " + res); return res; } // match target against a pattern containing wildcards like the Unix shell // * for a sequence of zero or more of any character, // ? for any single character. public static boolean match2(String target, String pattern) { if (target == null || pattern == null) { return false; } // this includes both strings empty if (target.equals(pattern)) { return true; } if (pattern.length() == 0) { return false; } if (pattern.charAt(0) == '*') { // try greatest match first, then backtrack for (int j = target.length(); j >= 0; j--) { if (match(target.substring(j), pattern.substring(1))) { return true; } } return false; } if (target.length() == 0) { return false; } // if first chars match, check the rest recursively if (target.charAt(0) == pattern.charAt(0) || pattern.charAt(0) == '?') { return match(target.substring(1), pattern.substring(1)); } return false; } // match2 } // class Glob class ClientClosedConnectionException extends Exception { public ClientClosedConnectionException() { super(); } public ClientClosedConnectionException(String s) { super(s); } } // class ClientClosedConnectionException