/*********************************************************************** * * $CVSHeader$ * * This file is part of WebScarab, an Open Web Application Security * Project utility. For details, please see http://www.owasp.org/ * * Copyright (c) 2002 - 2004 Rogan Dawes * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * * Getting Source * ============== * * Source for this application is maintained at Sourceforge.net, a * repository for free software projects. * * For details, please see http://www.sourceforge.net/projects/owasp * */ /* * Message.java * * Created on May 12, 2003, 11:10 PM */ package org.owasp.webscarab.model; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.UnsupportedEncodingException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.logging.Logger; import java.util.zip.GZIPInputStream; import java.util.zip.GZIPOutputStream; import org.owasp.webscarab.httpclient.ChunkedInputStream; import org.owasp.webscarab.httpclient.ChunkedOutputStream; import org.owasp.webscarab.httpclient.FixedLengthInputStream; /** Message is a class that is used to represent the bulk of an HTTP message, namely * the headers, and (possibly null) body. Messages should not be instantiated * directly, but should rather be created by a derived class, namely Request or * Response. * @author rdawes */ public class Message { private ArrayList<NamedValue> _headers = null; private NamedValue[] NO_HEADERS = new NamedValue[0]; private static final byte[] NO_CONTENT = new byte[0]; InputStream _contentStream = null; ByteArrayOutputStream _content = null; boolean _chunked = false; boolean _gzipped = false; int _length = -1; protected Logger _logger = Logger.getLogger(this.getClass().getName()); /** Message is a class that is used to represent the bulk of an HTTP message, namely * the headers, and (possibly null) body. Messages should not be instantiated * directly, but should rather be created by a derived class, namely Request or * Response. */ public Message() { } /** * Instructs the class to read the headers from the InputStream, and assign the * InputStream as the contentStream, from which the body of the message can be * read. * @throws IOException Propagates any IOExceptions thrown by the InputStream read methods * @param is the InputStream to read the Message headers and body from */ public void read(InputStream is) throws IOException { _headers = null; String previous = null; String line = null; do { line=readLine(is); _logger.finer("Header: " + line); if (line.startsWith(" ")) { if (previous == null) { _logger.severe("Got a continuation header but had no previous header line"); } else { previous = previous.trim() + " " + line.trim(); } } else { if (previous != null) { String[] pair = previous.split(":", 2); if (pair.length == 2) { addHeader(new NamedValue(pair[0], pair[1].trim())); } else { _logger.warning("Error parsing header: '" + previous + "'"); } } previous = line; } } while (!line.equals("")); _contentStream = is; if (_chunked) { _contentStream = new ChunkedInputStream(_contentStream); } else if (_length > -1) { _contentStream = new FixedLengthInputStream(_contentStream, _length); } } /** * Writes the Message headers and content to the supplied OutputStream * @param os The OutputStream to write the Message headers and content to * @throws IOException any IOException thrown by the supplied OutputStream, or any IOException thrown by the InputStream from which this Message was originally read (if any) */ public void write(OutputStream os) throws IOException { write(os, "\r\n"); } /** * Writes the Message headers and content to the supplied OutputStream * @param os The OutputStream to write the Message headers and content to * @throws IOException any IOException thrown by the supplied OutputStream, or any IOException thrown by the InputStream from which this Message was originally read (if any) * @param crlf the line ending to use for the headers */ public void write(OutputStream os, String crlf) throws IOException { if (_headers != null) { for (int i=0; i<_headers.size(); i++) { NamedValue nv = _headers.get(i); os.write(new String(nv.getName() + ": " + nv.getValue() + crlf).getBytes()); _logger.finest("Header: " + nv); } } os.write(crlf.getBytes()); _logger.finer("wrote headers"); if (_chunked) { os = new ChunkedOutputStream(os); } if (_contentStream != null) { _logger.finer("Flushing contentStream"); flushContentStream(os); _logger.finer("Done flushing contentStream"); } else if (_content != null && _content.size() > 0) { _logger.finer("Writing content bytes"); os.write(_content.toByteArray()); _logger.finest("Content: \n" + new String(_content.toByteArray())); _logger.finer("Done writing content bytes"); } if (_chunked) { ((ChunkedOutputStream) os).writeTrailer(); } } /** * Instructs the class to read the headers and content from the supplied StringBuffer * N.B. The "Content-length" header is updated automatically to reflect the true size * of the content, if one exists * @param buffer The StringBuffer to parse the headers and content from. This buffer is "consumed" i.e. characters are removed from the buffer as the Message is parsed. * @throws ParseException if there is an error parsing the Message structure */ public void parse(StringBuffer buffer) throws ParseException { _headers = null; String previous = null; String line = null; do { line=getLine(buffer); if (line.startsWith(" ")) { if (previous == null) { _logger.severe("Got a continuation header but had no previous header line"); } else { previous = previous.trim() + " " + line.trim(); } } else { if (previous != null) { String[] pair = previous.split(":", 2); if (pair.length == 2) { addHeader(new NamedValue(pair[0], pair[1].trim())); } else { _logger.warning("Error parsing header: '" + previous + "'"); } } previous = line; } } while (!line.equals("")); _content = new ByteArrayOutputStream(); try { _content.write(buffer.toString().getBytes()); } catch (IOException ioe) {} // can't fail String cl = getHeader("Content-length"); if (cl != null) { setHeader(new NamedValue("Content-length", Integer.toString(_content.size()))); } } /** Returns a String representation of the message, *including* the message body. * @return The string representation of the message */ public String toString() { return toString("\r\n"); } /** Returns a String representation of the message, *including* the message body. * Lines of the header are separated by the supplied "CarriageReturnLineFeed" string. * @return a String representation of the Message. * @param crlf The required line separator string */ public String toString(String crlf) { StringBuffer buff = new StringBuffer(); if (_headers != null) { for (int i=0; i<_headers.size(); i++) { NamedValue nv = _headers.get(i); if (nv.getName().equalsIgnoreCase("Transfer-Encoding") && nv.getValue().indexOf("chunked")>-1) { buff.append("X-" + nv.getName() + ": " + nv.getValue() + crlf); } else if (nv.getName().equalsIgnoreCase("Content-Encoding") && nv.getValue().indexOf("gzip")>-1) { buff.append("X-" + nv.getName() + ": " + nv.getValue() + crlf); } else { buff.append(nv.getName() + ": " + nv.getValue() + crlf); } } } byte[] content = getContent(); if (_chunked && content != null) { buff.append("Content-length: " + Integer.toString(content.length) + crlf); } buff.append(crlf); if (content != null) { try { buff.append(new String(content, "UTF-8")); } catch (UnsupportedEncodingException uee) {}; // must support UTF-8 } return buff.toString(); } private void updateFlagsForHeader(NamedValue header) { if (header.getName().equalsIgnoreCase("Transfer-Encoding")) { if (header.getValue().indexOf("chunked")>-1) { _chunked = true; } else { _chunked = false; } } else if (header.getName().equalsIgnoreCase("Content-Encoding")) { if (header.getValue().indexOf("gzip")>-1) { _gzipped = true; } else { _gzipped = false; } } else if (header.getName().equalsIgnoreCase("Content-length")) { try { _length = Integer.parseInt(header.getValue().trim()); } catch (NumberFormatException nfe) { _logger.warning("Error parsing the content-length '" + header.getValue() + "' : " + nfe); } } } /** * sets the value of a header. This overwrites any previous values of headers with the same name. * @param name the name of the header (without a colon) * @param value the value of the header */ public void setHeader(String name, String value) { setHeader(new NamedValue(name, value.trim())); } public void setHeader(NamedValue header) { updateFlagsForHeader(header); if (_headers == null) { _headers = new ArrayList<NamedValue>(1); } else { for (int i=0; i<_headers.size(); i++) { NamedValue nv = _headers.get(i); if (nv.getName().equalsIgnoreCase(header.getName())) { _headers.set(i,header); return; } } } _headers.add(header); } /** * Adds a header with the specified name and value. This preserves any previous * headers with the same name, and adds another header with the same name. * @param name the name of the header (without a colon) * @param value the value of the header */ public void addHeader(String name, String value) { addHeader(new NamedValue(name, value.trim())); } public void addHeader(NamedValue header) { updateFlagsForHeader(header); if (_headers == null) { _headers = new ArrayList<NamedValue>(1); } _headers.add(header); } /** * Removes a header * @param name the name of the header (without a colon) * @return the value of the header that was deleted */ public String deleteHeader(String name) { if (_headers == null) { return null; } for (int i=0; i<_headers.size(); i++) { NamedValue nv = _headers.get(i); if (nv.getName().equalsIgnoreCase(name)) { _headers.remove(i); updateFlagsForHeader(new NamedValue(name, "")); return nv.getValue(); } } return null; } /** * Returns an array of header names * @return an array of the header names */ public String[] getHeaderNames() { if (_headers == null || _headers.size() == 0) { return new String[0]; } String[] names = new String[_headers.size()]; for (int i=0; i<_headers.size(); i++) { NamedValue nv = _headers.get(i); names[i] = nv.getName(); } return names; } /** * Returns the value of the requested header * @param name the name of the header (without a colon) * @return the value of the header in question (null if the header did not exist) */ public String getHeader(String name) { if (_headers == null) { return null; } for (int i=0; i<_headers.size(); i++) { NamedValue nv = _headers.get(i); if (nv.getName().equalsIgnoreCase(name)) { return nv.getValue(); } } return null; } /** * Returns all the values of the requested header, if there are multiple items * @param name the name of the header (without a colon) * @return the values of the header in question (null if the header did not exist) */ public String[] getHeaders(String name) { if (_headers == null) return null; ArrayList<String> values = new ArrayList<String>(); for (int i=0; i<_headers.size(); i++) { NamedValue nv = _headers.get(i); if (nv.getName().equalsIgnoreCase(name)) { values.add(nv.getValue()); } } if (values.size() == 0) return null; return values.toArray(new String[0]); } /** * returns the header names and their values * @return an array of NamedValue's representing the names and values * of the headers */ public NamedValue[] getHeaders() { if (_headers == null || _headers.size() == 0) { return new NamedValue[0]; } return _headers.toArray(NO_HEADERS); } /** * sets the headers * @param table a two dimensional array of Strings, where table[i][0] is the header name and * table[i][1] is the header value */ public void setHeaders(NamedValue[] headers) { if (_headers == null) { _headers = new ArrayList<NamedValue>(); } else { _headers.clear(); } for (int i=0; i<headers.length; i++) { addHeader(headers[i]); } } /** * a protected method to read a line up to and including the CR or CRLF. * * We don't use a BufferedInputStream so that we don't read further than we should * i.e. into the message body, or next message! * @param is The InputStream to read the line from * @throws IOException if an IOException occurs while reading from the supplied InputStream * @return the line that was read, WITHOUT the CR or CRLF */ protected String readLine(InputStream is) throws IOException { if (is == null) { NullPointerException npe = new NullPointerException("InputStream may not be null!"); npe.printStackTrace(); throw npe; } StringBuffer line = new StringBuffer(); int i; char c=0x00; i = is.read(); if (i == -1) return null; while (i > -1 && i != 10 && i != 13) { // Convert the int to a char c = (char)(i & 0xFF); line = line.append(c); i = is.read(); } if (i == 13) { // 10 is unix LF, but DOS does 13+10, so read the 10 if we got 13 i = is.read(); } _logger.finest(line.toString()); return line.toString(); } /** * a protected method to read a line up to and including the CR or CRLF. * Removes the line from the supplied StringBuffer. * @param buffer the StringBuffer to read the line from * @return the line that was read, WITHOUT the CR or CRLF */ protected String getLine(StringBuffer buffer) { int lf = buffer.indexOf("\n"); if (lf > -1) { int cr = buffer.indexOf("\r"); if (cr == -1 || cr > lf) { cr = lf; } String line = buffer.substring(0,cr); buffer.delete(0, lf+1); _logger.finest("line is '" + line + "'"); return line; } else if (buffer.length() > 0) { String line = buffer.toString(); buffer.setLength(0); _logger.finest("line is '" + line + "'"); return line; } else { return ""; } } /** getContent returns the message body that accompanied the request. * if the message was read from an InputStream, it reads the content from * the InputStream and returns a copy of it. * If the message body was chunked, or gzipped (according to the headers) * it returns the unchunked and unzipped content. * * @return Returns a byte array containing the message body */ public byte[] getContent() { try { flushContentStream(null); } catch (IOException ioe) { _logger.info("IOException flushing the contentStream: " + ioe); } if (_content != null && _gzipped) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); GZIPInputStream gzis = new GZIPInputStream(new ByteArrayInputStream(_content.toByteArray())); byte[] buff = new byte[1024]; int got; while ((got = gzis.read(buff))>-1) { baos.write(buff, 0, got); } return baos.toByteArray(); } catch (IOException ioe) { _logger.info("IOException unzipping content : " + ioe); return NO_CONTENT; } } if (_content != null) { return _content.toByteArray(); } else { return NO_CONTENT; } } /** * reads all content from the content stream if one exists. Bytes read are stored internally, and returned via getContent() */ public void flushContentStream() { try { flushContentStream(null); } catch (IOException ioe) { _logger.info("Exception flushing the contentStream " + ioe); } } /** reads all the bytes in the contentStream into a local ByteArrayOutputStream * where they can be retrieved by the getContent() methods. * Optionally writes the bytes read to the supplied outputstream * This method immediately throws any IOExceptions that occur while reading * the contentStream, but defers any exceptions that occur writing to the * supplied outputStream until the entire contentStream has been read and * saved. */ private void flushContentStream(OutputStream os) throws IOException { IOException ioe = null; if (_contentStream == null) return; if (_content == null) _content = new ByteArrayOutputStream(); byte[] buf = new byte[4096]; _logger.finest("Reading initial bytes from contentStream " + _contentStream); int got = _contentStream.read(buf); _logger.finest("Got " + got + " bytes"); while (got > 0) { _content.write(buf,0, got); if (os != null) { try { os.write(buf,0,got); } catch (IOException e) { _logger.info("IOException ioe writing to output stream : " + e); _logger.info("Had seen " + (_content.size()-got) + " bytes, was writing " + got); ioe = e; os = null; } } got = _contentStream.read(buf); _logger.finest("Got " + got + " bytes"); } _contentStream = null; if (ioe != null) throw ioe; } /** * sets the message to not have a body. This is typical for a CONNECT request or * response, which should not read any body. */ public void setNoBody() { _content = null; _contentStream = null; } /** * Sets the content of the message body. If the message headers indicate that the * content is gzipped, the content is automatically compressed * @param bytes a byte array containing the message body */ public void setContent(byte[] bytes) { // discard whatever is pending in the content stream try { flushContentStream(null); } catch (IOException ioe) { _logger.info("IOException flushing the contentStream " + ioe); } if (_gzipped) { try { _content = new ByteArrayOutputStream(); GZIPOutputStream gzos = new GZIPOutputStream(_content); gzos.write(bytes); gzos.close(); } catch (IOException ioe) { _logger.info("IOException gzipping content : " + ioe); } } else { _content = new ByteArrayOutputStream(); try { if (bytes != null) _content.write(bytes); } catch (IOException ioe) {} // can't fail } String cl = getHeader("Content-length"); if (cl != null) { setHeader(new NamedValue("Content-length", Integer.toString(_content.size()))); } } public boolean equals(Object obj) { if (! (obj instanceof Message)) return false; Message mess = (Message) obj; NamedValue[] myHeaders = getHeaders(); NamedValue[] thoseHeaders = mess.getHeaders(); if (myHeaders.length != thoseHeaders.length) return false; for (int i=0; i<myHeaders.length; i++) { if (!myHeaders[i].getName().equalsIgnoreCase(thoseHeaders[i].getName())) return false; if (!myHeaders[i].getValue().equals(thoseHeaders[i].getValue())) return false; } byte[] myContent = getContent(); byte[] thatContent = mess.getContent(); return Arrays.equals(myContent, thatContent); } }