View Javadoc

1   /**********************************************************************
2    * HttpHeaderParser.java
3    * created on 02.04.2006 by netseeker
4    * $Source: /cvsroot/ejoe/EJOE/src/de/netseeker/ejoe/http/HttpHeaderParser.java,v $
5    * $Date: 2007/11/17 10:57:01 $
6    * $Revision: 1.2 $
7    *
8    * ====================================================================
9    *
10   *  Copyright 2006 netseeker aka Michael Manske
11   *
12   *  Licensed under the Apache License, Version 2.0 (the "License");
13   *  you may not use this file except in compliance with the License.
14   *  You may obtain a copy of the License at
15   *
16   *      http://www.apache.org/licenses/LICENSE-2.0
17   *
18   *  Unless required by applicable law or agreed to in writing, software
19   *  distributed under the License is distributed on an "AS IS" BASIS,
20   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21   *  See the License for the specific language governing permissions and
22   *  limitations under the License.
23   * ====================================================================
24   *
25   * This file is part of the de.netseeker.ejoe.http framework.
26   * For more information on the author, please see
27   * <http://www.manskes.de/>.
28   *
29   *********************************************************************/
30  
31  package de.netseeker.ejoe.http;
32  
33  import java.nio.ByteBuffer;
34  import java.nio.CharBuffer;
35  import java.nio.charset.Charset;
36  import java.util.logging.Level;
37  import java.util.logging.Logger;
38  import java.util.regex.Matcher;
39  import java.util.regex.Pattern;
40  
41  import de.netseeker.ejoe.cache.ByteBufferAllocator;
42  
43  /***
44   * This is the main HTTP header parser of EJOE. It's able to validate an ByteBuffer if it contains a valid HTTP header
45   * and can detect:
46   * <ul>
47   * <li>the end of the HTTP header</li>
48   * <li>the length of the HTTP header in bytes</li>
49   * <li>upcoming content length</li>
50   * </ul>
51   * It's not possible to create an instance directly, instead use HttpRequestParser or HttpResponseParser to create
52   * concrete parser instances.
53   * 
54   * @author netseeker
55   * @since 0.3.9.1
56   */
57  public class HttpHeaderParser
58  {
59      private static final Logger    logger                 = Logger.getLogger( HttpHeaderParser.class.getName() );
60  
61      protected static final String  LINE_SEP               = "\r\n";
62  
63      protected static final Charset csISO                  = Charset.forName( "ISO-8859-1" );
64  
65      protected static final Pattern pLineEnd               = Pattern.compile( "$", Pattern.MULTILINE );
66  
67      protected static final Pattern pContentLength         = Pattern.compile( ".*^Content-Length://s([^//s]+)$.*",
68                                                                               Pattern.CASE_INSENSITIVE
69                                                                                       | Pattern.MULTILINE
70                                                                                       | Pattern.DOTALL );
71  
72      protected static final Pattern pConnection            = Pattern.compile( ".*^Connection://s([^//s]+)$.*",
73                                                                               Pattern.CASE_INSENSITIVE
74                                                                                       | Pattern.MULTILINE
75                                                                                       | Pattern.DOTALL );
76  
77      private int                    contentLength;
78  
79      private int                    headerLength;
80  
81      private CharBuffer             charHeader;
82  
83      private ByteBuffer             byteHeader;
84  
85      private boolean                hasPrereadContent      = false;
86  
87      private boolean                isPersistentConnection = false;
88  
89      private boolean                hasCompression         = false;
90  
91      /***
92       * Hidden constructor, only child classes are permitted to create a new instance
93       * 
94       * @param buf ByteBuffer expected to contain a HTTP header [ + optional preread content ]
95       */
96      protected HttpHeaderParser(ByteBuffer buf)
97      {
98          this.byteHeader = buf;
99          this.charHeader = csISO.decode( buf );
100         this.byteHeader.rewind();
101         this.contentLength = extractContentLength();
102         this.isPersistentConnection = isConnectionPersistent();
103         this.headerLength = extractHeaderLength( buf );
104         this.hasPrereadContent = (byteHeader.limit() > headerLength);
105         if ( this.hasPrereadContent )
106         {
107             resizeByteHeaderForFurtherReading();
108         }
109     }
110 
111     /***
112      * Returns the content length
113      * 
114      * @return the content length
115      */
116     public int getContentLength()
117     {
118         return contentLength;
119     }
120 
121     /***
122      * Returns the length of the HTTP header in bytes without any preread content
123      * 
124      * @return the length of the header
125      */
126     public int getHeaderLength()
127     {
128         return headerLength;
129     }
130 
131     /***
132      * Checks if the HTTP header seems to be valid
133      * 
134      * @return true if the HTTP header seems to be valid (at least for EJOE) otherwise false
135      */
136     public boolean isValid()
137     {
138         return ((headerLength > 0) && (contentLength >= 0));
139     }
140 
141     /***
142      * @return The decoded underlying buffer
143      */
144     public CharBuffer getCharHeader()
145     {
146         return charHeader;
147     }
148 
149     /***
150      * @return the underlying ByteBuffer
151      */
152     public ByteBuffer getByteHeader()
153     {
154         return byteHeader;
155     }
156 
157     /***
158      * @return true if the underlying ByteBuffer already contains one or more preread bytes of content data
159      */
160     public boolean hasPrereadContent()
161     {
162         return hasPrereadContent;
163     }
164 
165     /***
166      * Indicates whether the value for the HTTP header line "Connection:" was "close" or "open".
167      * 
168      * @return true if the HTTP header did contain a "Connection:" header line and the value was "open" otherwise false
169      */
170     public boolean isPersistentConnection()
171     {
172         return isPersistentConnection;
173     }
174 
175     /***
176      * Returns the compression setting in the HTTP-Header.
177      * 
178      * @return true if the HTTP header requested use of GZIP compression otherwise false
179      */
180     public boolean hasCompression()
181     {
182         return hasCompression;
183     }
184 
185     protected void setCompression( boolean enable )
186     {
187         hasCompression = enable;
188     }
189 
190     /***
191      * Determines the length of the expected content as stated in the "Content-Length:" section of the header
192      * 
193      * @return the content length
194      */
195     protected int extractContentLength()
196     {
197         try
198         {
199             Matcher matcher = pContentLength.matcher( getCharHeader() );
200             if ( matcher.matches() )
201             {
202                 return Integer.parseInt( matcher.group( 1 ).trim() );
203             }
204 
205         }
206         catch ( Exception e )
207         {
208             logger.log( Level.WARNING, "Failed to determine content length!", e );
209         }
210 
211         return 0;
212     }
213 
214     /***
215      * Checks if the client has requested a persistent connection
216      * 
217      * @return true if the client requested a persitent connection otherwise false
218      */
219     protected boolean isConnectionPersistent()
220     {
221         try
222         {
223             Matcher matcher = pConnection.matcher( getCharHeader() );
224             if ( matcher.matches() )
225             {
226                 return matcher.group( 1 ).trim().equalsIgnoreCase( "keep-alive" );
227             }
228         }
229         catch ( Exception e )
230         {
231             logger.log( Level.WARNING, "Failed to determine content length!", e );
232         }
233 
234         return false;
235     }
236 
237     /***
238      * Resizes and repositions the internal ByteBuffer for upcoming read operations which will append content data to
239      * the buffer
240      */
241     private void resizeByteHeaderForFurtherReading()
242     {
243         this.byteHeader.position( headerLength );
244         this.byteHeader.compact();
245         if ( this.byteHeader.capacity() < getContentLength() )
246         {
247             this.byteHeader = ByteBufferAllocator.reAllocate( this.byteHeader, getContentLength() );
248         }
249         if ( this.byteHeader.limit() != getContentLength() )
250         {
251             this.byteHeader.limit( getContentLength() );
252         }
253     }
254 
255     /***
256      * Searches the ByteBuffer for two line breaks (either "\r\n\r\n" or "\n\n") to detect the end of HTTP header data
257      * 
258      * @param bb ByteBuffer containing HTTP header data
259      * @return the length of the HTTP header in bytes
260      */
261     protected static int extractHeaderLength( ByteBuffer bb )
262     {
263         int length = -1;
264         if ( bb.limit() >= 3 )
265         {
266             int pos = bb.position();
267             for ( int i = 3; i < bb.limit(); i++ )
268             {
269                 if ( ((bb.get( i - 3 ) == '\r') && (bb.get( i - 2 ) == '\n') && (bb.get( i - 1 ) == '\r') && (bb
270                         .get( i ) == '\n'))
271                         || ((bb.get( i - 1 ) == '\n') && (bb.get( i ) == '\n')) )
272                 {
273                     length = i + 1;
274                     break;
275                 }
276             }
277 
278             bb.position( pos );
279         }
280 
281         return length;
282     }
283 
284     /***
285      * Checks if the HTTP is complete. A header is then complete if either "\r\n\r\n" or "\n\n" are found within the
286      * given ByteBuffer
287      * 
288      * @param bb ByteBuffer containing HTTP header data
289      * @return true if the HTTP header is complete (the buffer contains a terminated HTTP header [ + optional prearead
290      *         content ]) otherwise false
291      */
292     public static boolean isComplete( ByteBuffer bb )
293     {
294         int pos = bb.position();
295         int limit = bb.limit();
296         bb.flip();
297         boolean complete = (extractHeaderLength( bb ) != -1);
298         bb.limit( limit );
299         bb.position( pos );
300 
301         return complete;
302     }
303 }