// (c) 1999, Intuit, Inc // $Id: HTTPConnection.java,v 1.35 2003/10/14 22:07:56 srevilak Exp $ /* * @(#)HTTPConnection * * Copyright (c) 1999 Intuit Inc. All Rights Reserved. * */ package com.intuit.util; /** * IMPORTANT: DO NOT make this class dependent on any * com.intuit.* class. We need to be able to distribute this * to other companies. If you have config stuff you want to * depend on, enhance the WebClient class. billo 12-oct-2000 * * It's kind of wrong that Log class is used here. But it would be too painful * to make the logging capability a parameter or something, so let's * live with it. If you are trying to use this class outside * Intuit's class library, just replace all the //Log.whatever calls * with your own logging mechanism. I isolated all calls to //Log.* in the * methods: error, debug, and info. */ import java.io.*; import java.net.*; import java.util.*; import java.security.*; import javax.net.ssl.*; /** * <p>HTTPConnection class is a simple instance-oriented HTTP client. * Unlike the crappy HttpUrlConnection class that comes with java, * you can set proxy settings on each connection you make, instead * of globally for your whole process. * * <p>See the main() method for examples of using this class. * * <p>Note: Sun's JSSE Java SSL implementation is required to use this class. * */ public class HTTPConnection extends Thread { String hostName = null; int port = 80; String proxyHostName = null; int proxyPort = 0; Vector headers = new Vector(); String cookiedata = null; String requestBody; String requestHeaders; Vector responseHeaders = new Vector(); String responseHeadersRaw = null; String responseBody = null; String responseStatus; int responseBytes = 0; char[] readbuf; byte[] databuf; byte[] responseBodyBinary = null; Exception caughtException = null; String urlFile; URL url; String method = null; boolean followRedirect = false; boolean shutdown = false; int maxRedirects = MAX_REDIRECT; int redirectCount = 0; Object sock; static final int BUFSIZE = 8192 * 16; static final int MAX_REDIRECT = 5; //static final int BUFSIZE = 128; int TIMEOUT = 180000; // milliseconds static final String HTTP_VERSION = "HTTP/1.0"; public HTTPConnection() { method = "GET"; readbuf = new char[BUFSIZE]; //Util.trace("httpconnection constructor"); }; public HTTPConnection (URL u) { this(); setURL(u); } public HTTPConnection (String u) { this(); setURL(u); } public void setURL(URL u) { url = u; hostName = url.getHost(); urlFile = url.getFile(); setDaemon(true); if (url.getPort() > 0) { port = url.getPort(); } else if (u.getProtocol().equalsIgnoreCase("https")) { //addSSLProvider(); port = 443; useSSL = true; } } public void setURL(String u) { boolean SSL = false; if (u.startsWith("https:")) { addSSLProvider(); u = "http:" + u.substring(6); SSL = true; port = 443; } try { url = (new URL(u)); hostName = url.getHost(); urlFile = url.getFile(); setDaemon(true); if (url.getPort() > 0) { port = url.getPort(); } else if (url.getProtocol().equalsIgnoreCase("https")) { SSL = true; port = 443; } if (SSL) { useSSL = true; } } catch (MalformedURLException e) { // we're hosed... error(this, e); } } protected void debug(Object o, String s) { //Log.debug(o, s); } protected void info(Object o, String s) { //Log.info(o, s); } protected void error(Object o, Throwable t) { //Log.error(o, t); } protected void error(Object o, String s) { //Log.error(o, s); } /** * Set the timeout on connections. CAUTION: do not mess with this * except under special circumstances. i.e. you know * with absolute certainty that an HTTP server will NEVER be slower * than the timeout and succeed in the response. The deault is * 180000 ms (3 minutes), which is what most browsers use. */ public void setTimeout(int ms) { TIMEOUT = ms; } public void setMaxRedirects(int max) { maxRedirects = max; } SSLSocketFactory factory = null; public void setFactory(SSLSocketFactory f) { factory = f; } protected OutputStream outputStream = null; /** * tell HTTPConnection to use this output stream * instead of writing the response body into * a byte array. It's up to the caller to open and close the * supplied stream. */ public void setOutputStream(OutputStream out) { outputStream = out; } /** * Parse a cookie header into an array of cookies as per * RFC2109 - HTTP Cookies * * @param cookieHdr The Cookie header value. * @return hash of cookie name-values. or null if * empty or null header */ public static Hashtable parseCookieHeader(String cookieHdr) { return parseCookieHeader(cookieHdr, null); } /** * Parse a cookie header into an array of cookies as per * RFC2109 - HTTP Cookies * * @param cookieHdr The Cookie header value. * @param ht The Hashtable you want to store values in. If null, one is created. * @return hash of cookie name-values. or null if * empty or null header */ public static Hashtable parseCookieHeader(String cookieHdr, Hashtable ht) { if(cookieHdr == null || cookieHdr.length() == 0) return null; Hashtable jar = new Hashtable(); if (ht == null) { jar = new Hashtable(); } else { jar = ht; } StringTokenizer stok = new StringTokenizer(cookieHdr, ";"); while (stok.hasMoreTokens()) { try { String tok = stok.nextToken(); int equals_pos = tok.indexOf('='); if (equals_pos > 0) { String name = tok.substring(0, equals_pos); name = name.trim(); String value = tok.substring(equals_pos + 1); jar.put(name, value); } else if ( tok.length() > 0 && equals_pos == -1 ) { String name = tok; jar.put(name, ""); } } catch (IllegalArgumentException badcookie) { } catch (NoSuchElementException badcookie) { } } return jar; } private SSLSocketFactory getFactory() { if (factory == null) { factory = (SSLSocketFactory)SSLSocketFactory.getDefault(); } return factory; } /** * Select the proxy to use for the connection. * * @param host Host name or IP address. * @param port Port number */ public void setProxy(String host, int port) { proxyPort = port; proxyHostName = host; } /** * select the method. * * @param m "GET" or "POST" or whatever. */ public void setMethod(String m) { method = m; } /** * Tell the connection to follow redirects or not. * */ public void setFollowRedirects(boolean enable) { followRedirect = enable; } /** * Set the request body. * * @param body The body to be posted or put. */ public void setBody(String body) { requestBody = body; } /** * retrieve the response body after connect() is called. * @return the response as a String, in ISO-8859-1 encoding. * */ public String getBody() { if (responseBody == null && responseBodyBinary != null) { try { responseBody = new String(responseBodyBinary, "ISO-8859-1"); } catch (UnsupportedEncodingException e) { responseBody = new String(responseBodyBinary); } } return responseBody; } /** * Returns the response body as a byte array. * Call this *after* connect() */ public byte[] getBodyBytes() { return responseBodyBinary; } /** * Return the response headers as a single String. * After connect(), obviously. */ public String getHeaders() { return responseHeadersRaw; } /** * Return the response headers as a vector of strings. * After connect(), obviously. */ public Vector getHeadersVector() { return responseHeaders; } private boolean useSSL = false; /** * Enable SSL using JSSE library. See the JSSE documentation * for an explanation of how to use this. These parameters * are currently ignored. Sorry. Make sure you call addSSLProvider() * first! * * @param certAlias * @param keyStore * @param keyStorePassword * @param keyPassword * @param ignoreHostnameMismatch * */ public void enableSSL(String certAlias, String keyStore, String keyStorePassword, String keyPassword, boolean ignoreHostnameMismatch) { useSSL = true; } public String getStatus() { return responseStatus; } public void addHeader(String h, String v) { headers.add(h + ": " + v); } /** * add a cookie name/value pair. * @param cookie should be a string like myvar=myval */ public void addCookie (String cookie) { if (cookiedata != null) { cookiedata += "; " + cookie; } else { cookiedata = cookie; } } long connectTime; /** * Perform the HTTP request and response. After making this call * you can retrieve the status, body and headers with the * appropriate getWhatever() methods. */ public void doConnect() throws Exception { connectTime = System.currentTimeMillis(); // check if it is malformed URL if ( (urlFile == null) || urlFile.equals("") ){ urlFile = "/"; } start(); int byteCount = responseBytes; while (isAlive()) { if (responseBytes > byteCount) { byteCount = responseBytes; connectTime = System.currentTimeMillis(); } else if (System.currentTimeMillis() - connectTime > TIMEOUT) { // timeout with no new bytes received. break; } sleep(5); } if (isAlive()) { // We timed out. abort(); throw new Exception("HTTP connection timeout."); } else if (caughtException != null) { throw caughtException; } } protected void abort() { shutdown = true; interrupt(); } /** * Old method for compatibility: returns the entire response * including headers, status and body as a string. Don't use this unless * You need to since it will allocate a big String. * * You should use doConnect() instead. This method will be deprecated * on or before July 2001. */ public String connect() throws Exception { doConnect(); responseBody = getBody(); // for compatibility with old version return responseStatus + "\r\n" + responseHeadersRaw + "\r\n" + responseBody; } protected void buildHeaders() { StringBuffer sb = new StringBuffer(""); if ((proxyHostName == null) || useSSL) { sb.append("Host: " + hostName + "\r\n"); } for (Enumeration e = headers.elements(); e.hasMoreElements(); ) { sb.append((String)e.nextElement() + "\r\n"); } if (cookiedata != null) { sb.append("Cookie: " + cookiedata + "\r\n"); cookiedata = null; } requestHeaders = sb.toString(); } protected PrintWriter getPrintWriter () throws IOException { if (useSSL) { SSLSocket ss = (SSLSocket)sock; return new PrintWriter(ss.getOutputStream()); } else { Socket s = (Socket)sock; return new PrintWriter(s.getOutputStream()); } } protected InputStream getTextReader() throws IOException { if (useSSL) { SSLSocket ss = (SSLSocket)sock; return ( ss.getInputStream() ); } else { Socket s = (Socket)sock; return ( s.getInputStream() ); } } protected BufferedInputStream getBinaryReader() throws IOException { if (useSSL) { SSLSocket ss = (SSLSocket)sock; return new BufferedInputStream( ss.getInputStream() ); } else { Socket s = (Socket)sock; return new BufferedInputStream( s.getInputStream() ); } } private Object getSocket(String host, int port) throws UnknownHostException, IOException { if (useSSL) { if (proxyHostName == null) { SSLSocketFactory factory = getFactory(); SSLSocket sslSock = (SSLSocket)factory.createSocket(host, port); return (Object)sslSock; } else { Socket tunnel = new Socket(proxyHostName, proxyPort); OutputStream out = tunnel.getOutputStream(); String connect = "CONNECT " + host + ":" + port + " HTTP/1.0\r\n\r\n"; byte buf[]; try { buf = connect.getBytes("ASCII7"); } catch (UnsupportedEncodingException ignored) { // This better never freaking happen. buf = connect.getBytes(); } out.write(buf); out.flush(); byte reply[] = new byte[200]; int replyLen = 0; int newlinesSeen = 0; boolean headerDone = false; // Done on first newline InputStream in = tunnel.getInputStream(); boolean error = false; while (newlinesSeen < 2) { int i = in.read(); if (i < 0) { throw new IOException("Unexpected EOF from proxy"); } if (i == '\n') { headerDone = true; ++newlinesSeen; } else if (i != '\r') { newlinesSeen = 0; if (!headerDone && replyLen < reply.length) { reply[replyLen++] = (byte) i; } } } // Converting the byte array to a string is slightly wasteful // in the case where the connection was successful, but it's // insignificant compared to the network overhead. String replyStr; try { replyStr = new String(reply, 0, replyLen, "ASCII7"); } catch (UnsupportedEncodingException ignored) { replyStr = new String(reply, 0, replyLen); } // We asked for HTTP/1.0, so we should get that back if (!replyStr.startsWith("HTTP/1.0 200")) { throw new IOException("Unable to tunnel through " + proxyHostName + ":" + proxyPort + ". Proxy returns \"" + replyStr + "\""); } SSLSocketFactory factory = getFactory(); SSLSocket sslSock = (SSLSocket)factory.createSocket(tunnel, host, port, true); return (Object)sslSock; } } else { if (proxyHostName != null) { //System.err.println("Connecting thru proxy " + proxyHostName); Socket sock = new Socket(proxyHostName, proxyPort); return (Object)sock; } else { Socket sock = new Socket(host, port); return (Object)sock; } } } /** * It totally sucks that we have to do this, but since * the input stream implementation of sockets doesn't allow * us to put characters back, we have to read one char at a time, at least * until we get all the HTTP headers. */ protected String getLine (InputStream in) throws IOException { StringBuffer linebuf = new StringBuffer(80); int nextChar; boolean expectNewline = false; while ((nextChar = in.read()) != -1) { if (nextChar == 0x0a) { break; } if (expectNewline) { error(this, "Expected newline after carriage return. Bad webserver! Bad!" + linebuf.toString()); break; } if (nextChar == 0x0d) { expectNewline = true; continue; } linebuf.append((char)nextChar); } return linebuf.toString(); } /** * This method should probably be private. Don't call it. */ public void run() { Thread.currentThread().setName ("HTTPConnection:" + url.toString()); //debug(HTTPConnection.class, "thread starts"); doWork(); if ( !shutdown && followRedirect ) { while ( (getStatus() != null) && ((getStatus().indexOf("301 Found") != -1) || (getStatus().indexOf("302 Found") != -1)) ) { redirectCount++; // only loop through redirect certain number of times, if (redirectCount > maxRedirects) { info(this, "reached max redirects " + maxRedirects); break; } // get Location header which has the new URL Vector hv = getHeadersVector(); Enumeration e = hv.elements(); String header; int index; String u = null; try { while (e.hasMoreElements()) { header = (String)e.nextElement(); index = header.indexOf("Location: "); if (index != -1) { u = header.substring(index + 10); debug(this, "follow redirect to: " + u); break; } } if (u != null) { debug(this, "reinitialize to " + u); if (u.startsWith("https:")) { addSSLProvider(); u = "http:" + u.substring(6); useSSL = true; port = 443; } url = (new URL(u)); hostName = url.getHost(); urlFile = url.getFile(); if (url.getPort() > 0) { port = url.getPort(); } else if (url.getProtocol().equalsIgnoreCase("https")) { useSSL = true; port = 443; } // reinitialize all response variables responseHeaders = new Vector(); responseBytes = 0; responseBody = null; doWork(); } else { // no more redirect break; } } catch (MalformedURLException ex) { // we're hosed..., no more redirect error(this, ex); break; } } } } protected void openConnectionObject(String hostName, int port) throws UnknownHostException, IOException { sock = getSocket(hostName, port); } protected void closeConnectionObject() throws IOException { try { if (sock != null) { if (useSSL) { SSLSocket ss = (SSLSocket)sock; ss.close(); } else { Socket s = (Socket)sock; s.close(); } } } finally { sock = null; } } protected void doWork() { InputStream in = null; BufferedInputStream bin = null; PrintWriter out = null; ByteArrayOutputStream bout = null; int readlen = 0; shutdown = false; buildHeaders(); while (true) { try { //System.out.println("1"); openConnectionObject(hostName, port); if (shutdown) { break; } //System.out.println("2"); out = getPrintWriter(); in = getTextReader(); if (shutdown) { break; } //System.out.println("4"); if ((proxyHostName != null) && !useSSL) { out.print(method + " " + url.toString() + " " + HTTP_VERSION); } else { out.print(method + " " + urlFile + " " + HTTP_VERSION); } out.print("\r\n"); out.print(requestHeaders); //debug(HTTPConnection.class, "finished writing headers:\n" + requestHeaders); if (requestBody != null) { out.print("Content-length: " + requestBody.length()); out.print("\r\n"); out.print("\r\n"); out.print(requestBody); } else { out.print("\r\n"); } out.flush(); if (shutdown) { break; } //System.out.println("5"); // get the headers and status boolean firstline = true; responseHeadersRaw = ""; while (!shutdown) { String line = getLine(in); if (line == null || line.equals("")) { break; } if (firstline == true) { responseStatus = line; firstline = false; } else { responseHeadersRaw += line + "\r\n"; responseHeaders.add(line); } //System.err.println("Read a line: " + line); responseBytes += line.length(); } //System.out.println("6"); //debug(HTTPConnection.class, "status: " + responseStatus); //debug(HTTPConnection.class, "finished reading headers:\n" + responseHeadersRaw); /* if followRedirect and response is 302 or 301, * then disgard the response body from this run * however read the response body if the number of * redirects reach maxRedirects * 2002/02/11, jinglei */ if ( (getStatus() != null) && ((getStatus().indexOf("301 Found") != -1) || (getStatus().indexOf("302 Found") != -1)) && followRedirect && (redirectCount <= maxRedirects) ) { return; } // fixme: use content length, if supplied. if (outputStream == null) { bout = new ByteArrayOutputStream(); outputStream = bout; } // OK now read the content bin = getBinaryReader(); databuf = new byte[BUFSIZE]; while (true) { int n = bin.read(databuf, 0, BUFSIZE); if (n < 0) { break; } //System.err.println("****Read bytes: " + n + "--\n" + //new String(databuf, 0, n)); outputStream.write(databuf, 0, n); } if (bout != null) { responseBodyBinary = bout.toByteArray(); bout.close(); } //debug(HTTPConnection.class, "finished reading body"); // it's up to the caller to close outputStream if they supplied it. } catch (Exception e) { error(this, e); caughtException = e; } finally { try { closeConnectionObject(); if (out != null) { out.close(); } if (in != null) { in.close(); } if (bin != null) { bin.close(); } if (bout != null) { bout.close(); } } catch (IOException e) { // totally hosed error(this, e); } } break; } } static private boolean sslProviderAdded = false; static private synchronized void addSSLProvider() { if (sslProviderAdded != true) { Security.addProvider(new com.sun.net.ssl.internal.ssl.Provider()); Properties sysProps = System.getProperties(); sysProps.put("java.protocol.handler.pkgs","com.sun.net.ssl.internal.www.protocol"); System.setProperties(sysProps); sslProviderAdded = true; } } static public void main(String args[]) throws Exception { PrintWriter out = null; String command = args[1]; try { if (args[0].equals("-")) { out = new PrintWriter(System.out); } else { out = new PrintWriter(new FileWriter(args[0])); } HTTPConnection hc = null; if (command.equals("get")) { hc = new HTTPConnection(args[2]); hc.setMethod("GET"); hc.setFollowRedirects(true); hc.setMaxRedirects(1); hc.doConnect(); String h = hc.getHeaders(); Vector hv = hc.getHeadersVector(); Enumeration e = hv.elements(); out.println("Status: " + hc.getStatus()); out.println("Headers in Vector:"); while (e.hasMoreElements()) { out.println((String)e.nextElement()); } String body = hc.getBody(); out.println("Headers:"); out.println(h); out.println("Body:"); out.println(body); } if (command.equals("getfile")) { hc = new HTTPConnection(args[2]); String filename = args[3]; hc.setMethod("GET"); hc.setFollowRedirects(true); hc.doConnect(); String h = hc.getHeaders(); byte[] body = hc.getBodyBytes(); out.println("Status: " + hc.getStatus()); Vector hv = hc.getHeadersVector(); Enumeration e = hv.elements(); out.println("Headers in Vector:"); while (e.hasMoreElements()) { out.println((String)e.nextElement()); } out.println("Headers:"); out.println(h); FileOutputStream fout = new FileOutputStream(filename); fout.write(body, 0, body.length); fout.close(); out.println("Wrote body to file " + filename + " " + body.length + " bytes"); } if (command.equals("post")) { hc = new HTTPConnection(args[2]); hc.setMethod("POST"); hc.setFollowRedirects(true); hc.addCookie("qbn.login_test=testme"); hc.addHeader("Content-type", "application/x-www-form-urlencoded"); hc.setProxy(args[3], 80); hc.setBody(args[4]); hc.doConnect(); String h = hc.getHeaders(); String body = hc.getBody(); out.println("Headers:"); out.println(h); out.println("Body:"); out.println(body); } } catch (Exception e) { System.err.println("Exception"); e.printStackTrace(); } finally { if (null != out) { out.flush(); // BOTH OF THESE STEPS ARE VERY IMPORTANT! out.close(); } } } }