Index: 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.15 diff -u -r1.15 ChunkedInputStream.java --- java/org/apache/commons/httpclient/ChunkedInputStream.java 16 Feb 2003 13:08:32 -0000 1.15 +++ java/org/apache/commons/httpclient/ChunkedInputStream.java 6 May 2003 07:44:44 -0000 @@ -102,6 +102,9 @@ /** The current position within the current chunk */ private int pos; + /** True if we'are at the beginning of stream */ + private boolean bof = true; + /** True if we've reached the end of stream */ private boolean eof = false; @@ -122,19 +125,14 @@ public ChunkedInputStream( final InputStream in, final HttpMethod method) throws IOException { - if (null == in) { - throw new NullPointerException("InputStream parameter"); + if (in == null) { + throw new IllegalArgumentException("InputStream parameter may not be null"); } - if (null == method) { - throw new NullPointerException("HttpMethod parameter"); + if (method == null) { + throw new IllegalArgumentException("HttpMethod parameter may not be null"); } this.in = in; this.method = method; - this.chunkSize = getChunkSizeFromInputStream(in, false); - if (chunkSize == 0) { - eof = true; - parseTrailerHeaders(); - } this.pos = 0; } @@ -215,17 +213,29 @@ } /** - * Read the next chunk. + * Read the CRLF terminator. * @throws IOException If an IO error occurs. */ - private void nextChunk() throws IOException { + private void readCRLF() throws IOException { int cr = in.read(); int lf = in.read(); if ((cr != '\r') || (lf != '\n')) { throw new IOException( "CRLF expected at end of chunk: " + cr + "/" + lf); } + } + + + /** + * Read the next chunk. + * @throws IOException If an IO error occurs. + */ + private void nextChunk() throws IOException { + if (!bof) { + readCRLF(); + } chunkSize = getChunkSizeFromInputStream(in); + bof = false; pos = 0; if (chunkSize == 0) { eof = true; @@ -246,8 +256,7 @@ * * @throws IOException when the chunk size could not be parsed */ - private static int getChunkSizeFromInputStream( - final InputStream in, boolean required) + private static int getChunkSizeFromInputStream(final InputStream in) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); @@ -256,11 +265,7 @@ while (state != -1) { int b = in.read(); if (b == -1) { - if (required) { - throw new IOException("chunked stream ended unexpectedly"); - } else { - return 0; - } + throw new IOException("chunked stream ended unexpectedly"); } switch (state) { case 0: @@ -319,21 +324,6 @@ return result; } - /** - * Expects the stream to start with a chunksize in hex with optional - * comments after a semicolon. The line must end with a CRLF: "a3; some - * comment\r\n" Positions the stream at the start of the next line. - * - * @param in The new input stream. - * - * @return the chunk size as integer - * - * @throws IOException when the chunk size could not be parsed - */ - private static int getChunkSizeFromInputStream(final InputStream in) - throws IOException { - return getChunkSizeFromInputStream(in, true); - } /** * Reads and stores the Trailer headers. * @throws IOException If an IO problem occurs Index: 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.63 diff -u -r1.63 HttpConnection.java --- java/org/apache/commons/httpclient/HttpConnection.java 1 May 2003 23:21:27 -0000 1.63 +++ java/org/apache/commons/httpclient/HttpConnection.java 6 May 2003 07:44:45 -0000 @@ -826,7 +826,8 @@ } /** - * Tests if input data avaialble. + * Tests if input data avaialble. This method returns immediately + * and does not perform any read operations on the input socket * * @return boolean true if input data is availble, * false otherwise. @@ -841,6 +842,39 @@ } /** + * Tests if input data becomes available within the given period time in milliseconds. + * + * @param timeout The number milliseconds to wait for input data to become available + * @return boolean true if input data is availble, + * false otherwise. + * + * @throws IOException If an IO problem occurs + * @throws IllegalStateException If the connection isn't open. + */ + public boolean isResponseAvailable(int timeout) + throws IOException { + assertOpen(); + boolean result = false; + if (this.inputStream.available() > 0) { + result = true; + } else { + try { + this.socket.setSoTimeout(timeout); + int byteRead = inputStream.read(); + if (byteRead != -1) { + inputStream.unread(byteRead); + result = true; + } + } catch (InterruptedIOException e) { + //ignore + } finally { + socket.setSoTimeout(soTimeout); + } + } + return result; + } + + /** * Write the specified bytes to my output stream. * * @param data the data to be written @@ -977,10 +1011,9 @@ } /** - * Read up to "\r\n" from my (unchunked) input stream. + * Read up to "\n" from my (unchunked) input stream. * If the stream ends before the line terminator is found, * the last part of the string will still be returned. - * '\r' and '\n' are allowed to appear individually in the stream. * * @throws IllegalStateException if I am not connected * @throws IOException if an I/O problem occurs @@ -1352,5 +1385,4 @@ /** the connection manager that created this connection or null */ private HttpConnectionManager httpConnectionManager; - } Index: 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.144 diff -u -r1.144 HttpMethodBase.java --- java/org/apache/commons/httpclient/HttpMethodBase.java 2 May 2003 22:06:10 -0000 1.144 +++ java/org/apache/commons/httpclient/HttpMethodBase.java 6 May 2003 07:44:46 -0000 @@ -1966,7 +1966,11 @@ // RFC2616, 4.4 item number 3 if (transferEncodingHeader != null) { if ("chunked".equalsIgnoreCase(transferEncodingHeader.getValue())) { - result = new ChunkedInputStream(is, this); + // Some HTTP servers do not bother sending a closing chunk + // if response body is empty + if (conn.isResponseAvailable(conn.getSoTimeout())) { + result = new ChunkedInputStream(is, this); + } } } else { int expectedLength = getResponseContentLength(); Index: java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java,v retrieving revision 1.15 diff -u -r1.15 MultiThreadedHttpConnectionManager.java --- java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java 27 Apr 2003 19:43:41 -0000 1.15 +++ java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java 6 May 2003 07:44:47 -0000 @@ -831,6 +831,14 @@ } } + public boolean isResponseAvailable(int timeout) throws IOException { + if (hasConnection()) { + return wrappedConnection.isResponseAvailable(timeout); + } else { + return false; + } + } + public boolean isSecure() { if (hasConnection()) { return wrappedConnection.isSecure(); Index: java/org/apache/commons/httpclient/methods/HeadMethod.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/HeadMethod.java,v retrieving revision 1.17 diff -u -r1.17 HeadMethod.java --- java/org/apache/commons/httpclient/methods/HeadMethod.java 2 Feb 2003 04:30:13 -0000 1.17 +++ java/org/apache/commons/httpclient/methods/HeadMethod.java 6 May 2003 07:44:47 -0000 @@ -66,6 +66,7 @@ import java.io.IOException; import org.apache.commons.httpclient.HttpConnection; +import org.apache.commons.httpclient.HttpException; import org.apache.commons.httpclient.HttpMethodBase; import org.apache.commons.httpclient.HttpState; import org.apache.commons.logging.Log; @@ -91,6 +92,7 @@ * @author Remy Maucherat * @author Mike Bowler * @author Jeff Dever + * @author oleg Kalnichevski * * @version $Revision: 1.17 $ * @since 1.0 @@ -101,6 +103,8 @@ /** Log object for this class. */ private static final Log LOG = LogFactory.getLog(HeadMethod.class); + private int bodyReadTimeout = 100; + //~ Constructors ··························································· /** @@ -163,10 +167,46 @@ throws IOException { LOG.trace( "enter HeadMethod.readResponseBody(HttpState, HttpConnection)"); + + if (this.bodyReadTimeout < 0) { + responseBodyConsumed(); + } else { + if (LOG.isDebugEnabled()) { + LOG.debug("Check for non-compliant response body. Timeout in " + + this.bodyReadTimeout + " ms"); + } + if (conn.isResponseAvailable(this.bodyReadTimeout)) { + if (isStrictMode()) { + throw new HttpException( + "Body content may not be sent in response to HTTP HEAD request"); + } else { + LOG.warn("Body content returned in response to HTTP HEAD"); + } + super.readResponseBody(state, conn); + } + } - // despite the possible presence of a content-length header, - // HEAD returns no response body - responseBodyConsumed(); - return; } + /** + * Return non-compliant response body check timeout. + * + * @return The period of time in milliseconds to wait for a response + * body from a non-compliant server. -1 returned when + * non-compliant response body check is disabled + */ + public int getBodyReadTimeout() { + return this.bodyReadTimeout; + } + + /** + * Set non-compliant response body check timeout. + * + * @param timeout The period of time in milliseconds to wait for a response + * body from a non-compliant server. -1 can be used to + * disable non-compliant response body check + */ + public void setBodyReadTimeout(int timeout) { + this.bodyReadTimeout = timeout; + } + } Index: test/org/apache/commons/httpclient/NoncompliantHeadMethod.java =================================================================== RCS file: test/org/apache/commons/httpclient/NoncompliantHeadMethod.java diff -N test/org/apache/commons/httpclient/NoncompliantHeadMethod.java --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ test/org/apache/commons/httpclient/NoncompliantHeadMethod.java 6 May 2003 07:44:47 -0000 @@ -0,0 +1,90 @@ +/* + * ==================================================================== + * + * The Apache Software License, Version 1.1 + * + * Copyright (c) 2003 The Apache Software Foundation. All rights + * reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * 3. The end-user documentation included with the redistribution, if + * any, must include the following acknowlegement: + * "This product includes software developed by the + * Apache Software Foundation (http://www.apache.org/)." + * Alternately, this acknowlegement may appear in the software itself, + * if and wherever such third-party acknowlegements normally appear. + * + * 4. The names "The Jakarta Project", "Commons", and "Apache Software + * Foundation" must not be used to endorse or promote products derived + * from this software without prior written permission. For written + * permission, please contact apache@apache.org. + * + * 5. Products derived from this software may not be called "Apache" + * nor may "Apache" appear in their names without prior written + * permission of the Apache Group. + * + * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED + * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE APACHE SOFTWARE FOUNDATION OR + * ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF + * USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT + * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + * ==================================================================== + * + * 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 + * . + * + * [Additional notices, if required by prior licensing conditions] + * + */ +package org.apache.commons.httpclient; + +import org.apache.commons.httpclient.methods.HeadMethod; + +/** + * HTTP GET methid intended to simulate side-effects of + * interaction with non-compiant HTTP servers or proxies + * + * @author Oleg Kalnichevski + */ + +public class NoncompliantHeadMethod extends HeadMethod { + + public NoncompliantHeadMethod(){ + super(); + } + + public NoncompliantHeadMethod(String uri) { + super(uri); + } + + /** + * Expect HTTP HEAD but perform HTTP GET instead in order to + * simulate the behaviour of a non-compliant HTTP server sending + * body content in response to HTTP HEAD request + * + */ + public String getName() { + return "GET"; + } + +} Index: 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.14 diff -u -r1.14 SimpleHttpConnection.java --- test/org/apache/commons/httpclient/SimpleHttpConnection.java 27 Apr 2003 19:43:42 -0000 1.14 +++ test/org/apache/commons/httpclient/SimpleHttpConnection.java 6 May 2003 07:44:47 -0000 @@ -91,7 +91,7 @@ Vector headers = new Vector(); Vector bodies = new Vector(); - ByteArrayInputStream inputStream; + InputStream inputStream; ByteArrayOutputStream bodyOutputStream = null; @@ -190,6 +190,15 @@ } } + public boolean isResponseAvailable() throws IOException { + assertOpen(); + return inputStream.available() > 0; + } + + public boolean isResponseAvailable(int timeout) throws IOException { + return isResponseAvailable(); + } + public void write(byte[] data) throws IOException, IllegalStateException, HttpRecoverableException { } @@ -216,6 +225,5 @@ public void flushRequestOutputStream() throws IOException { assertOpen(); } - } Index: test/org/apache/commons/httpclient/TestMethodsNoHost.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestMethodsNoHost.java,v retrieving revision 1.17 diff -u -r1.17 TestMethodsNoHost.java --- test/org/apache/commons/httpclient/TestMethodsNoHost.java 17 Apr 2003 11:34:19 -0000 1.17 +++ test/org/apache/commons/httpclient/TestMethodsNoHost.java 6 May 2003 07:44:48 -0000 @@ -210,6 +210,7 @@ // note - this test is here because the HEAD method handler overrides the // standard behavior for reading a response body. HeadMethod headMethod = new HeadMethod("/"); + headMethod.setBodyReadTimeout(-1); conn.addResponse(headers, ""); Index: test/org/apache/commons/httpclient/TestStreams.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestStreams.java,v retrieving revision 1.10 diff -u -r1.10 TestStreams.java --- test/org/apache/commons/httpclient/TestStreams.java 13 Feb 2003 21:31:53 -0000 1.10 +++ test/org/apache/commons/httpclient/TestStreams.java 6 May 2003 07:44:48 -0000 @@ -152,19 +152,6 @@ assertEquals(0, out.size()); } - public void testNoncompliantEmptyChunkedInputStream() throws IOException { - HttpMethod method = new SimpleHttpMethod(); - - InputStream in = new ChunkedInputStream(new ByteArrayInputStream(new byte[] {}), method); - byte[] buffer = new byte[300]; - ByteArrayOutputStream out = new ByteArrayOutputStream(); - int len; - while ((len = in.read(buffer)) > 0) { - out.write(buffer, 0, len); - } - assertEquals(0, out.size()); - } - public void testContentLengthInputStream() throws IOException { String correct = "1234567890123456"; InputStream in = new ContentLengthInputStream(new ByteArrayInputStream(HttpConstants.getBytes(correct)), 10); Index: test/org/apache/commons/httpclient/TestWebappNoncompliant.java =================================================================== RCS file: /home/cvspublic/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestWebappNoncompliant.java,v retrieving revision 1.4 diff -u -r1.4 TestWebappNoncompliant.java --- test/org/apache/commons/httpclient/TestWebappNoncompliant.java 16 Mar 2003 00:09:18 -0000 1.4 +++ test/org/apache/commons/httpclient/TestWebappNoncompliant.java 6 May 2003 07:44:48 -0000 @@ -128,5 +128,37 @@ } - + /** + * Test if a response to HEAD method from non-compliant server + * that contains an unexpected body content can be correctly redirected + */ + + public void testNoncompliantHeadWithResponseBody() + throws Exception { + HttpClient client = createHttpClient(); + HeadMethod method = new NoncompliantHeadMethod("/" + getWebappContext() + "/redirect"); + client.executeMethod(method); + assertEquals(200,method.getStatusCode()); + method.releaseConnection(); + } + + /** + * Test if a response to HEAD method from non-compliant server + * causes an HttpException to be thrown + */ + + public void testNoncompliantHeadStrickMode() + throws Exception { + HttpClient client = createHttpClient(); + client.setStrictMode(true); + HeadMethod method = new NoncompliantHeadMethod("/" + getWebappContext() + "/body"); + try { + client.executeMethod(method); + fail("HttpException should have been thrown"); + } catch(HttpException e) { + // Expected + } + method.releaseConnection(); + } + }