Index: src/java/org/apache/commons/httpclient/ChunkedInputStream.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ChunkedInputStream.java,v retrieving revision 1.22 diff -u -r1.22 ChunkedInputStream.java --- src/java/org/apache/commons/httpclient/ChunkedInputStream.java 18 Apr 2004 23:51:34 -0000 1.22 +++ src/java/org/apache/commons/httpclient/ChunkedInputStream.java 18 Jul 2004 19:13:21 -0000 @@ -301,7 +301,7 @@ private void parseTrailerHeaders() throws IOException { Header[] footers = null; try { - footers = HttpParser.parseHeaders(in, + footers = method.getParams().getHttpParser().parseHeaders(in, method.getParams().getHttpElementCharset()); } catch(HttpException e) { LOG.error("Error parsing trailer headers", e); Index: src/java/org/apache/commons/httpclient/HttpConnection.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v retrieving revision 1.96 diff -u -r1.96 HttpConnection.java --- src/java/org/apache/commons/httpclient/HttpConnection.java 5 Jul 2004 22:46:58 -0000 1.96 +++ src/java/org/apache/commons/httpclient/HttpConnection.java 18 Jul 2004 19:13:25 -0000 @@ -1024,7 +1024,7 @@ LOG.trace("enter HttpConnection.readLine()"); assertOpen(); - return HttpParser.readLine(inputStream); + return getParams().getHttpParser().readLine(inputStream); } /** @@ -1044,7 +1044,7 @@ LOG.trace("enter HttpConnection.readLine()"); assertOpen(); - return HttpParser.readLine(inputStream, charset); + return getParams().getHttpParser().readLine(inputStream, charset); } /** Index: src/java/org/apache/commons/httpclient/HttpMethodBase.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java,v retrieving revision 1.210 diff -u -r1.210 HttpMethodBase.java --- src/java/org/apache/commons/httpclient/HttpMethodBase.java 5 Jul 2004 22:46:58 -0000 1.210 +++ src/java/org/apache/commons/httpclient/HttpMethodBase.java 18 Jul 2004 19:13:33 -0000 @@ -1746,7 +1746,7 @@ getResponseHeaderGroup().clear(); - Header[] headers = HttpParser.parseHeaders( + Header[] headers = getParams().getHttpParser().parseHeaders( conn.getResponseInputStream(), getParams().getHttpElementCharset()); if (Wire.HEADER_WIRE.enabled()) { for (int i = 0; i < headers.length; i++) { Index: src/java/org/apache/commons/httpclient/HttpParser.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpParser.java,v retrieving revision 1.12 diff -u -r1.12 HttpParser.java --- src/java/org/apache/commons/httpclient/HttpParser.java 13 May 2004 04:03:25 -0000 1.12 +++ src/java/org/apache/commons/httpclient/HttpParser.java 18 Jul 2004 19:13:33 -0000 @@ -31,12 +31,8 @@ import java.io.IOException; import java.io.InputStream; -import java.io.ByteArrayOutputStream; -import java.util.ArrayList; -import org.apache.commons.httpclient.util.EncodingUtil; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.apache.commons.httpclient.util.DefaultHttpParser; /** * A utility class for parsing http header values. @@ -45,17 +41,17 @@ * @author Oleg Kalnichevski * * @since 2.0beta1 + * @deprecated Use {@link org.apache.commons.httpclient.util.HttpParser} instead */ public class HttpParser { - /** Log object for this class. */ - private static final Log LOG = LogFactory.getLog(HttpParser.class); - /** * Constructor for HttpParser. */ private HttpParser() { } + private static final org.apache.commons.httpclient.util.HttpParser PARSER = DefaultHttpParser.INSTANCE; + /** * Return byte array from an (unchunked) input stream. * Stop reading when "\n" terminator encountered @@ -69,20 +65,7 @@ * @return a byte array from the stream */ public static byte[] readRawLine(InputStream inputStream) throws IOException { - LOG.trace("enter HttpParser.readRawLine()"); - - ByteArrayOutputStream buf = new ByteArrayOutputStream(); - int ch; - while ((ch = inputStream.read()) >= 0) { - buf.write(ch); - if (ch == '\n') { - break; - } - } - if (buf.size() == 0) { - return null; - } - return buf.toByteArray(); + return PARSER.readRawLine(inputStream); } /** @@ -100,24 +83,7 @@ * @since 3.0 */ public static String readLine(InputStream inputStream, String charset) throws IOException { - LOG.trace("enter HttpParser.readLine(InputStream, String)"); - byte[] rawdata = readRawLine(inputStream); - if (rawdata == null) { - return null; - } - int len = rawdata.length; - int offset = 0; - if (len > 0) { - if (rawdata[len - 1] == '\n') { - offset++; - if (len > 1) { - if (rawdata[len - 2] == '\r') { - offset++; - } - } - } - } - return EncodingUtil.getString(rawdata, 0, len - offset, charset); + return PARSER.readLine(inputStream, charset); } /** @@ -135,8 +101,7 @@ */ public static String readLine(InputStream inputStream) throws IOException { - LOG.trace("enter HttpParser.readLine(InputStream)"); - return readLine(inputStream, "US-ASCII"); + return PARSER.readLine(inputStream); } /** @@ -154,52 +119,7 @@ * @since 3.0 */ public static Header[] parseHeaders(InputStream is, String charset) throws IOException, HttpException { - LOG.trace("enter HeaderParser.parseHeaders(InputStream, String)"); - - ArrayList headers = new ArrayList(); - String name = null; - StringBuffer value = null; - for (; ;) { - String line = HttpParser.readLine(is, charset); - if ((line == null) || (line.length() < 1)) { - break; - } - - // Parse the header name and value - // Check for folded headers first - // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2 - // discussion on folded headers - if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t')) { - // we have continuation folded header - // so append value - if (value != null) { - value.append(' '); - value.append(line.trim()); - } - } else { - // make sure we save the previous name,value pair if present - if (name != null) { - headers.add(new Header(name, value.toString())); - } - - // Otherwise we should have normal HTTP header line - // Parse the header name and value - int colon = line.indexOf(":"); - if (colon < 0) { - throw new ProtocolException("Unable to parse header: " + line); - } - name = line.substring(0, colon).trim(); - value = new StringBuffer(line.substring(colon + 1).trim()); - } - - } - - // make sure we save the last name,value pair if present - if (name != null) { - headers.add(new Header(name, value.toString())); - } - - return (Header[]) headers.toArray(new Header[headers.size()]); + return PARSER.parseHeaders(is, charset); } /** @@ -216,7 +136,6 @@ * @deprecated use #parseHeaders(InputStream, String) */ public static Header[] parseHeaders(InputStream is) throws IOException, HttpException { - LOG.trace("enter HeaderParser.parseHeaders(InputStream, String)"); - return parseHeaders(is, "US-ASCII"); + return PARSER.parseHeaders(is); } } Index: src/java/org/apache/commons/httpclient/params/DefaultHttpParams.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/params/DefaultHttpParams.java,v retrieving revision 1.8 diff -u -r1.8 DefaultHttpParams.java --- src/java/org/apache/commons/httpclient/params/DefaultHttpParams.java 13 May 2004 04:01:22 -0000 1.8 +++ src/java/org/apache/commons/httpclient/params/DefaultHttpParams.java 18 Jul 2004 19:13:34 -0000 @@ -32,6 +32,8 @@ import java.io.Serializable; import java.util.HashMap; +import org.apache.commons.httpclient.util.DefaultHttpParser; +import org.apache.commons.httpclient.util.HttpParser; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -250,4 +252,28 @@ clone.setDefaults(this.defaults); return clone; } + + /** + * Defines the HttpParser instance to be used by default. + *

+ * By default, this is {@link org.apache.commons.httpclient.util.DefaultHttpParser}. + *

+ * + * This parameter expects a value of type {@link org.apache.commons.httpclient.util.HttpParser} + */ + public static final String HTTP_PARSER_INSTANCE = "http.parser.instance"; + + /** + * Returns the currently used HttpParser instance. + * + * @return HttpParser instance + */ + public HttpParser getHttpParser() { + HttpParser parser = (HttpParser)getParameter(HTTP_PARSER_INSTANCE); + if(parser == null) { + parser = DefaultHttpParser.INSTANCE; + } + return parser; + } + } Index: src/java/org/apache/commons/httpclient/params/HttpMethodParams.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/params/HttpMethodParams.java,v retrieving revision 1.13 diff -u -r1.13 HttpMethodParams.java --- src/java/org/apache/commons/httpclient/params/HttpMethodParams.java 13 May 2004 04:01:22 -0000 1.13 +++ src/java/org/apache/commons/httpclient/params/HttpMethodParams.java 18 Jul 2004 19:13:35 -0000 @@ -227,7 +227,7 @@ *

* @see java.net.SocketOptions#SO_TIMEOUT */ - public static final String SO_TIMEOUT = "http.socket.timeout"; + public static final String SO_TIMEOUT = "http.socket.timeout"; /** * Creates a new collection of parameters with the collection returned @@ -427,5 +427,4 @@ setParameters(PROTOCOL_STRICTNESS_PARAMETERS, new Boolean(false)); setIntParameter(STATUS_LINE_GARBAGE_LIMIT, Integer.MAX_VALUE); } - } Index: src/test/org/apache/commons/httpclient/SimpleHttpConnection.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/SimpleHttpConnection.java,v retrieving revision 1.18 diff -u -r1.18 SimpleHttpConnection.java --- src/test/org/apache/commons/httpclient/SimpleHttpConnection.java 22 Feb 2004 18:08:49 -0000 1.18 +++ src/test/org/apache/commons/httpclient/SimpleHttpConnection.java 18 Jul 2004 19:13:37 -0000 @@ -40,6 +40,7 @@ import java.util.Vector; import org.apache.commons.httpclient.protocol.Protocol; +import org.apache.commons.httpclient.util.DefaultHttpParser; import org.apache.commons.httpclient.util.EncodingUtil; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -169,16 +170,16 @@ } public void write(byte[] data) - throws IOException, IllegalStateException, HttpRecoverableException { + throws IOException, IllegalStateException, HttpException { } public void writeLine() - throws IOException, IllegalStateException, HttpRecoverableException { + throws IOException, IllegalStateException, HttpException { } public String readLine(String charset) throws IOException, IllegalStateException { - String str = HttpParser.readLine(inputStream, charset); + String str = DefaultHttpParser.INSTANCE.readLine(inputStream, charset); log.debug("read: " + str); return str; } @@ -188,7 +189,7 @@ */ public String readLine() throws IOException, IllegalStateException { - String str = HttpParser.readLine(inputStream); + String str = DefaultHttpParser.INSTANCE.readLine(inputStream); log.debug("read: " + str); return str; } Index: src/test/org/apache/commons/httpclient/TestHttpParser.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestHttpParser.java,v retrieving revision 1.3 diff -u -r1.3 TestHttpParser.java --- src/test/org/apache/commons/httpclient/TestHttpParser.java 22 Feb 2004 18:08:49 -0000 1.3 +++ src/test/org/apache/commons/httpclient/TestHttpParser.java 18 Jul 2004 19:13:37 -0000 @@ -33,7 +33,13 @@ import java.io.ByteArrayInputStream; import java.io.InputStream; -import junit.framework.*; +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +import org.apache.commons.httpclient.util.DefaultHttpParser; +import org.apache.commons.httpclient.util.HttpParser; +import org.apache.commons.httpclient.util.PickyHttpParser; /** * Simple tests for {@link HttpParser}. @@ -62,18 +68,24 @@ return new TestSuite(TestHttpParser.class); } - public void testReadHttpLine() throws Exception { + public void testReadHttpLine_DefaultHttpParser() throws Exception { + testReadHttpLine(DefaultHttpParser.INSTANCE); + } + public void testReadHttpLine_PickyHttpParser() throws Exception { + testReadHttpLine(PickyHttpParser.INSTANCE); + } + private void testReadHttpLine(HttpParser parser) throws Exception { InputStream instream = new ByteArrayInputStream( "\r\r\nstuff\r\n".getBytes(HTTP_ELEMENT_CHARSET)); - assertEquals("\r", HttpParser.readLine(instream, HTTP_ELEMENT_CHARSET)); - assertEquals("stuff", HttpParser.readLine(instream, HTTP_ELEMENT_CHARSET)); - assertEquals(null, HttpParser.readLine(instream, HTTP_ELEMENT_CHARSET)); + assertEquals("\r", parser.readLine(instream, HTTP_ELEMENT_CHARSET)); + assertEquals("stuff", parser.readLine(instream, HTTP_ELEMENT_CHARSET)); + assertEquals(null, parser.readLine(instream, HTTP_ELEMENT_CHARSET)); instream = new ByteArrayInputStream( "\n\r\nstuff\r\n".getBytes("US-ASCII")); - assertEquals("", HttpParser.readLine(instream, HTTP_ELEMENT_CHARSET)); - assertEquals("", HttpParser.readLine(instream, HTTP_ELEMENT_CHARSET)); - assertEquals("stuff", HttpParser.readLine(instream, HTTP_ELEMENT_CHARSET)); - assertEquals(null, HttpParser.readLine(instream, HTTP_ELEMENT_CHARSET)); + assertEquals("", parser.readLine(instream, HTTP_ELEMENT_CHARSET)); + assertEquals("", parser.readLine(instream, HTTP_ELEMENT_CHARSET)); + assertEquals("stuff", parser.readLine(instream, HTTP_ELEMENT_CHARSET)); + assertEquals(null, parser.readLine(instream, HTTP_ELEMENT_CHARSET)); } } Index: src/test/org/apache/commons/httpclient/TestNoHost.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestNoHost.java,v retrieving revision 1.38 diff -u -r1.38 TestNoHost.java --- src/test/org/apache/commons/httpclient/TestNoHost.java 11 May 2004 20:43:55 -0000 1.38 +++ src/test/org/apache/commons/httpclient/TestNoHost.java 18 Jul 2004 19:13:37 -0000 @@ -86,6 +86,7 @@ suite.addTest(TestHttpVersion.suite()); suite.addTest(TestEffectiveHttpVersion.suite()); suite.addTest(TestHttpParser.suite()); + suite.addTest(TestPickyHttpParser.suite()); suite.addTest(TestBadContentLength.suite()); suite.addTest(TestEquals.suite()); suite.addTestSuite(TestIdleConnectionTimeout.class); Index: src/test/org/apache/commons/httpclient/server/SimpleHttpServerConnection.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/server/SimpleHttpServerConnection.java,v retrieving revision 1.9 diff -u -r1.9 SimpleHttpServerConnection.java --- src/test/org/apache/commons/httpclient/server/SimpleHttpServerConnection.java 13 Jun 2004 20:22:20 -0000 1.9 +++ src/test/org/apache/commons/httpclient/server/SimpleHttpServerConnection.java 18 Jul 2004 19:13:37 -0000 @@ -37,8 +37,8 @@ import java.io.UnsupportedEncodingException; import java.net.Socket; -import org.apache.commons.httpclient.HttpParser; import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.util.DefaultHttpParser; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -141,7 +141,7 @@ private void readRequest() throws IOException { String line; do { - line = HttpParser.readLine(in, HTTP_ELEMENT_CHARSET); + line = DefaultHttpParser.INSTANCE.readLine(in, HTTP_ELEMENT_CHARSET); } while (line != null && line.length() == 0); if (line == null) { @@ -153,7 +153,7 @@ try { request = new SimpleRequest( RequestLine.parseLine(line), - HttpParser.parseHeaders(in, HTTP_ELEMENT_CHARSET), + DefaultHttpParser.INSTANCE.parseHeaders(in, HTTP_ELEMENT_CHARSET), null); } catch (IOException e) { connectionClose(); Index: src/java/org/apache/commons/httpclient/util/DefaultHttpParser.java =================================================================== RCS file: src/java/org/apache/commons/httpclient/util/DefaultHttpParser.java diff -N src/java/org/apache/commons/httpclient/util/DefaultHttpParser.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/java/org/apache/commons/httpclient/util/DefaultHttpParser.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,225 @@ +/* + * $Header: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpParser.java,v 1.12 2004/05/13 04:03:25 mbecke Exp $ + * $Revision: 1.12 $ + * $Date: 2004/05/13 04:03:25 $ + * + * ==================================================================== + * + * Copyright 1999-2004 The Apache Software Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * ==================================================================== + * + * This software consists of voluntary contributions made by many + * individuals on behalf of the Apache Software Foundation. For more + * information on the Apache Software Foundation, please see + * . + * + */ + +package org.apache.commons.httpclient.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.ProtocolException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A utility class for parsing http header values. + * + * @author Michael Becke + * @author Oleg Kalnichevski + * + * @since 2.0beta1 + */ +public class DefaultHttpParser implements HttpParser { + public static final DefaultHttpParser INSTANCE = new DefaultHttpParser(); + + /** Log object for this class. */ + private static final Log LOG = LogFactory.getLog(DefaultHttpParser.class); + + /** + * Constructor for HttpParser. + */ + protected DefaultHttpParser() { } + + /** + * Return byte array from an (unchunked) input stream. + * Stop reading when "\n" terminator encountered + * If the stream ends before the line terminator is found, + * the last part of the string will still be returned. + * If no input data available, null is returned + * + * @param inputStream the stream to read from + * + * @throws IOException if an I/O problem occurs + * @return a byte array from the stream + */ + public byte[] readRawLine(InputStream inputStream) throws IOException { + LOG.trace("enter HttpParser.readRawLine()"); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + int ch; + while ((ch = inputStream.read()) >= 0) { + buf.write(ch); + if (ch == '\n') { + break; + } + } + if (buf.size() == 0) { + return null; + } + return buf.toByteArray(); + } + + /** + * Read up to "\n" from an (unchunked) input stream. + * If the stream ends before the line terminator is found, + * the last part of the string will still be returned. + * If no input data available, null is returned + * + * @param inputStream the stream to read from + * @param charset charset of HTTP protocol elements + * + * @throws IOException if an I/O problem occurs + * @return a line from the stream + * + * @since 3.0 + */ + public String readLine(InputStream inputStream, String charset) throws IOException { + LOG.trace("enter HttpParser.readLine(InputStream, String)"); + byte[] rawdata = readRawLine(inputStream); + if (rawdata == null) { + return null; + } + int len = rawdata.length; + int offset = 0; + if (len > 0) { + if (rawdata[len - 1] == '\n') { + offset++; + if (len > 1) { + if (rawdata[len - 2] == '\r') { + offset++; + } + } + } + } + return EncodingUtil.getString(rawdata, 0, len - offset, charset); + } + + /** + * Read up to "\n" from an (unchunked) input stream. + * If the stream ends before the line terminator is found, + * the last part of the string will still be returned. + * If no input data available, null is returned + * + * @param inputStream the stream to read from + * + * @throws IOException if an I/O problem occurs + * @return a line from the stream + * + * @deprecated use #readLine(InputStream, String) + */ + + public String readLine(InputStream inputStream) throws IOException { + LOG.trace("enter HttpParser.readLine(InputStream)"); + return readLine(inputStream, "US-ASCII"); + } + + /** + * Parses headers from the given stream. Headers with the same name are not + * combined. + * + * @param is the stream to read headers from + * @param charset the charset to use for reading the data + * + * @return an array of headers in the order in which they were parsed + * + * @throws IOException if an IO error occurs while reading from the stream + * @throws HttpException if there is an error parsing a header value + * + * @since 3.0 + */ + public Header[] parseHeaders(InputStream is, String charset) throws IOException, HttpException { + LOG.trace("enter HeaderParser.parseHeaders(InputStream, String)"); + + ArrayList headers = new ArrayList(); + String name = null; + StringBuffer value = null; + for (; ;) { + String line = readLine(is, charset); + if ((line == null) || (line.length() < 1)) { + break; + } + + // Parse the header name and value + // Check for folded headers first + // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2 + // discussion on folded headers + if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t')) { + // we have continuation folded header + // so append value + if (value != null) { + value.append(' '); + value.append(line.trim()); + } + } else { + // make sure we save the previous name,value pair if present + if (name != null) { + headers.add(new Header(name, value.toString())); + } + + // Otherwise we should have normal HTTP header line + // Parse the header name and value + int colon = line.indexOf(":"); + if (colon < 0) { + throw new ProtocolException("Unable to parse header: " + line); + } + name = line.substring(0, colon).trim(); + value = new StringBuffer(line.substring(colon + 1).trim()); + } + + } + + // make sure we save the last name,value pair if present + if (name != null) { + headers.add(new Header(name, value.toString())); + } + + return (Header[]) headers.toArray(new Header[headers.size()]); + } + + /** + * Parses headers from the given stream. Headers with the same name are not + * combined. + * + * @param is the stream to read headers from + * + * @return an array of headers in the order in which they were parsed + * + * @throws IOException if an IO error occurs while reading from the stream + * @throws HttpException if there is an error parsing a header value + * + * @deprecated use #parseHeaders(InputStream, String) + */ + public Header[] parseHeaders(InputStream is) throws IOException, HttpException { + LOG.trace("enter HeaderParser.parseHeaders(InputStream, String)"); + return parseHeaders(is, "US-ASCII"); + } +} Index: src/java/org/apache/commons/httpclient/util/HttpParser.java =================================================================== RCS file: src/java/org/apache/commons/httpclient/util/HttpParser.java diff -N src/java/org/apache/commons/httpclient/util/HttpParser.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/java/org/apache/commons/httpclient/util/HttpParser.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,91 @@ +package org.apache.commons.httpclient.util; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpException; + +/** + * An interface for utility classes for parsing http header values. + * + * @author Michael Becke + * @author Oleg Kalnichevski + * @author Christian Kohlschuetter + */ +public interface HttpParser { + /** + * Return byte array from an (unchunked) input stream. + * Stop reading when "\n" terminator encountered + * If the stream ends before the line terminator is found, + * the last part of the string will still be returned. + * If no input data available, null is returned + * + * @param inputStream the stream to read from + * + * @throws IOException if an I/O problem occurs + * @return a byte array from the stream + */ + public abstract byte[] readRawLine(InputStream inputStream) + throws IOException; + + /** + * Read up to "\n" from an (unchunked) input stream. + * If the stream ends before the line terminator is found, + * the last part of the string will still be returned. + * If no input data available, null is returned + * + * @param inputStream the stream to read from + * @param charset The charset to be used for converting + * + * @throws IOException if an I/O problem occurs + * @return a line from the stream + */ + public abstract String readLine(InputStream inputStream, String charset) + throws IOException; + + /** + * Read up to "\n" from an (unchunked) input stream. + * If the stream ends before the line terminator is found, + * the last part of the string will still be returned. + * If no input data available, null is returned + * + * @param inputStream the stream to read from + * + * @throws IOException if an I/O problem occurs + * @return a line from the stream + * @deprecated use #readLine(InputStream, String) + */ + public abstract String readLine(InputStream inputStream) throws IOException; + + /** + * Parses headers from the given stream. Headers with the same name are not + * combined. + * + * @param is the stream to read headers from + * + * @return an array of headers in the order in which they were parsed + * + * @throws IOException if an IO error occurs while reading from the stream + * @throws HttpException if there is an error parsing a header value + * @deprecated Use #parseHeaders(InputStream, String) + */ + public abstract Header[] parseHeaders(InputStream is) + throws IOException, HttpException; + + /** + * Parses headers from the given stream. Headers with the same name are not + * combined. + * + * @param is the stream to read headers from + * @param charset the charset to use for reading the data + * + * @return an array of headers in the order in which they were parsed + * + * @throws IOException if an IO error occurs while reading from the stream + * @throws HttpException if there is an error parsing a header value + * + * @since 3.0 + */ + public abstract Header[] parseHeaders(InputStream is, String charset) throws IOException, HttpException; +} Index: src/java/org/apache/commons/httpclient/util/PickyHttpParser.java =================================================================== RCS file: src/java/org/apache/commons/httpclient/util/PickyHttpParser.java diff -N src/java/org/apache/commons/httpclient/util/PickyHttpParser.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/java/org/apache/commons/httpclient/util/PickyHttpParser.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,173 @@ +package org.apache.commons.httpclient.util; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; + +import org.apache.commons.httpclient.Header; +import org.apache.commons.httpclient.HttpException; +import org.apache.commons.httpclient.ProtocolException; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A utility class for parsing http header values. + * + * This implementation differs from {@link DefaultHttpParser} + * in specially treating unusually long response headers. + * + * There is a default instance available with preset limits, and you + * can define limits by creating your own instances. + * + * A HttpException is thrown if at least one of these limits is exceeded. + * + * @author Christian Kohlschuetter + * @author Michael Becke + * @author Oleg Kalnichevski + */ +public class PickyHttpParser extends DefaultHttpParser { + /** + * The default PickyHttpParser instance with the follwing settings: + *

+ * Maximum length of an HTTP header name: 256
+ * Maximum length of an HTTP header line: 64k
+ * Maximum length of an HTTP header value (can span over several lines): 256k
+ * Maximum total length of a request/response header section: 1M
+ * Maximum number of headers: 1000
+ */ + public static final HttpParser INSTANCE = new PickyHttpParser(); + + /** Log object for this class. */ + private final Log LOG = LogFactory.getLog(PickyHttpParser.class); + + /** Maximum length of an HTTP header name */ + private final int nameLengthLimit; + + /** Maximum length of an HTTP header value (can span over several lines) */ + private final int valueLengthLimit; + + /** Maximum length of an HTTP header line */ + private final int lineLengthLimit; + + /** Maximum number of headers */ + private final int numHeadersLimit; + + /** Maximum total length of a request/response header section */ + private final int headersLengthLimit; + + private PickyHttpParser() { + this(256, 64*1024, 256 * 1024, 1 * 1024 * 1024, 1000); + } + + /** + * Creates a new custom PickyHttpParser + * + * @param nameLengthLimit Maximum length of an HTTP header name + * @param lineLengthLimit Maximum length of an HTTP header line + * @param valueLengthLimit Maximum length of an HTTP header value (can span over several lines) + * @param headersLengthLimit Maximum total length of a request/response header section + * @param numHeadersLimit Maximum number of headers + */ + public PickyHttpParser(int nameLengthLimit, int lineLengthLimit, int valueLengthLimit, int headersLengthLimit, int numHeadersLimit) { + super(); + this.nameLengthLimit = nameLengthLimit + 1; // including the colon + this.lineLengthLimit = lineLengthLimit; + this.valueLengthLimit = valueLengthLimit; + this.headersLengthLimit = headersLengthLimit; + this.numHeadersLimit = numHeadersLimit; + } + + public byte[] readRawLine(InputStream inputStream) throws IOException { + LOG.trace("enter HttpParser.readRawLine()"); + + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + int ch; + int n = 0; + while ((ch = inputStream.read()) >= 0) { + buf.write(ch); + n++; + if (ch == '\n') { + break; + } + if(n > lineLengthLimit) { + throw new HttpException("Line too long (> "+lineLengthLimit+" bytes)"); + } + } + if (buf.size() == 0) { + return null; + } + return buf.toByteArray(); + } + + public Header[] parseHeaders(InputStream is, String charset) throws IOException, HttpException { + LOG.trace("enter HeaderParser.parseHeaders(InputStream, String)"); + + ArrayList headers = new ArrayList(); + String name = null; + StringBuffer value = null; + + int headersLength = 0; + + while(true) { + if(headers.size() > numHeadersLimit) { + throw new HttpException("Too many headers (> "+numHeadersLimit+")"); + } + String line = readLine(is, charset); + if ((line == null) || (line.length() < 1)) { + break; + } + + headersLength += line.length(); + if(headersLength > headersLengthLimit) { + throw new HttpException("Header section too long (> "+headersLengthLimit+")"); + } + + // Parse the header name and value + // Check for folded headers first + // Detect LWS-char see HTTP/1.0 or HTTP/1.1 Section 2.2 + // discussion on folded headers + if ((line.charAt(0) == ' ') || (line.charAt(0) == '\t')) { + // we have continuation folded header + // so append value + if (value != null) { + value.append(' '); + value.append(line.trim()); + if(value.length() > valueLengthLimit) { + throw new HttpException("Header value too long (> "+valueLengthLimit+")"); + } + } + } else { + // make sure we save the previous name,value pair if present + if (name != null) { + headers.add(new Header(name, value.toString())); + } + + // Otherwise we should have normal HTTP header line + // Parse the header name and value + int colon = line.indexOf(":"); + if (colon < 0) { + throw new ProtocolException("Unable to parse header: " + line); + } + if (colon > nameLengthLimit) { + throw new ProtocolException("Header name too long (> "+nameLengthLimit+")"); + } + name = line.substring(0, colon).trim(); + value = new StringBuffer(line.substring(colon + 1).trim()); + if(value.length() > valueLengthLimit) { + throw new HttpException("Header value too long (> "+valueLengthLimit+")"); + } + } + + } + + // make sure we save the last name,value pair if present + if (name != null) { + headers.add(new Header(name, value.toString())); + if(headers.size() > numHeadersLimit) { + throw new HttpException("Too many headers (> "+numHeadersLimit+")"); + } + } + + return (Header[]) headers.toArray(new Header[headers.size()]); + } +} Index: src/test/org/apache/commons/httpclient/TestPickyHttpParser.java =================================================================== RCS file: src/test/org/apache/commons/httpclient/TestPickyHttpParser.java diff -N src/test/org/apache/commons/httpclient/TestPickyHttpParser.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ src/test/org/apache/commons/httpclient/TestPickyHttpParser.java 1 Jan 1970 00:00:00 -0000 @@ -0,0 +1,300 @@ +package org.apache.commons.httpclient; + +import java.io.IOException; + +import junit.framework.Test; +import junit.framework.TestSuite; + +import org.apache.commons.httpclient.methods.GetMethod; +import org.apache.commons.httpclient.params.HttpMethodParams; +import org.apache.commons.httpclient.server.HttpRequestHandler; +import org.apache.commons.httpclient.server.RequestLine; +import org.apache.commons.httpclient.server.ResponseWriter; +import org.apache.commons.httpclient.server.SimpleHttpServer; +import org.apache.commons.httpclient.server.SimpleHttpServerConnection; +import org.apache.commons.httpclient.server.SimpleRequest; +import org.apache.commons.httpclient.util.PickyHttpParser; +import org.apache.commons.httpclient.util.TimeoutController; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * Tests HttpClient's behaviour when receiving bad response headers + * using the {@link PickyHttpParser}. + *

+ * A very simple HTTP Server will be setup on a free local port during testing, which + * returns bad/malicious response headers. + *

+ * + * @author Christian Kohlschuetter + */ +public class TestPickyHttpParser extends TestNoHost { + private static final Log LOG = LogFactory.getLog(SimpleHttpServer.class); + + private HttpClient client = null; + private SimpleHttpServer server = null; + + // ------------------------------------------------------------ Constructor + public TestPickyHttpParser(String testName) { + super(testName); + } + + // ------------------------------------------------------------------- Main + public static void main(String args[]) { + String[] testCaseName = { TestPickyHttpParser.class.getName()}; + junit.textui.TestRunner.main(testCaseName); + } + + // ------------------------------------------------------- TestCase Methods + + public static Test suite() { + return new TestSuite(TestPickyHttpParser.class); + } + + // ----------------------------------------------------------- Test Methods + + public void setUp() throws IOException { + /* + * uncomment to enable wire log + */ + /* + System.setProperty( + "org.apache.commons.logging.Log", + "org.apache.commons.logging.impl.SimpleLog"); + System.setProperty( + "org.apache.commons.logging.simplelog.showdatetime", + "true"); + System.setProperty( + "org.apache.commons.logging.simplelog.log.httpclient.wire", + "debug"); + System.setProperty( + "org.apache.commons.logging.simplelog.log.org.apache.commons.httpclient", + "debug"); + */ + client = new HttpClient(); + + // Uncomment the following line to see DefaultHttpParser failing all the tests + client.getParams().setParameter(HttpMethodParams.HTTP_PARSER_INSTANCE, PickyHttpParser.INSTANCE); + + server = new SimpleHttpServer(); // use arbitrary port + server.setRequestHandler(new MyHttpRequestHandler()); + } + + public void tearDown() throws IOException { + client = null; + + server.destroy(); + } + + /* + public void testForDebuggingOnly() + throws InterruptedException { + while (server.isRunning()) { + Thread.sleep(100); + } + } + */ + + /** + * HttpClient connects to the test server and performs a + * request. + * + * The server responds to the request and sends a HTTP/1.0 200 OK + * followed by a Header with infinite number of data. + * + * Expected behavior: + * HTTPClient should detect that the header line is too long and + * throw a HttpException. + * + * @throws IOException + */ + public void testLongHeaderLine() throws IOException { + client.getParams().makeLenient(); + + final long waitMax = 3 * 1000; // [ms] + FetchJob fj = + new FetchJob( + client, + "http://" + + server.getLocalAddress() + + ":" + + server.getLocalPort() + + "/long_line"); + + try { + TimeoutController.execute(fj, waitMax); + } catch (TimeoutController.TimeoutException e) { + throw new IOException("FetchJob timeout"); + } + + Throwable t = fj.getThrowable(); + if (t != null) { + if (t instanceof HttpException) { + LOG.info("Received expected HttpException", t); + return; + } + fail("Unexpected error in FetchJob: " + t.toString()); + } else { + fail("Did not receive expected HttpException"); + } + } + + /** + * HttpClient connects to the test server and performs a + * request. + * + * The server responds to the request and sends a HTTP/1.0 200 OK + * followed by an infinite number of Headers. + * + * Expected behavior: + * HTTPClient should detect that too many headers were received and + * throw a HttpException. + * + * @throws IOException + */ + public void testInfiniteHeaderLines() throws IOException { + client.getParams().makeLenient(); + + final long waitMax = 3 * 1000; // [ms] + FetchJob fj = + new FetchJob( + client, + "http://" + + server.getLocalAddress() + + ":" + + server.getLocalPort() + + "/infinite_lines"); + + try { + TimeoutController.execute(fj, waitMax); + } catch (TimeoutController.TimeoutException e) { + throw new IOException("FetchJob timeout"); + } + + Throwable t = fj.getThrowable(); + if (t != null) { + if (t instanceof HttpException) { + LOG.info("Received expected HttpException", t); + return; + } + fail("Unexpected error in FetchJob: " + t.toString()); + } else { + fail("Did not receive expected HttpException"); + } + } + + /** + * HttpClient connects to the test server and performs a + * request. + * + * The server responds to the request and sends a HTTP/1.0 200 OK + * followed by a Header with infinite number of folded lines. + * + * Expected behavior: + * HTTPClient should detect that the Header is too long and + * throw a HttpException. + * + * @throws IOException + */ + public void testInfinitelyFoldedHeaderLine() throws IOException { + client.getParams().makeLenient(); + + final long waitMax = 3 * 1000; // [ms] + FetchJob fj = + new FetchJob( + client, + "http://" + + server.getLocalAddress() + + ":" + + server.getLocalPort() + + "/infinitely_folded_header"); + + try { + TimeoutController.execute(fj, waitMax); + } catch (TimeoutController.TimeoutException e) { + throw new IOException("FetchJob timeout"); + } + + Throwable t = fj.getThrowable(); + if (t != null) { + if (t instanceof HttpException) { + LOG.info("Received expected HttpException", t); + return; + } + fail("Unexpected error in FetchJob: " + t.toString()); + } else { + fail("Did not receive expected HttpException"); + } + } + + private class MyHttpRequestHandler implements HttpRequestHandler { + public boolean processRequest(SimpleHttpServerConnection conn, SimpleRequest request) + throws IOException { + RequestLine requestLine = request.getRequestLine(); + ResponseWriter out = conn.getWriter(); + if ("GET".equals(requestLine.getMethod())) { + + if ("/long_line".equals(requestLine.getUri())) { + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: text/html"); + out.print("Nonsense: "); + while (true) { + out.print("12345"); + out.flush(); + } + } else if ("/infinite_lines".equals(requestLine.getUri())) { + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: text/html"); + while (true) { + out.println("Nonsense: 12345"); + out.flush(); + } + } else if ("/infinitely_folded_header".equals(requestLine.getUri())) { + out.println("HTTP/1.1 200 OK"); + out.println("Content-Type: text/html"); + out.print("Nonsense: "); + while (true) { + for(int i=0;i<8;i++) { + out.print("0123456789"); + } + out.println(); + out.flush(); + out.print(" "); + } + } + } + + return false; + } + } + + private class FetchJob implements Runnable { + private Throwable throwable = null; + private HttpClient client; + private String uri; + + public FetchJob(HttpClient client, String uri) { + this.client = client; + this.uri = uri; + } + + public Throwable getThrowable() { + return throwable; + } + + public void run() { + GetMethod method = null; + try { + method = new GetMethod(uri); + client.executeMethod(method); + } catch (Throwable t) { + throwable = t; + } finally { + if (method != null) { + method.releaseConnection(); + } + } + } + } + +}