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 }