/* this belongs to the WebRaveler package */ package webraveler.http; import java.io.*; import java.net.SocketTimeoutException; import java.util.*; import java.util.zip.GZIPInputStream; import webraveler.html.HTMLPage2; import webraveler.io.ChunkedInputStream; /** * HTTP response * * @author The Web Raveler Development Squad * @version 1.0 */ public class HTTPResponse extends HTTPMessage implements Serializable { /* variables */ public String protocolVersion; public int status; public String reasonPhrase; public byte entityBytes[]; public HTMLPage2 htmlPage; /* constructors */ /** * Useless default constructor. */ public HTTPResponse() { super(); } /** * Construct an HTTPResponse directly from an InputStream. * Note that this is the preferred way of creating response * objects. * * @param in * @throws IOException */ public HTTPResponse(InputStream in) throws IOException { try{ String line; StringBuffer response = new StringBuffer(); // read in lines, breaking when we get to the empty line while( (line = HTTPMessage.readLine(in)) != null ) { response.append(line+"\r\n"); if (line.equals("")) break; }//while //parse the string we just got, populating the headers //as welll as the status code, etc. from the first line parseHeaders(parseFirstLine(response.toString())); /* set the entity bytes for the entity body */ setEntityBytes(in); /* if it's an html message, convert the entityBytes into a text string */ String contentType = getHeader("content-type"); if ( contentType != null && contentType.toLowerCase().startsWith("text/html") ) { String line2; StringBuffer text = new StringBuffer(); InputStream input = new ByteArrayInputStream(entityBytes); InputStreamReader inputReader = new InputStreamReader(input); BufferedReader inputBuffReader = new BufferedReader(inputReader); try { while ( (line2 = inputBuffReader.readLine()) != null ) { text.append(line2 + "\n"); } } catch(IOException ioe) {ioe.printStackTrace();} htmlPage = new HTMLPage2(text.toString()); }//if } catch (SocketTimeoutException ste) { throw new IOException("HTTPResponse: Server timed out."); } //catch }//HTTPResponse(InputStream) /** * Constructs an HTTPResponse given a protocolVersion, a status, a reasonPhrase, * a Hashtable of headers, and an entity byte array... Useful when we have all * the ingrediants and want to modify something to create a new message * * @param protocolVersion a String containing the protocol and version of the message * @param status an integer containing the status code * @param reasonPhrase a string representing the reason phrase * @param entityBytes a byte[] containing the entity, if any (if not, it should be null) * @param localHeaders a hashtable containing header fields/values as key/value pairs */ public HTTPResponse(String protocolVersion, int status, String reasonPhrase, byte[] entityBytes, Hashtable localHeaders) { this.protocolVersion = protocolVersion; this.status = status; this.reasonPhrase = reasonPhrase; this.entityBytes = entityBytes; this.headers = localHeaders; } /** * Create a new HTTPResponse with an HTMLPage2 payload. */ public HTTPResponse(String protocolVersion, int status, String reasonPhrase, HTMLPage2 htmlPage, Hashtable localHeaders) { this.protocolVersion = protocolVersion; this.status = status; this.reasonPhrase = reasonPhrase; this.htmlPage = htmlPage; this.headers = localHeaders; entityBytes = (htmlPage.toString()).getBytes(); } /** * Contructs an HTTPResponse with a string containing the unparsed response * and an InputStream containing the entity streams * * @param response a string containing the unparsed response * @param entityStream an input stream containing the entity stream */ // public HTTPResponse(String response, InputStream entityStream) { // // // //System.out.println("HTTPResponse: Parsing incoming response."); // /* Parse the first line */ // response = parseFirstLine(response); // // /* Parse the headers */ // parseHeaders(response); // // setHeader("connection","close"); // deleteHeader("keep-alive"); // deleteHeader("proxy-connection"); // addHeader("via","1.0 WR (WebRaveler2 v1.0)"); //add our via header // // /* set the entity bytes for the entity body */ // setEntityBytes(entityStream); // // /* if it's an html message, convert the entityBytes into a text string */ // String contentType = getHeader("content-type"); // if ( entityText == null && contentType != null && contentType.toLowerCase().startsWith("text/html") ) { // String line; // entityText = new String(""); // InputStream input = new ByteArrayInputStream(entityBytes); // InputStreamReader inputReader = new InputStreamReader(input); // BufferedReader inputBuffReader = new BufferedReader(inputReader); // try { // while ( (line = inputBuffReader.readLine()) != null ) { // entityText += line + "\n"; // } // } catch(IOException ioe) {ioe.printStackTrace();} // htmlPage = new HTMLPage2(entityText); // } // // } //HTTPResponse /* methods */ /** * Is this response a redirect (i.e., one with status equal to 30X)? */ public boolean isRedirect() { return (status == 302 || status == 301 || status == 307 || status == 303 || status == 305); }//isRedirect /** * Return the HTML page associated with this response. * * @return the HTMLPage2 associated with this response. */ public HTMLPage2 getHtmlPage(){ if ( htmlPage == null ) { System.out.println("WARNING: Returning null HTMLPage!"); } return htmlPage; }//HTMLPage2 getHtmlPage() /** * Set this response's entity to the given HTML page. * * @param htmlPage the HTMLPage2 this response will carry */ public void setHtmlPage(HTMLPage2 htmlPage) { this.htmlPage = htmlPage; deleteHeader("content-type"); setHeader("content-type","text/html"); correctContentLength(); } //void setHtmlPage(HTMLPage2) /** * Make the content-length header match the length * of the byte array in the entityBytes field. * */ public void correctContentLength() { //make sure the html page and entity bytes match String contentType = getHeader("content-type"); if( contentType != null && contentType.startsWith("text/html") ) { entityBytes = (htmlPage.toString()).getBytes(); } //if there's an entity, make sure the content-length header is correct if ( entityBytes != null ) { deleteHeader("content-length"); setHeader("content-length",new String(""+entityBytes.length)); } }//void correctContentLength() /** * Parses the first line and sets the status, protocol/version, and reason-phrase (if one exists) * * @param requestString a string containing the request * @return a string containing the request without the parsed first line */ public String parseFirstLine(String responseString) { int sep1,sep2,sep3; String statusString = new String(); sep1 = responseString.indexOf(" "); /* find the first space */ protocolVersion = responseString.substring(0,sep1).trim(); /* the method is the first token */ sep2 = responseString.indexOf(" ",sep1+1); /* find the second space */ sep3 = responseString.indexOf("\r"); /* find the carriage return */ /* if there's no reason-phrase, we just go to the end of the line, and parse the status int */ if (sep2 > sep3) { statusString = responseString.substring(sep1+1,sep3).trim(); reasonPhrase = ""; /* might screw up the reformation of the response message later on, but * the one extra whitespace character shouldn't matter. hopefully */ } else { /* there was a reason phrase */ statusString = responseString.substring(sep1+1,sep2).trim(); /* the URI comes next */ reasonPhrase = responseString.substring(sep2+1,sep3); /* next comes the protocol/version */ } status = Integer.parseInt(statusString); /* Omit the first line from the response string */ responseString = responseString.substring(sep3+2,responseString.length()); return responseString; } // parseFirstLine /** * Parses the headers * * @param headerString a string containing the headers */ public void parseHeaders(String headerString) { int colonIndex; int crlfIndex; String headerName = new String(); String headerField = new String(); while (headerString.length() != 0) { /* get the index of the colon*/ colonIndex = headerString.indexOf(":"); /* get the index of the crlf*/ crlfIndex = headerString.indexOf("\r"); if ( headerString.startsWith(" ") ) { /* it's a continuation line; just update the previous hash entry */ setHeader(headerName,headerField+" "+(headerString.substring(0,crlfIndex).trim().toLowerCase())); } else { /* this is a new header */ /* if there's no colon in the request, there are no more headers */ if (colonIndex == -1) break; headerName = headerString.substring(0,colonIndex).trim().toLowerCase(); headerField = headerString.substring(colonIndex+1,crlfIndex).trim(); /* Add the header to the hash table */ setHeader(headerName,headerField); } /* Omit this header from the response string */ headerString = headerString.substring(crlfIndex+2,headerString.length()); } // while } // parseHeaders /** * Takes the entityStream and puts in a byte array * * entity an input stream containing the entity body */ public void setEntityBytes(InputStream entity) { /* check for chunked encoding. if found, read in using the ChunkedInputStream class Daren wrote, * and put it in entityBytes as usual. otherwise, we just do simple reading in until the end of * the input */ if ( containsHeader("content-encoding") ) { String mimeType = (getHeader("content-type")).trim(); String encoding = getHeader("content-encoding"); if ( encoding.trim().equalsIgnoreCase("gzip") && mimeType != null && mimeType.equalsIgnoreCase("text/html") ) { decodeGzipHTML(entity); return; }//if } //if ByteArrayOutputStream entityByteStream = new ByteArrayOutputStream(); int input; String transferEncoding = getHeader("transfer-encoding"); if ( transferEncoding != null && transferEncoding.equals("chunked") ) { headers.remove("transfer-encoding"); ChunkedInputStream chunked_in = new ChunkedInputStream(entity); try{ /* method read() in ChunkedInputStream returns each byte, dechunked, in succession, * from the original InputStream */ while ( (input = chunked_in.read()) != -1 ) { entityByteStream.write(input); } entityBytes = entityByteStream.toByteArray(); }catch (IOException ioe) { ioe.printStackTrace(); entityBytes = null; } } else { /* it's not a chunked stream, so read it as usual */ /* this should be restricted to those responses known to return * entity values... otherwise, we'll halt on the attempt to read from * the input stream */ //System.out.println("HTTPResponse: Getting entity byte array."); try{ while( (input = entity.read()) != -1 ) { entityByteStream.write(input); /* write the input byte to the stream */ } entityBytes = entityByteStream.toByteArray(); //System.out.print("HTTPResponse: entityBytes = '"+entityBytes+"'\n"); }catch (Exception e) { //e.printStackTrace(); entityBytes = null; } } // else } // setEntityBytes /** * Writes the HTTP request to the given output stream * * @param out an OutputStream to write the request to */ public synchronized void write(OutputStream out) { try { //print(0); /* Write the first line to the output stream */ String firstLine = protocolVersion + " " + status + " " + reasonPhrase + "\r\n"; out.write(firstLine.getBytes()); /* Write the headers to the output stream */ String headerString = new String(); if (headers != null) { for (Enumeration e = headers.keys() ; e.hasMoreElements() ;) { String element = (String) e.nextElement(); String field = getHeader(element); headerString += element + ": " + field + "\r\n"; } // for } out.write(headerString.getBytes()); out.write("\r\n".getBytes()); if (entityBytes != null) { out.write(entityBytes); } out.flush(); } catch (IOException ioe) { //ioe.printStackTrace(); } } // write /** * If a text/html entity is gzipped (i.e., by RabbIT), decode it into htmlPage. * * @param in an InputStream that contains gzipped data to be read */ public void decodeGzipHTML(InputStream in) { try{ System.out.println("HTTPResponse: handling gzipped html"); GZIPInputStream gzipInput = new GZIPInputStream(in); byte buff[] = new byte[128]; ByteArrayOutputStream byteOut = new ByteArrayOutputStream(); int len = 0; while ( (len = gzipInput.read(buff,0,128)) > 0 ) { byteOut.write(buff,0,len); /* write the new data to the buffer */ }//while entityBytes = byteOut.toByteArray(); htmlPage = new HTMLPage2(new String(entityBytes,0,entityBytes.length)); }catch (IOException ioe) { ioe.printStackTrace(); System.out.println("HTTPResponse: decoding failed."); } setHeader("content-length",""+entityBytes.length); deleteHeader("content-encoding"); }//decodeGzipHTML /** * Print the contents of the response to stdout, with the ProxyServer id integer * * @param count an int containing the id of the calling ProxyServer instance */ public synchronized void print(int count){ try{ System.out.print("HTTPResponse: Response#"+count+": \n'"); String firstLine = protocolVersion + " " + status + " " + reasonPhrase + "\r\n"; System.out.write(firstLine.getBytes()); /* Write the headers to the output stream */ String headerString = new String(); for (Enumeration e = headers.keys() ; e.hasMoreElements() ;) { String element = (String) e.nextElement(); //String field = getHeader(element); String field = (String) headers.get(element); headerString += element + ": " + field + "\r\n"; } // for System.out.write(headerString.getBytes()); System.out.write("\r\n".getBytes()); //System.out.write(entityBytes); System.out.println("'\n"); }catch(IOException ioe) { ioe.printStackTrace(); } } } // class HTTPResponse