You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@commons.apache.org by ol...@apache.org on 2003/05/08 19:33:53 UTC

cvs commit: jakarta-commons/httpclient/src/test/org/apache/commons/httpclient NoncompliantHeadMethod.java SimpleHttpConnection.java TestStreams.java TestWebappNoncompliant.java

olegk       2003/05/08 10:33:53

  Modified:    httpclient/src/java/org/apache/commons/httpclient
                        ChunkedInputStream.java HttpConnection.java
                        HttpMethodBase.java
                        MultiThreadedHttpConnectionManager.java
               httpclient/src/java/org/apache/commons/httpclient/methods
                        HeadMethod.java
               httpclient/src/test/org/apache/commons/httpclient
                        SimpleHttpConnection.java TestStreams.java
                        TestWebappNoncompliant.java
  Added:       httpclient/src/test/org/apache/commons/httpclient
                        NoncompliantHeadMethod.java
  Log:
  Bug fix #19235 (Problem with redirect on HEAD when (bad, naughty) server returns body content)
  
  Changelog:
  
  - Minor ChunkedInputStream class clean-ups.
  - HttpExcption is thrown in the strict mode when chunk-encoded body has been declared with 'Transfer-Encoding' header but not sent.
  - Per default HttpClient does not check for presence of HTTP HEAD response body. Such check can be optionally activated on an individual HEAD method when necessary. In the strict mode presence of a content body in response to HTTP HEAD request will cause an HttpException to be thrown.
  
  Contributed by Oleg Kalnichevski
  
  Revision  Changes    Path
  1.16      +26 -36    jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ChunkedInputStream.java
  
  Index: ChunkedInputStream.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/ChunkedInputStream.java,v
  retrieving revision 1.15
  retrieving revision 1.16
  diff -u -r1.15 -r1.16
  --- ChunkedInputStream.java	16 Feb 2003 13:08:32 -0000	1.15
  +++ ChunkedInputStream.java	8 May 2003 17:33:51 -0000	1.16
  @@ -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
  
  
  
  1.65      +47 -8     jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java
  
  Index: HttpConnection.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpConnection.java,v
  retrieving revision 1.64
  retrieving revision 1.65
  diff -u -r1.64 -r1.65
  --- HttpConnection.java	7 May 2003 03:30:36 -0000	1.64
  +++ HttpConnection.java	8 May 2003 17:33:51 -0000	1.65
  @@ -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 <tt>true</tt> if input data is availble, 
        *                 <tt>false</tt> otherwise.
  @@ -836,11 +837,51 @@
        */
       public boolean isResponseAvailable() 
           throws IOException {
  +        LOG.trace("enter HttpConnection.isResponseAvailable()");
           assertOpen();
           return this.inputStream.available() > 0;
       }
   
       /**
  +     * 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 <tt>true</tt> if input data is availble, 
  +     *                 <tt>false</tt> otherwise.
  +     * 
  +     * @throws IOException If an IO problem occurs
  +     * @throws IllegalStateException If the connection isn't open.
  +     */
  +    public boolean isResponseAvailable(int timeout) 
  +        throws IOException {
  +        LOG.trace("enter HttpConnection.isResponseAvailable(int)");
  +        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);
  +                    LOG.debug("Input data available");
  +                    result = true;
  +                } else {
  +                    LOG.debug("Input data not available");
  +                }
  +            } catch (InterruptedIOException e) {
  +                if (LOG.isDebugEnabled()) {
  +                    LOG.debug("Input data not available after " + timeout + " ms");
  +                }
  +            } finally {
  +                socket.setSoTimeout(soTimeout);
  +            }
  +        }
  +        return result;
  +    }
  +
  +    /**
        * Write the specified bytes to my output stream.
        *
        * @param data the data to be written
  @@ -977,10 +1018,9 @@
       }
   
       /**
  -     * Read up to <tt>"\r\n"</tt> from my (unchunked) input stream.
  +     * Read up to <tt>"\n"</tt> 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
  @@ -1354,5 +1394,4 @@
       
       /** the connection manager that created this connection or null */
       private HttpConnectionManager httpConnectionManager;
  -
   }
  
  
  
  1.145     +15 -5     jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java
  
  Index: HttpMethodBase.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/HttpMethodBase.java,v
  retrieving revision 1.144
  retrieving revision 1.145
  diff -u -r1.144 -r1.145
  --- HttpMethodBase.java	2 May 2003 22:06:10 -0000	1.144
  +++ HttpMethodBase.java	8 May 2003 17:33:51 -0000	1.145
  @@ -1966,7 +1966,17 @@
           // 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 {
  +                    if (this.isStrictMode()) {
  +                        throw new HttpException("Chunk-encoded body declared but not sent");
  +                    } else {
  +                        LOG.warn("Chunk-encoded body missing");
  +                    }
  +                }
               }
           } else {
               int expectedLength = getResponseContentLength();
  
  
  
  1.16      +11 -3     jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java
  
  Index: MultiThreadedHttpConnectionManager.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/MultiThreadedHttpConnectionManager.java,v
  retrieving revision 1.15
  retrieving revision 1.16
  diff -u -r1.15 -r1.16
  --- MultiThreadedHttpConnectionManager.java	27 Apr 2003 19:43:41 -0000	1.15
  +++ MultiThreadedHttpConnectionManager.java	8 May 2003 17:33:52 -0000	1.16
  @@ -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();
  
  
  
  1.18      +48 -8     jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/HeadMethod.java
  
  Index: HeadMethod.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/java/org/apache/commons/httpclient/methods/HeadMethod.java,v
  retrieving revision 1.17
  retrieving revision 1.18
  diff -u -r1.17 -r1.18
  --- HeadMethod.java	2 Feb 2003 04:30:13 -0000	1.17
  +++ HeadMethod.java	8 May 2003 17:33:52 -0000	1.18
  @@ -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 <a href="mailto:remm@apache.org">Remy Maucherat</a>
    * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
    * @author <a href="mailto:jsdever@apache.org">Jeff Dever</a>
  + * @author <a href="mailto:oleg@ural.ru">oleg Kalnichevski</a>
    *
    * @version $Revision$
    * @since 1.0
  @@ -101,6 +103,8 @@
       /** Log object for this class. */
       private static final Log LOG = LogFactory.getLog(HeadMethod.class);
   
  +    private int bodyCheckTimeout = -1; /* Disabled per default */
  +
       //~ Constructors �����������������������������������������������������������
   
       /**
  @@ -163,10 +167,46 @@
       throws IOException {
           LOG.trace(
               "enter HeadMethod.readResponseBody(HttpState, HttpConnection)");
  +        
  +        if (this.bodyCheckTimeout < 0) {
  +            responseBodyConsumed();
  +        } else {
  +            if (LOG.isDebugEnabled()) {
  +                LOG.debug("Check for non-compliant response body. Timeout in " 
  +                 + this.bodyCheckTimeout + " ms");    
  +            }
  +            if (conn.isResponseAvailable(this.bodyCheckTimeout)) {
  +                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. <tt>-1</tt> returned when 
  +     *         non-compliant response body check is disabled
  +	 */
  +    public int getBodyCheckTimeout() {
  +        return this.bodyCheckTimeout;
  +    }
  +
  +	/**
  +     * 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. <tt>-1</tt> can be used to 
  +     *         disable non-compliant response body check 
  +	 */
  +    public void setBodyCheckTimeout(int timeout) {
  +        this.bodyCheckTimeout = timeout;
  +    }
  +
   }
  
  
  
  1.15      +13 -5     jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/SimpleHttpConnection.java
  
  Index: SimpleHttpConnection.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/SimpleHttpConnection.java,v
  retrieving revision 1.14
  retrieving revision 1.15
  diff -u -r1.14 -r1.15
  --- SimpleHttpConnection.java	27 Apr 2003 19:43:42 -0000	1.14
  +++ SimpleHttpConnection.java	8 May 2003 17:33:53 -0000	1.15
  @@ -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();
       }
  -
   }
   
  
  
  
  1.11      +3 -16     jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestStreams.java
  
  Index: TestStreams.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestStreams.java,v
  retrieving revision 1.10
  retrieving revision 1.11
  diff -u -r1.10 -r1.11
  --- TestStreams.java	13 Feb 2003 21:31:53 -0000	1.10
  +++ TestStreams.java	8 May 2003 17:33:53 -0000	1.11
  @@ -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);
  
  
  
  1.5       +35 -1     jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestWebappNoncompliant.java
  
  Index: TestWebappNoncompliant.java
  ===================================================================
  RCS file: /home/cvs/jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/TestWebappNoncompliant.java,v
  retrieving revision 1.4
  retrieving revision 1.5
  diff -u -r1.4 -r1.5
  --- TestWebappNoncompliant.java	16 Mar 2003 00:09:18 -0000	1.4
  +++ TestWebappNoncompliant.java	8 May 2003 17:33:53 -0000	1.5
  @@ -128,5 +128,39 @@
       }
   
   
  -    
  +    /** 
  +     * 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");
  +          method.setBodyCheckTimeout(50);
  +          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 testNoncompliantHeadStrictMode() 
  +      throws Exception {
  +          HttpClient client = createHttpClient();
  +          client.setStrictMode(true);
  +          HeadMethod method = new NoncompliantHeadMethod("/" + getWebappContext() + "/body");
  +          method.setBodyCheckTimeout(50);
  +          try {
  +              client.executeMethod(method);
  +              fail("HttpException should have been thrown"); 
  +          } catch(HttpException e) {
  +              // Expected
  +          }
  +          method.releaseConnection();
  +    }
  +
   }
  
  
  
  1.1                  jakarta-commons/httpclient/src/test/org/apache/commons/httpclient/NoncompliantHeadMethod.java
  
  Index: NoncompliantHeadMethod.java
  ===================================================================
  /*
   * ====================================================================
   *
   * 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
   * <http://www.apache.org/>.
   *
   * [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";
  	}
  
  }
  
  
  

---------------------------------------------------------------------
To unsubscribe, e-mail: commons-dev-unsubscribe@jakarta.apache.org
For additional commands, e-mail: commons-dev-help@jakarta.apache.org